import { and, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { yachts, yachtOwnershipHistory, yachtTags, clients } from '@/lib/db/schema';
import type { Yacht } from '@/lib/db/schema/yachts';
import { companies } from '@/lib/db/schema/companies';
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { logger } from '@/lib/logger';
import {
  syncEntityFolderName,
  applyEntityArchivedSuffix,
} from '@/lib/services/document-folders.service';
import { emitToRoom } from '@/lib/socket/server';
import { setEntityTags } from '@/lib/services/entity-tags.helper';
import { diffEntity } from '@/lib/entity-diff';
import { buildListQuery } from '@/lib/db/query-builder';
import { withTransaction } from '@/lib/db/utils';
import type { z } from 'zod';
import type {
  createYachtSchema,
  UpdateYachtInput,
  TransferOwnershipInput,
  ListYachtsInput,
} from '@/lib/validators/yachts';

type CreateYachtInput = z.input<typeof createYachtSchema>;

async function assertOwnerExists(
  portId: string,
  owner: { type: 'client' | 'company'; id: string },
  tx: typeof db,
): Promise<void> {
  if (owner.type === 'client') {
    const client = await tx.query.clients.findFirst({
      where: and(eq(clients.id, owner.id), eq(clients.portId, portId)),
    });
    if (!client) throw new ValidationError('owner not found');
  } else {
    const company = await tx.query.companies.findFirst({
      where: and(eq(companies.id, owner.id), eq(companies.portId, portId)),
    });
    if (!company) throw new ValidationError('owner not found');
  }
}

export async function createYacht(portId: string, data: CreateYachtInput, meta: AuditMeta) {
  return await withTransaction(async (tx) => {
    await assertOwnerExists(portId, data.owner, tx);

    const [yacht] = await tx
      .insert(yachts)
      .values({
        portId,
        name: data.name,
        hullNumber: data.hullNumber ?? null,
        registration: data.registration ?? null,
        flag: data.flag ?? null,
        yearBuilt: data.yearBuilt ?? null,
        builder: data.builder ?? null,
        model: data.model ?? null,
        hullMaterial: data.hullMaterial ?? null,
        lengthFt: data.lengthFt ?? null,
        widthFt: data.widthFt ?? null,
        draftFt: data.draftFt ?? null,
        lengthM: data.lengthM ?? null,
        widthM: data.widthM ?? null,
        draftM: data.draftM ?? null,
        currentOwnerType: data.owner.type,
        currentOwnerId: data.owner.id,
        status: data.status ?? 'active',
        notes: data.notes ?? null,
        // Phase 3c - origin tracking. Defaults to 'manual' at the DB
        // level; pass-through allows the EOI spawn flow to mark the row
        // as 'eoi-generated' with the generating document_id.
        source: data.source ?? 'manual',
        sourceDocumentId: data.sourceDocumentId ?? null,
      })
      .returning();

    await tx.insert(yachtOwnershipHistory).values({
      yachtId: yacht!.id,
      ownerType: data.owner.type,
      ownerId: data.owner.id,
      startDate: new Date(),
      endDate: null,
      createdBy: meta.userId,
    });

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'create',
      entityType: 'yacht',
      entityId: yacht!.id,
      newValue: { name: yacht!.name, owner: data.owner },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'yacht:created', { yachtId: yacht!.id });

    return yacht!;
  });
}

export async function getYachtById(id: string, portId: string) {
  const yacht = await db.query.yachts.findFirst({
    where: and(eq(yachts.id, id), eq(yachts.portId, portId)),
    with: {
      tags: { with: { tag: true } },
    },
  });
  if (!yacht) throw new NotFoundError('Yacht');
  const { tags: tagJoins, ...rest } = yacht as typeof yacht & {
    tags: Array<{ tag: { id: string; name: string; color: string } }>;
  };

  // Aggregated note count for the Notes tab badge. Mirrors the
  // symmetric-reach used by the NotesList that renders below it.
  const { countForYachtAggregated } = await import('@/lib/services/notes.service');
  const noteCount = await countForYachtAggregated(portId, id).catch(() => 0);

  return {
    ...rest,
    tags: tagJoins.map((t) => t.tag),
    noteCount,
  };
}

export async function updateYacht(
  id: string,
  portId: string,
  data: UpdateYachtInput,
  meta: AuditMeta,
) {
  // Defense-in-depth: owner changes must go through /transfer, not PATCH.
  const dataRecord = data as Record<string, unknown>;
  if (
    Object.prototype.hasOwnProperty.call(dataRecord, 'currentOwnerType') ||
    Object.prototype.hasOwnProperty.call(dataRecord, 'currentOwnerId')
  ) {
    throw new ValidationError('use /transfer to change ownership');
  }

  const existing = await db.query.yachts.findFirst({
    where: eq(yachts.id, id),
  });

  if (!existing || existing.portId !== portId) {
    throw new NotFoundError('Yacht');
  }

  const { diff } = diffEntity(toAuditJson(existing), data as Record<string, unknown>);

  const [updated] = await db
    .update(yachts)
    .set({ ...data, updatedAt: new Date() })
    .where(and(eq(yachts.id, id), eq(yachts.portId, portId)))
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'yacht',
    entityId: id,
    oldValue: diff as Record<string, unknown>,
    newValue: data as Record<string, unknown>,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'yacht:updated', {
    yachtId: id,
    changedFields: Object.keys(diff),
  });

  if (data.name !== undefined) {
    await syncEntityFolderName(portId, 'yacht', id, meta.userId).catch((err) => {
      logger.warn({ err, yachtId: id, portId }, 'Failed to sync yacht folder name');
    });
  }

  return updated!;
}

export async function archiveYacht(id: string, portId: string, meta: AuditMeta) {
  const existing = await db.query.yachts.findFirst({
    where: eq(yachts.id, id),
  });

  if (!existing || existing.portId !== portId) {
    throw new NotFoundError('Yacht');
  }

  // NOTE: bypassing the shared `softDelete(...)` util: it sets the raw
  // column key `archived_at`, which Drizzle does not recognise (the JS
  // key is `archivedAt`) and therefore emits an empty SET clause. Until
  // the utility is fixed, do the update inline.
  await db
    .update(yachts)
    .set({ archivedAt: new Date() })
    .where(and(eq(yachts.id, id), eq(yachts.portId, portId)));

  void applyEntityArchivedSuffix(portId, 'yacht', id, meta.userId).catch((err) => {
    logger.warn({ err, yachtId: id, portId }, 'Failed to apply archived suffix to yacht folder');
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'archive',
    entityType: 'yacht',
    entityId: id,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'yacht:archived', { yachtId: id });
}

/**
 * Transaction-aware ownership transfer. Performs the FULL ledger move —
 * closes the open `yacht_ownership_history` row, opens a new one for the
 * new owner, and updates the yacht's denormalized current-owner columns —
 * so the history ledger and the denormalized columns never drift apart.
 *
 * Use this from any flow that moves a yacht to a new owner inside a
 * transaction (smart-archive / restore included). The public
 * `transferOwnership` wraps this in its own tx + audit/socket emissions.
 *
 * NOTE: callers are responsible for any same-owner / owner-exists
 * validation they need; this helper intentionally does only the ledger
 * write so it stays composable. `effectiveDate` defaults to now() when
 * omitted (archive/restore have no operator-chosen date).
 */
export async function transferOwnershipTx(
  tx: typeof db,
  args: {
    yachtId: string;
    newOwner: { type: 'client' | 'company'; id: string };
    effectiveDate?: Date;
    transferReason?: string | null;
    transferNotes?: string | null;
    createdBy: string;
  },
): Promise<Yacht> {
  const effectiveDate = args.effectiveDate ?? new Date();

  // Close the currently-active history row (endDate IS NULL → guarded by
  // the idx_yoh_active partial unique index, so there's at most one).
  await tx
    .update(yachtOwnershipHistory)
    .set({ endDate: effectiveDate })
    .where(
      and(
        eq(yachtOwnershipHistory.yachtId, args.yachtId),
        sql`${yachtOwnershipHistory.endDate} IS NULL`,
      ),
    );

  // Open the new active row for the incoming owner.
  await tx.insert(yachtOwnershipHistory).values({
    yachtId: args.yachtId,
    ownerType: args.newOwner.type,
    ownerId: args.newOwner.id,
    startDate: effectiveDate,
    endDate: null,
    transferReason: args.transferReason ?? null,
    transferNotes: args.transferNotes ?? null,
    createdBy: args.createdBy,
  });

  // Update denormalized current-owner columns to match the ledger head.
  const [updated] = await tx
    .update(yachts)
    .set({
      currentOwnerType: args.newOwner.type,
      currentOwnerId: args.newOwner.id,
      updatedAt: new Date(),
    })
    .where(eq(yachts.id, args.yachtId))
    .returning();

  return updated!;
}

export async function transferOwnership(
  yachtId: string,
  portId: string,
  data: TransferOwnershipInput,
  meta: AuditMeta,
) {
  return await withTransaction(async (tx) => {
    const yacht = await tx.query.yachts.findFirst({
      where: and(eq(yachts.id, yachtId), eq(yachts.portId, portId)),
    });
    if (!yacht) throw new NotFoundError('Yacht');

    if (
      yacht.currentOwnerType === data.newOwner.type &&
      yacht.currentOwnerId === data.newOwner.id
    ) {
      throw new ValidationError('same owner - nothing to transfer');
    }

    await assertOwnerExists(portId, data.newOwner, tx);

    // Resolve old + new owner names so the audit log row reads as a
    // sentence ("Matt transferred owner from Smith to Jones") rather
    // than a generic "updated this record." Resolution mirrors the
    // assertOwnerExists pattern — same client/company tables, scoped
    // to the same port.
    const resolveOwnerName = async (
      ownerType: string | null,
      ownerId: string | null,
    ): Promise<string | null> => {
      if (!ownerType || !ownerId) return null;
      if (ownerType === 'client') {
        const row = await tx.query.clients.findFirst({
          where: and(eq(clients.id, ownerId), eq(clients.portId, portId)),
          columns: { fullName: true },
        });
        return row?.fullName ?? null;
      }
      if (ownerType === 'company') {
        const row = await tx.query.companies.findFirst({
          where: and(eq(companies.id, ownerId), eq(companies.portId, portId)),
          columns: { name: true },
        });
        return row?.name ?? null;
      }
      return null;
    };
    const [oldOwnerName, newOwnerName] = await Promise.all([
      resolveOwnerName(yacht.currentOwnerType, yacht.currentOwnerId),
      resolveOwnerName(data.newOwner.type, data.newOwner.id),
    ]);

    // Close the open history row, open the new one, and sync the
    // denormalized columns — all via the shared tx-aware helper so the
    // ledger invariant holds for every transfer pathway.
    const updated = await transferOwnershipTx(tx, {
      yachtId,
      newOwner: data.newOwner,
      effectiveDate: data.effectiveDate,
      transferReason: data.transferReason ?? null,
      transferNotes: data.transferNotes ?? null,
      createdBy: meta.userId,
    });

    // Audit log shape designed for the EntityActivityFeed sentence
    // formatter: a discrete `transfer` action + human-readable owner
    // names render as "Matt transferred owner from X to Y" instead of
    // the generic "updated this record." Reason + new-owner-type
    // ride along in metadata for downstream consumers that need the
    // structured form.
    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'transfer',
      entityType: 'yacht',
      entityId: yachtId,
      fieldChanged: 'owner',
      // oldValue/newValue are Record<string, unknown> in the audit schema;
      // wrap the owner-name strings in a `name` field so the type matches
      // and the feed's `formatValueForField` can pluck the readable label.
      oldValue: oldOwnerName ? { name: oldOwnerName } : undefined,
      newValue: newOwnerName ? { name: newOwnerName } : undefined,
      metadata: {
        newOwnerType: data.newOwner.type,
        newOwnerId: data.newOwner.id,
        reason: data.transferReason ?? null,
        notes: data.transferNotes ?? null,
      },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'yacht:ownership_transferred', {
      yachtId,
      newOwner: data.newOwner,
    });

    return updated!;
  });
}

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

export async function listYachts(portId: string, query: ListYachtsInput) {
  const { page, limit, sort, order, search, includeArchived, ownerType, ownerId, status } = query;

  const filters = [];
  if (ownerType) filters.push(eq(yachts.currentOwnerType, ownerType));
  if (ownerId) filters.push(eq(yachts.currentOwnerId, ownerId));
  if (status) filters.push(eq(yachts.status, status));

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

  const result = await buildListQuery<Yacht>({
    table: yachts,
    portIdColumn: yachts.portId,
    portId,
    idColumn: yachts.id,
    updatedAtColumn: yachts.updatedAt,
    searchColumns: [yachts.name, yachts.hullNumber, yachts.registration],
    searchTerm: search,
    filters,
    sort: sort ? { column: sortColumn, direction: order } : undefined,
    page,
    pageSize: limit,
    includeArchived,
    archivedAtColumn: yachts.archivedAt,
  });

  if (result.data.length === 0) return result;

  // Resolve current owner names in two parallel batched queries instead of
  // an N+1 fetch from the client (was 1 round-trip per row from yacht-columns).
  const clientIds = result.data
    .filter((y) => y.currentOwnerType === 'client')
    .map((y) => y.currentOwnerId);
  const companyIds = result.data
    .filter((y) => y.currentOwnerType === 'company')
    .map((y) => y.currentOwnerId);

  const [clientRows, companyRows] = await Promise.all([
    clientIds.length > 0
      ? db
          .select({ id: clients.id, fullName: clients.fullName })
          .from(clients)
          .where(inArray(clients.id, clientIds))
      : Promise.resolve([] as { id: string; fullName: string }[]),
    companyIds.length > 0
      ? db
          .select({ id: companies.id, name: companies.name })
          .from(companies)
          .where(inArray(companies.id, companyIds))
      : Promise.resolve([] as { id: string; name: string }[]),
  ]);

  const clientNames = new Map(clientRows.map((r) => [r.id, r.fullName]));
  const companyNames = new Map(companyRows.map((r) => [r.id, r.name]));

  return {
    ...result,
    data: result.data.map((y) => ({
      ...y,
      currentOwnerName:
        y.currentOwnerType === 'client'
          ? (clientNames.get(y.currentOwnerId) ?? null)
          : (companyNames.get(y.currentOwnerId) ?? null),
    })),
  };
}

// ─── List for owner ───────────────────────────────────────────────────────────

export async function listYachtsForOwner(
  portId: string,
  ownerType: 'client' | 'company',
  ownerId: string,
) {
  // Owner-detail tabs only surface active yachts. Archived ones live in the
  // ownership history view and are reachable by id, not via this lister.
  return await db.query.yachts.findMany({
    where: and(
      eq(yachts.portId, portId),
      eq(yachts.currentOwnerType, ownerType),
      eq(yachts.currentOwnerId, ownerId),
      isNull(yachts.archivedAt),
    ),
    orderBy: (t, { desc }) => [desc(t.updatedAt)],
  });
}

// ─── Ownership history ────────────────────────────────────────────────────────

export async function listOwnershipHistory(yachtId: string, portId: string) {
  // First scope-check the yacht (throws NotFoundError if cross-tenant)
  await getYachtById(yachtId, portId);
  return await db.query.yachtOwnershipHistory.findMany({
    where: eq(yachtOwnershipHistory.yachtId, yachtId),
    orderBy: (t, { desc }) => [desc(t.startDate)],
  });
}

// ─── Autocomplete ─────────────────────────────────────────────────────────────

export async function autocomplete(portId: string, q: string) {
  // Empty query returns the top 20 most-recently-updated yachts so the
  // picker has something useful to show the moment it opens, instead of
  // a dead-end empty state until the rep types something.
  if (!q) {
    return await db
      .select()
      .from(yachts)
      .where(eq(yachts.portId, portId))
      .orderBy(desc(yachts.updatedAt))
      .limit(20);
  }
  const pattern = `%${q}%`;
  return await db
    .select()
    .from(yachts)
    .where(
      and(
        eq(yachts.portId, portId),
        or(
          ilike(yachts.name, pattern),
          ilike(yachts.hullNumber, pattern),
          ilike(yachts.registration, pattern),
        ),
      ),
    )
    .limit(10);
}

export async function setYachtTags(
  yachtId: string,
  portId: string,
  tagIds: string[],
  meta: AuditMeta,
) {
  const yacht = await db.query.yachts.findFirst({ where: eq(yachts.id, yachtId) });
  if (!yacht || yacht.portId !== portId) throw new NotFoundError('Yacht');

  await setEntityTags({
    joinTable: yachtTags,
    entityColumn: yachtTags.yachtId,
    tagColumn: yachtTags.tagId,
    entityId: yachtId,
    portId,
    tagIds,
    meta,
    entityType: 'yacht',
  });
}
