import { and, eq, inArray, isNull, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { berthTenancies, type BerthTenancy } from '@/lib/db/schema/tenancies';
import { berths } from '@/lib/db/schema/berths';
import { clients } from '@/lib/db/schema/clients';
import { files } from '@/lib/db/schema/documents';
import { yachts } from '@/lib/db/schema/yachts';
import { companyMemberships } from '@/lib/db/schema/companies';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { buildListQuery } from '@/lib/db/query-builder';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import type { z } from 'zod';
import { createPendingSchema } from '@/lib/validators/tenancies';
import type {
  ActivateInput,
  EndTenancyInput,
  CancelInput,
  ListTenanciesInput,
  RenewTenancyInput,
  TransferTenancyInput,
  UpdateTenancyInput,
} from '@/lib/validators/tenancies';

// Use z.input so callers (including tests) can omit fields with
// `.default()` like `tenureType`. The service re-parses below to get
// the post-coercion shape Drizzle expects (Date, defaulted tenureType).
type CreatePendingInput = z.input<typeof createPendingSchema>;

export type { BerthTenancy };

// ─── Helpers ─────────────────────────────────────────────────────────────────

/**
 * Returns true if the error is a Postgres unique-violation (SQLSTATE 23505)
 * raised specifically on the `idx_bt_active` partial unique index. Narrowing
 * to this constraint name prevents us from swallowing unrelated unique
 * violations.
 */
function isBerthActiveConflict(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as {
    code?: unknown;
    constraint_name?: unknown;
    constraint?: unknown;
    cause?: { code?: unknown; constraint_name?: unknown; constraint?: unknown };
  };
  const code = e.code ?? e.cause?.code;
  if (code !== '23505') return false;
  const constraint =
    e.constraint_name ?? e.constraint ?? e.cause?.constraint_name ?? e.cause?.constraint;
  return constraint === 'idx_bt_active';
}

/**
 * Cross-references the tenancy's client against the yacht's current owner.
 * Either the yacht is directly owned by the client, OR the client has an
 * active (endDate IS NULL) company_membership on the owning company.
 */
async function assertClientOwnsOrRepresentsYacht(
  yacht: { currentOwnerType: string; currentOwnerId: string },
  clientId: string,
): Promise<void> {
  if (yacht.currentOwnerType === 'client' && yacht.currentOwnerId === clientId) {
    return;
  }

  if (yacht.currentOwnerType === 'company') {
    const membership = await db.query.companyMemberships.findFirst({
      where: and(
        eq(companyMemberships.companyId, yacht.currentOwnerId),
        eq(companyMemberships.clientId, clientId),
        isNull(companyMemberships.endDate),
      ),
    });
    if (membership) return;
  }

  throw new ValidationError('yacht does not belong to tenancy client');
}

async function loadScoped(id: string, portId: string): Promise<BerthTenancy> {
  const row = await db.query.berthTenancies.findFirst({
    where: and(eq(berthTenancies.id, id), eq(berthTenancies.portId, portId)),
  });
  if (!row) throw new NotFoundError('Tenancy');
  return row;
}

// ─── Create (pending) ────────────────────────────────────────────────────────

export async function createPending(
  portId: string,
  data: CreatePendingInput,
  meta: AuditMeta,
): Promise<BerthTenancy & { activeTenancyWarning?: string }> {
  // Tenant-scoped existence checks (berth, client, yacht).
  const berth = await db.query.berths.findFirst({
    where: and(eq(berths.id, data.berthId), eq(berths.portId, portId)),
  });
  if (!berth) throw new ValidationError('berth not found');

  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, data.clientId), eq(clients.portId, portId)),
  });
  if (!client) throw new ValidationError('client not found');

  const yacht = await db.query.yachts.findFirst({
    where: and(eq(yachts.id, data.yachtId), eq(yachts.portId, portId)),
  });
  if (!yacht) throw new ValidationError('yacht not found');

  // Client must own the yacht directly OR be an active member of the owning company.
  await assertClientOwnsOrRepresentsYacht(
    { currentOwnerType: yacht.currentOwnerType, currentOwnerId: yacht.currentOwnerId },
    data.clientId,
  );

  // Soft-block surface: the partial unique index `idx_bt_active` only
  // covers `status='active'`, so any number of `pending` rows can be
  // minted on a berth that already has an active tenant — they'd all
  // ConflictError one-at-a-time at activate. We don't hard-reject here
  // (a queued/back-to-back pending tenancy is a legitimate workflow),
  // but we surface the existing active tenancy in the create response so
  // the UI can warn the rep instead of letting them walk into a
  // dead-end at activation time.
  const activeTenancy = await db.query.berthTenancies.findFirst({
    where: and(
      eq(berthTenancies.berthId, data.berthId),
      eq(berthTenancies.portId, portId),
      eq(berthTenancies.status, 'active'),
    ),
    columns: { id: true },
  });

  // Re-parse to apply coercions/defaults locally - Drizzle's .values()
  // wants the post-coercion shape (Date, defaulted enum), and v4's
  // z.input is too loose to satisfy that.
  const parsed = createPendingSchema.parse(data);

  // Lazy module-enable: wrap in tx + advisory lock so the first-ever
  // tenancy for this port flips `tenancies_module_enabled=true` atomically
  // with the insert. Subsequent inserts no-op the enable call (idempotent
  // UPSERT in the service).
  const tenancy = await db.transaction(async (tx) => {
    await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${portId}, 0))`);

    const [row] = await tx
      .insert(berthTenancies)
      .values({
        portId,
        berthId: parsed.berthId,
        clientId: parsed.clientId,
        yachtId: parsed.yachtId,
        interestId: parsed.interestId ?? null,
        status: 'pending',
        startDate: parsed.startDate,
        tenureType: parsed.tenureType,
        notes: parsed.notes ?? null,
        createdBy: meta.userId,
      })
      .returning();

    // First insert in this port? Flip the module on. We import lazily to
    // avoid a circular import (tenancies-module imports berthTenancies).
    const { enableTenanciesModule } = await import('@/lib/services/tenancies-module.service');
    await enableTenanciesModule(portId);

    return row!;
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'berth_tenancy',
    entityId: tenancy.id,
    newValue: {
      berthId: tenancy.berthId,
      clientId: tenancy.clientId,
      yachtId: tenancy.yachtId,
      status: tenancy.status,
      startDate: tenancy.startDate,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_tenancy:created', {
    tenancyId: tenancy.id,
    berthId: tenancy.berthId,
  });

  if (activeTenancy) {
    return {
      ...tenancy,
      activeTenancyWarning: `This berth already has an active tenancy (${activeTenancy.id}). The new pending tenancy cannot be activated until the existing one is ended, transferred, or cancelled.`,
    };
  }

  return tenancy;
}

// ─── Activate (pending → active) ─────────────────────────────────────────────

export async function activate(
  tenancyId: string,
  portId: string,
  data: ActivateInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);

  if (existing.status !== 'pending') {
    throw new ValidationError(`invalid transition: ${existing.status} → active`);
  }

  const patch: Partial<typeof berthTenancies.$inferInsert> = {
    status: 'active',
    updatedAt: new Date(),
  };
  if (data.contractFileId !== undefined) {
    // Verify the contract file lives in the caller's port. Without this,
    // a port-A user activating a port-A tenancy could attach a
    // port-B file id and downstream presigned-download paths (admin
    // export, portal contract download) would otherwise leak that
    // foreign-port content.
    if (data.contractFileId !== null) {
      const contractFile = await db.query.files.findFirst({
        where: and(eq(files.id, data.contractFileId), eq(files.portId, portId)),
      });
      if (!contractFile) {
        throw new ValidationError('contract file not found in this port');
      }
    }
    patch.contractFileId = data.contractFileId;
  }
  if (data.effectiveDate !== undefined) {
    patch.startDate = data.effectiveDate;
  }

  let updated: BerthTenancy | undefined;
  try {
    const rows = await db
      .update(berthTenancies)
      .set(patch)
      .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)))
      .returning();
    updated = rows[0];
  } catch (err) {
    if (isBerthActiveConflict(err)) {
      const conflicting = await db.query.berthTenancies.findFirst({
        where: and(
          eq(berthTenancies.berthId, existing.berthId),
          eq(berthTenancies.status, 'active'),
          eq(berthTenancies.portId, portId),
        ),
      });
      throw new ConflictError(
        conflicting
          ? `berth already has active tenancy (conflictingTenancyId: ${conflicting.id})`
          : 'berth already has active tenancy',
      );
    }
    throw err;
  }

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_tenancy',
    entityId: tenancyId,
    oldValue: { status: existing.status },
    newValue: { status: 'active', contractFileId: updated!.contractFileId },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_tenancy:activated', {
    tenancyId,
    berthId: updated!.berthId,
  });

  return updated!;
}

// ─── End (active → ended) ────────────────────────────────────────────────────

export async function endTenancy(
  tenancyId: string,
  portId: string,
  data: EndTenancyInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);

  if (existing.status !== 'active') {
    throw new ValidationError(`invalid transition: ${existing.status} → ended`);
  }

  const rows = await db
    .update(berthTenancies)
    .set({
      status: 'ended',
      endDate: data.endDate,
      notes: data.notes ?? existing.notes,
      updatedAt: new Date(),
    })
    .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)))
    .returning();

  const updated = rows[0]!;

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_tenancy',
    entityId: tenancyId,
    oldValue: { status: existing.status },
    newValue: { status: 'ended', endDate: updated.endDate },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_tenancy:ended', {
    tenancyId,
    berthId: updated.berthId,
  });

  return updated;
}

// ─── Cancel (pending|active → cancelled) ─────────────────────────────────────

export async function cancel(
  tenancyId: string,
  portId: string,
  data: CancelInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);

  if (existing.status !== 'pending' && existing.status !== 'active') {
    throw new ValidationError(`invalid transition: ${existing.status} → cancelled`);
  }

  const rows = await db
    .update(berthTenancies)
    .set({
      status: 'cancelled',
      notes: data.reason
        ? `${existing.notes ? `${existing.notes}\n` : ''}Cancelled: ${data.reason}`
        : existing.notes,
      updatedAt: new Date(),
    })
    .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)))
    .returning();

  const updated = rows[0]!;

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_tenancy',
    entityId: tenancyId,
    oldValue: { status: existing.status },
    newValue: { status: 'cancelled', reason: data.reason ?? null },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_tenancy:cancelled', {
    tenancyId,
    berthId: updated.berthId,
  });

  return updated;
}

// ─── Update (metadata only — non-status fields) ──────────────────────────────

export async function updateTenancy(
  tenancyId: string,
  portId: string,
  data: UpdateTenancyInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);

  const patch: Partial<typeof berthTenancies.$inferInsert> = { updatedAt: new Date() };
  if (data.startDate !== undefined) patch.startDate = data.startDate;
  if (data.endDate !== undefined) patch.endDate = data.endDate;
  if (data.tenureType !== undefined) patch.tenureType = data.tenureType;
  if (data.notes !== undefined) patch.notes = data.notes;

  const [updated] = await db
    .update(berthTenancies)
    .set(patch)
    .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)))
    .returning();

  if (!updated) throw new NotFoundError('Tenancy');

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_tenancy',
    entityId: tenancyId,
    oldValue: {
      startDate: existing.startDate,
      endDate: existing.endDate,
      tenureType: existing.tenureType,
      notes: existing.notes,
    },
    newValue: patch,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  // Metadata-only edit — emit `:updated`, NOT `:activated`. The latter
  // would fire false "tenancy activated" toasts and the wrong cache
  // invalidation on connected clients (the status hasn't changed here).
  emitToRoom(`port:${portId}`, 'berth_tenancy:updated', {
    tenancyId,
    berthId: updated.berthId,
  });

  return updated;
}

// ─── Renew (tenure-aware) ────────────────────────────────────────────────────

const PERMANENT_TENURES = new Set(['permanent', 'fee_simple', 'strata_lot']);

/**
 * Renew an active tenancy. Per docs/tenancies-design.md:
 *   - permanent / fee_simple / strata_lot: mutate the existing row in
 *     place (one record forever) — startDate moves forward, endDate may
 *     extend or null out, notes append.
 *   - fixed_term / seasonal: end the current row at its existing
 *     endDate (or now if missing), then mint a successor row with
 *     previousTenancyId pointing back. Successor inherits berth /
 *     client / yacht / interestId / contractFileId; rep can re-edit
 *     via the standard update action.
 */
export async function renewTenancy(
  tenancyId: string,
  portId: string,
  data: RenewTenancyInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);
  if (existing.status !== 'active') {
    throw new ValidationError(
      `Cannot renew a ${existing.status} tenancy — renewals require an active record.`,
    );
  }

  if (PERMANENT_TENURES.has(existing.tenureType)) {
    // Mutate-in-place path. End date can extend (or be removed entirely).
    const [updated] = await db
      .update(berthTenancies)
      .set({
        startDate: data.newStartDate,
        endDate: data.newEndDate ?? null,
        notes: data.notes
          ? `${existing.notes ? `${existing.notes}\n` : ''}Renewal: ${data.notes}`
          : existing.notes,
        updatedAt: new Date(),
      })
      .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)))
      .returning();

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'update',
      entityType: 'berth_tenancy',
      entityId: tenancyId,
      oldValue: { startDate: existing.startDate, endDate: existing.endDate },
      newValue: { startDate: updated!.startDate, endDate: updated!.endDate, kind: 'renew' },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'berth_tenancy:activated', {
      tenancyId,
      berthId: updated!.berthId,
    });
    return updated!;
  }

  // Mint-successor path (fixed_term / seasonal). End the current row
  // first so the partial unique index on (berthId, status='active')
  // doesn't conflict when the new row activates.
  if (!data.newEndDate) {
    throw new ValidationError(
      `Renewals on ${existing.tenureType} tenancies require a new end date.`,
    );
  }

  return db.transaction(async (tx) => {
    const closeDate = existing.endDate ?? new Date();
    await tx
      .update(berthTenancies)
      .set({ status: 'ended', endDate: closeDate, updatedAt: new Date() })
      .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)));

    const [inserted] = await tx
      .insert(berthTenancies)
      .values({
        portId,
        berthId: existing.berthId,
        clientId: existing.clientId,
        yachtId: existing.yachtId,
        interestId: existing.interestId,
        status: 'active',
        startDate: data.newStartDate,
        endDate: data.newEndDate,
        tenureType: existing.tenureType,
        contractFileId: existing.contractFileId,
        previousTenancyId: tenancyId,
        notes: data.notes
          ? `Renewal of #${tenancyId.slice(0, 8)}: ${data.notes}`
          : `Renewal of #${tenancyId.slice(0, 8)}`,
        createdBy: meta.userId,
      })
      .returning();

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'create',
      entityType: 'berth_tenancy',
      entityId: inserted!.id,
      newValue: {
        previousTenancyId: tenancyId,
        kind: 'renew_successor',
        startDate: inserted!.startDate,
        endDate: inserted!.endDate,
      },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'berth_tenancy:created', {
      tenancyId: inserted!.id,
      berthId: inserted!.berthId,
    });
    return inserted!;
  });
}

// ─── Transfer (client/yacht swap) ────────────────────────────────────────────

/**
 * Hand a berth's active tenancy to a new client / yacht pair. The
 * current row is ended at `transferDate`, then a fresh active row is
 * minted with `transferredFromTenancyId` pointing at the predecessor.
 * Berth + tenure type + contractFileId carry over; interestId resets
 * to null (the original deal is closed).
 */
export async function transferTenancy(
  tenancyId: string,
  portId: string,
  data: TransferTenancyInput,
  meta: AuditMeta,
): Promise<BerthTenancy> {
  const existing = await loadScoped(tenancyId, portId);
  if (existing.status !== 'active') {
    throw new ValidationError(
      `Cannot transfer a ${existing.status} tenancy — transfers require an active record.`,
    );
  }

  // Cross-validate new client + yacht live in this port.
  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, data.newClientId), eq(clients.portId, portId)),
  });
  if (!client) throw new ValidationError('new client not found in this port');
  const yacht = await db.query.yachts.findFirst({
    where: and(eq(yachts.id, data.newYachtId), eq(yachts.portId, portId)),
  });
  if (!yacht) throw new ValidationError('new yacht not found in this port');
  await assertClientOwnsOrRepresentsYacht(
    { currentOwnerType: yacht.currentOwnerType, currentOwnerId: yacht.currentOwnerId },
    data.newClientId,
  );

  return db.transaction(async (tx) => {
    await tx
      .update(berthTenancies)
      .set({ status: 'ended', endDate: data.transferDate, updatedAt: new Date() })
      .where(and(eq(berthTenancies.id, tenancyId), eq(berthTenancies.portId, portId)));

    const [inserted] = await tx
      .insert(berthTenancies)
      .values({
        portId,
        berthId: existing.berthId,
        clientId: data.newClientId,
        yachtId: data.newYachtId,
        interestId: null,
        status: 'active',
        startDate: data.transferDate,
        endDate: existing.endDate,
        tenureType: existing.tenureType,
        contractFileId: existing.contractFileId,
        transferredFromTenancyId: tenancyId,
        notes: data.notes
          ? `Transferred from #${tenancyId.slice(0, 8)}: ${data.notes}`
          : `Transferred from #${tenancyId.slice(0, 8)}`,
        createdBy: meta.userId,
      })
      .returning();

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'create',
      entityType: 'berth_tenancy',
      entityId: inserted!.id,
      newValue: {
        transferredFromTenancyId: tenancyId,
        kind: 'transfer',
        previousClientId: existing.clientId,
        newClientId: data.newClientId,
        previousYachtId: existing.yachtId,
        newYachtId: data.newYachtId,
      },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'berth_tenancy:created', {
      tenancyId: inserted!.id,
      berthId: inserted!.berthId,
    });
    return inserted!;
  });
}

// ─── Get ─────────────────────────────────────────────────────────────────────

export async function getById(id: string, portId: string): Promise<BerthTenancy> {
  return loadScoped(id, portId);
}

// ─── List ────────────────────────────────────────────────────────────────────

export async function listTenancies(
  portId: string,
  query: ListTenanciesInput,
): Promise<{ data: BerthTenancy[]; total: number }> {
  const { page, limit, sort, order, search, status, berthId, clientId, yachtId } = query;

  const filters = [];
  if (status) filters.push(eq(berthTenancies.status, status));
  if (berthId) filters.push(eq(berthTenancies.berthId, berthId));
  if (clientId) filters.push(eq(berthTenancies.clientId, clientId));
  if (yachtId) filters.push(eq(berthTenancies.yachtId, yachtId));

  let sortColumn:
    | typeof berthTenancies.startDate
    | typeof berthTenancies.createdAt
    | typeof berthTenancies.updatedAt = berthTenancies.updatedAt;
  if (sort === 'startDate') sortColumn = berthTenancies.startDate;
  else if (sort === 'createdAt') sortColumn = berthTenancies.createdAt;

  const result = await buildListQuery<BerthTenancy>({
    table: berthTenancies,
    portIdColumn: berthTenancies.portId,
    portId,
    idColumn: berthTenancies.id,
    updatedAtColumn: berthTenancies.updatedAt,
    searchColumns: search ? [berthTenancies.notes] : [],
    searchTerm: search,
    filters,
    sort: sort ? { column: sortColumn, direction: order } : undefined,
    page,
    pageSize: limit,
    includeArchived: true,
  });

  return result;
}

// ─── Webhook auto-create (signed Reservation Agreement → pending tenancies) ─

export interface AutoCreateOptions {
  signedAt: Date;
  /** Documents row id that fired the webhook; for audit + linkage. */
  sourceDocumentId: string;
  /** Signed-PDF files row id; mirrored to tenancies.contractFileId when set. */
  signedFileId: string | null;
}

/**
 * Webhook-driven branch: a `reservation_agreement` document just landed
 * `completed`. Materialize one **pending** tenancy per berth covered by the
 * interest's EOI bundle (`interest_berths.is_in_eoi_bundle = true`). The rep
 * then confirms start date + tenure type via the entity-tab UI to flip
 * `pending → active`.
 *
 * The webhook caller does not gate on `isTenanciesModuleEnabled` — the
 * row-exists fallback in that helper lazily surfaces the module from the
 * first successful insert onwards, which is what unlocks the very first
 * tenancy on a fresh port. Service-level idempotency: skips any berth that
 * already has a non-terminal tenancy in `(pending, active)` so a webhook
 * re-delivery never double-mints.
 *
 * Returns the newly-inserted rows (empty array when nothing to mint).
 */
export async function autoCreatePendingTenancies(
  portId: string,
  interestId: string,
  opts: AutoCreateOptions,
): Promise<BerthTenancy[]> {
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
    columns: { id: true, clientId: true, yachtId: true },
  });
  if (!interest) return [];
  if (!interest.yachtId) return [];

  const bundleBerths = await db
    .select({ berthId: interestBerths.berthId })
    .from(interestBerths)
    .where(and(eq(interestBerths.interestId, interestId), eq(interestBerths.isInEoiBundle, true)));

  if (bundleBerths.length === 0) return [];

  // Skip berths that already have a live (pending|active) tenancy — the
  // webhook may fire again after a retry and we never want to double-mint.
  const existingLive = await db
    .select({ berthId: berthTenancies.berthId })
    .from(berthTenancies)
    .where(
      and(
        eq(berthTenancies.portId, portId),
        inArray(berthTenancies.status, ['pending', 'active']),
        inArray(
          berthTenancies.berthId,
          bundleBerths.map((b) => b.berthId),
        ),
      ),
    );
  const liveBerthIds = new Set(existingLive.map((r) => r.berthId));

  const toInsert = bundleBerths.map((b) => b.berthId).filter((id) => !liveBerthIds.has(id));
  if (toInsert.length === 0) return [];

  // pg_advisory_xact_lock keyed off the port keeps concurrent webhooks
  // for the same port from racing on the per-berth idempotency check —
  // the partial unique index `idx_bt_active` is the real safety net but
  // the lock spares us the unique-violation roundtrip on the hot path.
  const inserted = await db.transaction(async (tx) => {
    await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${portId}, 0))`);

    const rows = await tx
      .insert(berthTenancies)
      .values(
        toInsert.map((berthId) => ({
          portId,
          berthId,
          clientId: interest.clientId,
          yachtId: interest.yachtId!,
          interestId,
          status: 'pending',
          startDate: opts.signedAt,
          tenureType: 'permanent',
          contractFileId: opts.signedFileId,
          notes: `Auto-created from signed Reservation Agreement (document ${opts.sourceDocumentId.slice(0, 8)}).`,
          createdBy: 'system',
        })),
      )
      .returning();

    return rows;
  });

  for (const row of inserted) {
    void createAuditLog({
      userId: 'system',
      portId,
      action: 'create',
      entityType: 'berth_tenancy',
      entityId: row.id,
      newValue: {
        berthId: row.berthId,
        clientId: row.clientId,
        yachtId: row.yachtId,
        interestId: row.interestId,
        status: row.status,
        source: 'reservation_agreement_signed',
        sourceDocumentId: opts.sourceDocumentId,
      },
      ipAddress: '0.0.0.0',
      userAgent: 'webhook',
    });
    emitToRoom(`port:${portId}`, 'berth_tenancy:created', {
      tenancyId: row.id,
      berthId: row.berthId,
    });
  }

  return inserted;
}
