import {
  pgTable,
  text,
  boolean,
  integer,
  timestamp,
  jsonb,
  index,
  uniqueIndex,
  primaryKey,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { ports } from './ports';

export const clients = pgTable(
  'clients',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    fullName: text('full_name').notNull(),
    /** ISO-3166-1 alpha-2 nationality code. */
    nationalityIso: text('nationality_iso'),
    preferredContactMethod: text('preferred_contact_method'), // email, phone, whatsapp
    preferredLanguage: text('preferred_language'),
    /** IANA timezone, e.g. 'Europe/Warsaw'. Validated client + server. */
    timezone: text('timezone'),
    source: text('source'), // website, manual, referral, broker
    sourceDetails: text('source_details'),
    /** When this client came out of a "Convert inquiry to client" triage
     *  step, points back at the originating `website_submissions` row.
     *  Drives the conversion-funnel-by-source chart. Migration 0065
     *  installs the FK with ON DELETE SET NULL - Drizzle doesn't reflect
     *  it here to avoid the cross-file circular import. */
    sourceInquiryId: text('source_inquiry_id'),
    archivedAt: timestamp('archived_at', { withTimezone: true }),
    /** Better-auth user id of the operator who archived this client. */
    archivedBy: text('archived_by'),
    /** Free-text reason captured at archive time. Required when archiving a
     *  client at deposit_paid or later (compliance trail). Optional when
     *  archiving an early-stage lead. */
    archiveReason: text('archive_reason'),
    /** Per-decision metadata captured during smart-archive flow. Used by
     *  the restore wizard to attempt reversal. Shape:
     *    { decisions: Array<{ kind, refId, ...specifics }>, decidedAt, decidedBy }
     *  See src/lib/services/client-archive.service.ts for the canonical
     *  payload schema. */
    archiveMetadata: jsonb('archive_metadata'),
    /** When this client was merged into another (the "loser" of a dedup
     *  merge), this points at the surviving client. Used by the
     *  /admin/duplicates review queue to redirect any stragglers, and by
     *  the unmerge flow to restore. Null for live clients. The Postgres
     *  self-FK is installed via migration 0042; Drizzle's table builder
     *  doesn't accept self-references in the column factory so the
     *  constraint isn't reflected here in `.references(...)`. */
    mergedIntoClientId: text('merged_into_client_id'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_clients_port').on(table.portId),
    index('idx_clients_name').on(table.portId, table.fullName),
    index('idx_clients_archived')
      .on(table.portId)
      .where(sql`${table.archivedAt} IS NULL`),
    index('idx_clients_nationality_iso').on(table.nationalityIso),
    index('idx_clients_merged_into').on(table.mergedIntoClientId),
  ],
);

export const clientContacts = pgTable(
  'client_contacts',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    clientId: text('client_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    channel: text('channel').notNull(), // email, phone, whatsapp, other
    value: text('value').notNull(),
    /** E.164-normalized phone number (only set when channel='phone'/'whatsapp'). */
    valueE164: text('value_e164'),
    /** ISO-3166-1 alpha-2 of the country this number was parsed against. */
    valueCountry: text('value_country'),
    label: text('label'), // primary, secondary, work, personal, broker, assistant
    isPrimary: boolean('is_primary').notNull().default(false),
    notes: text('notes'),
    // Phase 3 - origin tracking.
    // source: 'manual' | 'imported' | 'eoi-custom-input'
    // source_document_id: when source='eoi-custom-input', points at the
    // EOI document this row was spawned from. Surfaces an [EOI] badge
    // on the client detail panel + a link back to the generating doc.
    source: text('source').notNull().default('manual'),
    sourceDocumentId: text('source_document_id'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_cc_client').on(table.clientId),
    index('idx_cc_email')
      .on(table.channel, table.value)
      .where(sql`${table.channel} = 'email'`),
    index('idx_cc_phone')
      .on(table.channel, table.value)
      .where(sql`${table.channel} = 'phone'`),
    // At most one is_primary=true per (client_id, channel). Prevents
    // ambiguity when the /clients list pulls "the" primary phone/email.
    uniqueIndex('idx_cc_one_primary_per_channel')
      .on(table.clientId, table.channel)
      .where(sql`${table.isPrimary} = true`),
  ],
);

export const clientRelationships = pgTable(
  'client_relationships',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    clientAId: text('client_a_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    clientBId: text('client_b_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    relationshipType: text('relationship_type').notNull(), // referred_by, broker_for, family_member, same_vessel, custom
    description: text('description'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [index('idx_cr_port').on(table.portId)],
);

export const clientNotes = pgTable(
  'client_notes',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    clientId: text('client_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    authorId: text('author_id').notNull(), // user ID
    content: text('content').notNull(),
    mentions: text('mentions').array(), // array of mentioned user IDs
    isLocked: boolean('is_locked').notNull().default(false),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [index('idx_cn_client').on(table.clientId)],
);

export const clientTags = pgTable(
  'client_tags',
  {
    clientId: text('client_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    tagId: text('tag_id').notNull(), // references tags.id - defined later in system.ts
  },
  (table) => [primaryKey({ columns: [table.clientId, table.tagId] })],
);

export const clientMergeLog = pgTable(
  'client_merge_log',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    survivingClientId: text('surviving_client_id')
      .notNull()
      .references(() => clients.id),
    mergedClientId: text('merged_client_id').notNull(), // the client that was merged away (may no longer exist)
    mergedBy: text('merged_by').notNull(), // user ID
    mergeDetails: jsonb('merge_details').notNull(), // which fields were kept from which record
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [index('idx_cml_port').on(table.portId)],
);

/**
 * Pairs of clients flagged by the background scoring job as potential
 * duplicates. The `/admin/duplicates` review queue reads from here.
 *
 * Lifecycle:
 *   - Background job inserts a row when a pair scores >= the
 *     `dedup_review_queue_threshold` system setting.
 *   - User reviews in the admin UI and either merges (status='merged')
 *     or dismisses (status='dismissed').
 *   - Subsequent runs of the scoring job skip pairs already
 *     `dismissed` so the same false-positive doesn't keep reappearing.
 *     A future score increase recreates the row.
 *
 * Pairs are stored canonically with `clientAId < clientBId` (string
 * comparison) so the same pair only generates one row regardless of
 * scoring direction.
 */
export const clientMergeCandidates = pgTable(
  'client_merge_candidates',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    clientAId: text('client_a_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    clientBId: text('client_b_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    score: integer('score').notNull(),
    /** Human-readable rule list, e.g. ["email match", "phone match"]. */
    reasons: jsonb('reasons').notNull(),
    status: text('status').notNull().default('pending'), // pending | dismissed | merged
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    resolvedAt: timestamp('resolved_at', { withTimezone: true }),
    resolvedBy: text('resolved_by'),
  },
  (table) => [
    index('idx_cmc_port_status').on(table.portId, table.status),
    // Same pair shouldn't surface twice - enforce uniqueness on the
    // canonical (a < b) ordering.
    uniqueIndex('idx_cmc_pair').on(table.portId, table.clientAId, table.clientBId),
  ],
);

export const clientAddresses = pgTable(
  'client_addresses',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    clientId: text('client_id')
      .notNull()
      .references(() => clients.id, { onDelete: 'cascade' }),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id, { onDelete: 'cascade' }),
    label: text('label').notNull().default('Primary'),
    streetAddress: text('street_address'),
    city: text('city'),
    /** ISO 3166-2 subdivision code (e.g. 'PL-MZ', 'US-CA'). Optional. */
    subdivisionIso: text('subdivision_iso'),
    postalCode: text('postal_code'),
    /** ISO-3166-1 alpha-2 country code. */
    countryIso: text('country_iso'),
    isPrimary: boolean('is_primary').notNull().default(true),
    // Phase 3 - origin tracking, same pattern as client_contacts.
    source: text('source').notNull().default('manual'),
    sourceDocumentId: text('source_document_id'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_ca_client').on(table.clientId),
    index('idx_ca_port').on(table.portId),
    uniqueIndex('idx_ca_primary')
      .on(table.clientId)
      .where(sql`${table.isPrimary} = true`),
  ],
);

export type Client = typeof clients.$inferSelect;
export type NewClient = typeof clients.$inferInsert;
export type ClientContact = typeof clientContacts.$inferSelect;
export type NewClientContact = typeof clientContacts.$inferInsert;
export type ClientRelationship = typeof clientRelationships.$inferSelect;
export type NewClientRelationship = typeof clientRelationships.$inferInsert;
export type ClientNote = typeof clientNotes.$inferSelect;
export type NewClientNote = typeof clientNotes.$inferInsert;
export type ClientMergeLog = typeof clientMergeLog.$inferSelect;
export type NewClientMergeLog = typeof clientMergeLog.$inferInsert;
export type ClientAddress = typeof clientAddresses.$inferSelect;
export type NewClientAddress = typeof clientAddresses.$inferInsert;
export type ClientMergeCandidate = typeof clientMergeCandidates.$inferSelect;
export type NewClientMergeCandidate = typeof clientMergeCandidates.$inferInsert;
