import { and, count, eq, ilike, inArray, isNull } from 'drizzle-orm';

import { db } from '@/lib/db';
import {
  clients,
  clientContacts,
  clientRelationships,
  clientTags,
  clientAddresses,
} from '@/lib/db/schema/clients';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import { yachts } from '@/lib/db/schema/yachts';
import { berthReservations } from '@/lib/db/schema/reservations';
import { tags } from '@/lib/db/schema/system';
import { createAuditLog } from '@/lib/audit';
import { NotFoundError } from '@/lib/errors';
import { isPortalEnabledForPort } from '@/lib/services/portal-auth.service';
import { emitToRoom } from '@/lib/socket/server';
import { buildListQuery } from '@/lib/db/query-builder';
import { diffEntity } from '@/lib/entity-diff';
import { softDelete, restore, withTransaction } from '@/lib/db/utils';
import type {
  CreateClientInput,
  UpdateClientInput,
  ListClientsInput,
} from '@/lib/validators/clients';

// ─── Types ────────────────────────────────────────────────────────────────────

interface AuditMeta {
  userId: string;
  portId: string;
  ipAddress: string;
  userAgent: string;
}

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

export async function listClients(portId: string, query: ListClientsInput) {
  const { page, limit, sort, order, search, includeArchived, source, nationality, tagIds } = query;

  const filters = [];

  if (source) {
    filters.push(eq(clients.source, source));
  }
  if (nationality) {
    // Filter accepts an ISO-3166-1 alpha-2 code.
    filters.push(eq(clients.nationalityIso, nationality.toUpperCase()));
  }
  if (tagIds && tagIds.length > 0) {
    const clientsWithTags = await db
      .selectDistinct({ clientId: clientTags.clientId })
      .from(clientTags)
      .where(inArray(clientTags.tagId, tagIds));
    const matchingIds = clientsWithTags.map((r) => r.clientId);
    if (matchingIds.length > 0) {
      filters.push(inArray(clients.id, matchingIds));
    } else {
      // No clients match these tags — return empty
      return { data: [], total: 0 };
    }
  }

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

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

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

  const ids = result.data.map((r) => r.id);

  const [yachtCounts, companyCounts] = await Promise.all([
    db
      .select({ ownerId: yachts.currentOwnerId, count: count() })
      .from(yachts)
      .where(
        and(
          eq(yachts.portId, portId),
          eq(yachts.currentOwnerType, 'client'),
          inArray(yachts.currentOwnerId, ids),
          isNull(yachts.archivedAt),
        ),
      )
      .groupBy(yachts.currentOwnerId),
    db
      .select({ clientId: companyMemberships.clientId, count: count() })
      .from(companyMemberships)
      .where(and(inArray(companyMemberships.clientId, ids), isNull(companyMemberships.endDate)))
      .groupBy(companyMemberships.clientId),
  ]);

  const yachtCountMap = new Map(yachtCounts.map((r) => [r.ownerId, r.count]));
  const companyCountMap = new Map(companyCounts.map((r) => [r.clientId, r.count]));

  return {
    ...result,
    data: result.data.map((row) => ({
      ...row,
      yachtCount: yachtCountMap.get(row.id) ?? 0,
      companyCount: companyCountMap.get(row.id) ?? 0,
    })),
  };
}

// ─── Get by ID ────────────────────────────────────────────────────────────────

export async function getClientById(id: string, portId: string) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, id),
  });

  if (!client || client.portId !== portId) {
    throw new NotFoundError('Client');
  }

  const contacts = await db.query.clientContacts.findMany({
    where: eq(clientContacts.clientId, id),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });

  const addresses = await db.query.clientAddresses.findMany({
    where: eq(clientAddresses.clientId, id),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });

  const clientTagRows = await db
    .select({ tag: tags })
    .from(clientTags)
    .innerJoin(tags, eq(clientTags.tagId, tags.id))
    .where(eq(clientTags.clientId, id));

  const yachtRows = await db.query.yachts.findMany({
    where: and(
      eq(yachts.portId, portId),
      eq(yachts.currentOwnerType, 'client'),
      eq(yachts.currentOwnerId, id),
      isNull(yachts.archivedAt),
    ),
    columns: {
      id: true,
      name: true,
      hullNumber: true,
      registration: true,
      lengthFt: true,
      widthFt: true,
      status: true,
    },
  });

  const membershipRows = await db
    .select({
      membershipId: companyMemberships.id,
      role: companyMemberships.role,
      isPrimary: companyMemberships.isPrimary,
      startDate: companyMemberships.startDate,
      company: {
        id: companies.id,
        name: companies.name,
        legalName: companies.legalName,
        status: companies.status,
      },
    })
    .from(companyMemberships)
    .innerJoin(companies, eq(companyMemberships.companyId, companies.id))
    .where(
      and(
        eq(companyMemberships.clientId, id),
        eq(companies.portId, portId),
        isNull(companyMemberships.endDate),
      ),
    );

  const activeReservations = await db.query.berthReservations.findMany({
    where: and(
      eq(berthReservations.clientId, id),
      eq(berthReservations.portId, portId),
      eq(berthReservations.status, 'active'),
    ),
    columns: {
      id: true,
      berthId: true,
      yachtId: true,
      startDate: true,
      tenureType: true,
      status: true,
    },
  });

  const portalEnabled = await isPortalEnabledForPort(portId);

  return {
    ...client,
    contacts,
    addresses,
    tags: clientTagRows.map((r) => r.tag),
    yachts: yachtRows,
    companies: membershipRows,
    activeReservations,
    clientPortalEnabled: portalEnabled,
  };
}

// ─── Create ───────────────────────────────────────────────────────────────────

export async function createClient(portId: string, data: CreateClientInput, meta: AuditMeta) {
  const result = await withTransaction(async (tx) => {
    const { contacts: contactsInput, tagIds, ...clientData } = data;

    const [client] = await tx
      .insert(clients)
      .values({ portId, ...clientData })
      .returning();

    if (contactsInput.length > 0) {
      await tx
        .insert(clientContacts)
        .values(contactsInput.map((c) => ({ clientId: client!.id, ...c })));
    }

    if (tagIds && tagIds.length > 0) {
      await tx.insert(clientTags).values(tagIds.map((tagId) => ({ clientId: client!.id, tagId })));
    }

    return client!;
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'client',
    entityId: result.id,
    newValue: { fullName: result.fullName },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'client:created', {
    clientId: result.id,
    clientName: result.fullName ?? '',
    source: result.source ?? '',
  });

  void import('@/lib/services/webhook-dispatch').then(({ dispatchWebhookEvent }) =>
    dispatchWebhookEvent(portId, 'client:created', { clientId: result.id }),
  );

  return result;
}

// ─── Update ───────────────────────────────────────────────────────────────────

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

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

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

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

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

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

  void import('@/lib/services/webhook-dispatch').then(({ dispatchWebhookEvent }) =>
    dispatchWebhookEvent(portId, 'client:updated', { clientId: id }),
  );

  return updated;
}

// ─── Archive / Restore ────────────────────────────────────────────────────────

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

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

  await softDelete(clients, clients.id, id);

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

  emitToRoom(`port:${portId}`, 'client:archived', { clientId: id });

  void import('@/lib/services/webhook-dispatch').then(({ dispatchWebhookEvent }) =>
    dispatchWebhookEvent(portId, 'client:archived', { clientId: id }),
  );
}

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

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

  await restore(clients, clients.id, id);

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

  emitToRoom(`port:${portId}`, 'client:restored', { clientId: id });
}

// ─── Contacts ─────────────────────────────────────────────────────────────────

export async function listContacts(clientId: string, portId: string) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  return db.query.clientContacts.findMany({
    where: eq(clientContacts.clientId, clientId),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });
}

export async function addContact(
  clientId: string,
  portId: string,
  data: {
    channel: string;
    value: string;
    valueE164?: string | null;
    valueCountry?: string | null;
    label?: string;
    isPrimary?: boolean;
    notes?: string;
  },
  meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const [contact] = await db
    .insert(clientContacts)
    .values({ clientId, ...data })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'clientContact',
    entityId: contact!.id,
    newValue: { clientId, channel: contact!.channel },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['contacts'] });

  return contact!;
}

export async function updateContact(
  contactId: string,
  clientId: string,
  portId: string,
  data: Partial<{
    channel: string;
    value: string;
    valueE164: string | null;
    valueCountry: string | null;
    label: string;
    isPrimary: boolean;
    notes: string;
  }>,
  _meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const contact = await db.query.clientContacts.findFirst({
    where: and(eq(clientContacts.id, contactId), eq(clientContacts.clientId, clientId)),
  });
  if (!contact) throw new NotFoundError('Contact');

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

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['contacts'] });

  return updated;
}

export async function removeContact(
  contactId: string,
  clientId: string,
  portId: string,
  _meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const contact = await db.query.clientContacts.findFirst({
    where: and(eq(clientContacts.id, contactId), eq(clientContacts.clientId, clientId)),
  });
  if (!contact) throw new NotFoundError('Contact');

  await db.delete(clientContacts).where(eq(clientContacts.id, contactId));

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['contacts'] });
}

// ─── Addresses ────────────────────────────────────────────────────────────────

interface AddressInput {
  label?: string;
  streetAddress?: string | null;
  city?: string | null;
  subdivisionIso?: string | null;
  postalCode?: string | null;
  countryIso?: string | null;
  isPrimary?: boolean;
}

export async function listClientAddresses(clientId: string, portId: string) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  return db.query.clientAddresses.findMany({
    where: eq(clientAddresses.clientId, clientId),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });
}

export async function addClientAddress(
  clientId: string,
  portId: string,
  data: AddressInput,
  meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  // The unique partial index requires us to demote any existing primary
  // before inserting a new one. We grab a row lock on the client to
  // serialize concurrent primary-toggle requests against the same client —
  // without this, two simultaneous "isPrimary=true" inserts can both
  // observe "no existing primary" and one trips the unique index with a
  // 5xx instead of being safely ordered.
  const address = await withTransaction(async (tx) => {
    await tx.select({ id: clients.id }).from(clients).where(eq(clients.id, clientId)).for('update');

    const wantsPrimary = data.isPrimary ?? false;
    if (wantsPrimary) {
      await tx
        .update(clientAddresses)
        .set({ isPrimary: false })
        .where(and(eq(clientAddresses.clientId, clientId), eq(clientAddresses.isPrimary, true)));
    }
    const [row] = await tx
      .insert(clientAddresses)
      .values({
        clientId,
        portId,
        label: data.label ?? 'Primary',
        streetAddress: data.streetAddress ?? null,
        city: data.city ?? null,
        subdivisionIso: data.subdivisionIso ?? null,
        postalCode: data.postalCode ?? null,
        countryIso: data.countryIso ?? null,
        isPrimary: wantsPrimary,
      })
      .returning();
    return row!;
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'clientAddress',
    entityId: address.id,
    newValue: { clientId, label: address.label, countryIso: address.countryIso },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['addresses'] });

  return address;
}

export async function updateClientAddress(
  addressId: string,
  clientId: string,
  portId: string,
  data: AddressInput,
  _meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const existing = await db.query.clientAddresses.findFirst({
    where: and(eq(clientAddresses.id, addressId), eq(clientAddresses.clientId, clientId)),
  });
  if (!existing) throw new NotFoundError('Address');

  const updated = await withTransaction(async (tx) => {
    // Lock the client row to serialize primary-toggle changes — see addClientAddress.
    await tx.select({ id: clients.id }).from(clients).where(eq(clients.id, clientId)).for('update');

    if (data.isPrimary === true && !existing.isPrimary) {
      await tx
        .update(clientAddresses)
        .set({ isPrimary: false })
        .where(and(eq(clientAddresses.clientId, clientId), eq(clientAddresses.isPrimary, true)));
    }
    const [row] = await tx
      .update(clientAddresses)
      .set({ ...data, updatedAt: new Date() })
      .where(eq(clientAddresses.id, addressId))
      .returning();
    return row!;
  });

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['addresses'] });

  return updated;
}

export async function removeClientAddress(
  addressId: string,
  clientId: string,
  portId: string,
  _meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const address = await db.query.clientAddresses.findFirst({
    where: and(eq(clientAddresses.id, addressId), eq(clientAddresses.clientId, clientId)),
  });
  if (!address) throw new NotFoundError('Address');

  await db.delete(clientAddresses).where(eq(clientAddresses.id, addressId));

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['addresses'] });
}

// ─── Tags ─────────────────────────────────────────────────────────────────────

export async function setClientTags(
  clientId: string,
  portId: string,
  tagIds: string[],
  meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  await db.delete(clientTags).where(eq(clientTags.clientId, clientId));

  if (tagIds.length > 0) {
    await db.insert(clientTags).values(tagIds.map((tagId) => ({ clientId, tagId })));
  }

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'client',
    entityId: clientId,
    newValue: { tagIds },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'client:updated', { clientId, changedFields: ['tags'] });
}

// ─── Relationships ────────────────────────────────────────────────────────────

export async function listRelationships(clientId: string, portId: string) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  return db.query.clientRelationships.findMany({
    where: (r, { and, or, eq }) =>
      and(eq(r.portId, portId), or(eq(r.clientAId, clientId), eq(r.clientBId, clientId))),
  });
}

export async function createRelationship(
  clientId: string,
  portId: string,
  data: { clientBId: string; relationshipType: string; description?: string },
  meta: AuditMeta,
) {
  const client = await db.query.clients.findFirst({
    where: eq(clients.id, clientId),
  });
  if (!client || client.portId !== portId) throw new NotFoundError('Client');

  const [rel] = await db
    .insert(clientRelationships)
    .values({ portId, clientAId: clientId, ...data })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'clientRelationship',
    entityId: rel!.id,
    newValue: { clientAId: clientId, clientBId: data.clientBId, type: data.relationshipType },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return rel!;
}

export async function deleteRelationship(
  relId: string,
  clientId: string,
  portId: string,
  meta: AuditMeta,
) {
  const rel = await db.query.clientRelationships.findFirst({
    where: eq(clientRelationships.id, relId),
  });
  if (!rel || rel.portId !== portId) throw new NotFoundError('Relationship');

  await db.delete(clientRelationships).where(eq(clientRelationships.id, relId));

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'delete',
    entityType: 'clientRelationship',
    entityId: relId,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

// ─── Find Duplicates ──────────────────────────────────────────────────────────

export async function findDuplicates(portId: string, fullName: string) {
  return db.query.clients.findMany({
    where: (c, { and, eq }) => and(eq(c.portId, portId), ilike(c.fullName, `%${fullName}%`)),
    limit: 5,
  });
}

// ─── Options (for comboboxes) ─────────────────────────────────────────────────

export async function listClientOptions(portId: string, search?: string) {
  // Pickers only surface active rows. Archived clients are still resolvable
  // by id (e.g. history views) but should not appear in dropdowns.
  const conditions = [eq(clients.portId, portId), isNull(clients.archivedAt)];
  if (search) {
    conditions.push(ilike(clients.fullName, `%${search}%`));
  }

  return db
    .select({ id: clients.id, fullName: clients.fullName })
    .from(clients)
    .where(and(...conditions))
    .orderBy(clients.fullName)
    .limit(50);
}
