import {
  pgTable,
  primaryKey,
  text,
  boolean,
  integer,
  numeric,
  timestamp,
  jsonb,
  index,
  uniqueIndex,
  type AnyPgColumn,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { ports } from './ports';
import { clients } from './clients';
import { yachts } from './yachts';
import { companies } from './companies';
import { interests } from './interests';
import { berthTenancies } from './tenancies';

export const files = pgTable(
  'files',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    clientId: text('client_id').references(() => clients.id, { onDelete: 'set null' }),
    yachtId: text('yacht_id').references(() => yachts.id, { onDelete: 'set null' }),
    companyId: text('company_id').references(() => companies.id, { onDelete: 'set null' }),
    /**
     * Optional deal scope. When a file is uploaded inside an interest
     * (the EOI tab, the documents tab on the interest detail page), this
     * FK captures which deal it belongs to so the file can be filed
     * under the interest subfolder while still rolling up to the parent
     * client folder. NULL for client/yacht/company-level uploads.
     *
     * Added by migration 0078; not yet wired into ensureEntityFolder
     * (interest subfolder nesting) - see master UAT line 728+ for the
     * remaining work plan.
     */
    interestId: text('interest_id').references(() => interests.id, { onDelete: 'set null' }),
    folderId: text('folder_id').references((): AnyPgColumn => documentFolders.id, {
      onDelete: 'set null',
    }),
    filename: text('filename').notNull(),
    originalName: text('original_name').notNull(),
    mimeType: text('mime_type'),
    sizeBytes: text('size_bytes'), // stored as text to avoid bigint issues; parse as number in app
    storagePath: text('storage_path').notNull(),
    storageBucket: text('storage_bucket').notNull().default('crm-files'),
    category: text('category'), // eoi, contract, image, receipt, correspondence, misc
    uploadedBy: text('uploaded_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_files_port').on(table.portId),
    index('idx_files_client').on(table.clientId),
    index('idx_files_yacht').on(table.yachtId),
    index('idx_files_company').on(table.companyId),
    index('idx_files_folder').on(table.folderId),
    index('idx_files_port_folder').on(table.portId, table.folderId),
    // Composite indexes for the aggregated-projection queries
    // (`listFilesAggregatedByEntity`) - every join carries a defense-in-
    // depth `port_id` filter so the leading column matters at scale.
    index('idx_files_port_client').on(table.portId, table.clientId),
    index('idx_files_port_company').on(table.portId, table.companyId),
    index('idx_files_port_yacht').on(table.portId, table.yachtId),
    index('idx_files_interest').on(table.interestId),
    index('idx_files_port_interest').on(table.portId, table.interestId),
  ],
);

export const documents = pgTable(
  'documents',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    interestId: text('interest_id').references(() => interests.id, { onDelete: 'set null' }),
    // H-01: nullable; tolerate the owning client being hard-deleted (rare -
    // archive is the normal path - but if it happens the document row
    // should outlive it so the audit trail stays intact).
    clientId: text('client_id').references(() => clients.id, { onDelete: 'set null' }),
    yachtId: text('yacht_id').references(() => yachts.id, { onDelete: 'set null' }),
    companyId: text('company_id').references(() => companies.id, { onDelete: 'set null' }),
    tenancyId: text('tenancy_id').references(() => berthTenancies.id, {
      onDelete: 'set null',
    }),
    folderId: text('folder_id').references((): AnyPgColumn => documentFolders.id, {
      onDelete: 'set null',
    }),
    documentType: text('document_type').notNull(), // eoi, contract, nda, reservation_agreement, other
    title: text('title').notNull(),
    status: text('status').notNull().default('draft'), // draft, sent, partially_signed, completed, expired, cancelled
    documensoId: text('documenso_id'),
    /** Documenso v2 webhooks send only the numeric internal id (e.g. "19"),
     *  while every other v2 API path expects the public envelope_xxx string
     *  stored in `documensoId`. Captured at create-time so the webhook
     *  resolver can match incoming events by either id. Null for v1
     *  documents (where `documensoId` already holds the only id). */
    documensoNumericId: text('documenso_numeric_id'),
    // H-01: nullable file references; both pre- and post-signing blobs
    // can be soft-archived independently of the document row.
    fileId: text('file_id').references(() => files.id, { onDelete: 'set null' }),
    signedFileId: text('signed_file_id').references(() => files.id, { onDelete: 'set null' }),
    isManualUpload: boolean('is_manual_upload').notNull().default(false),
    /** Email addresses CC'd on the completion notification (the
     *  passive Documenso CC concept - see plan Q4). Per-document set
     *  by the rep; doesn't gate signing. */
    completionCcEmails: text('completion_cc_emails').array().default([]),
    /** Optional auto-reminder cadence - when set, a daily worker
     *  fires `sendSigningReminder()` for unsigned signers every
     *  N days until they complete. Null = manual reminders only. */
    autoReminderIntervalDays: integer('auto_reminder_interval_days'),
    notes: text('notes'),
    /** Phase 6 polish - rep-authored note inserted above the CTA in
     *  every signing-invitation email for THIS document. Falls back to
     *  the empty string when null. Plain-text (XSS-escaped by the
     *  email renderer); not Markdown. */
    invitationMessage: text('invitation_message'),
    /** Automate Signing mode. `manual` (default) = rep clicks each
     *  signer's invitation by hand. `sequential_auto` = first signer
     *  invited; webhook auto-fires the next-in-order on each
     *  recipient_signed event. `concurrent_auto` = all signers invited
     *  in one bulk dispatch; webhook only fires the broadcast on
     *  completion. CHECK constraint at the DB layer enforces the
     *  three-value enum. */
    automationMode: text('automation_mode').notNull().default('manual'),
    remindersDisabled: boolean('reminders_disabled').notNull().default(false),
    reminderCadenceOverride: integer('reminder_cadence_override'),
    // Phase 3 - per-document field overrides. When NULL, the canonical
    // client/yacht record value flows through; when set, this document
    // uses the override without touching the underlying record. Mirrors
    // the AcroForm field set per docs/eoi-documenso-field-mapping.md.
    // These are NOT promoted to client_contacts/addresses/yachts unless
    // the dialog's "Save as new" toggle is ticked at generate time.
    overrideClientEmail: text('override_client_email'),
    overrideClientPhone: text('override_client_phone'),
    overrideClientAddressLine1: text('override_client_address_line_1'),
    overrideClientAddressLine2: text('override_client_address_line_2'),
    overrideClientCity: text('override_client_city'),
    overrideClientState: text('override_client_state'),
    overrideClientPostalCode: text('override_client_postal_code'),
    overrideClientCountry: text('override_client_country'),
    overrideYachtName: text('override_yacht_name'),
    overrideYachtLengthFt: numeric('override_yacht_length_ft'),
    overrideYachtWidthFt: numeric('override_yacht_width_ft'),
    overrideYachtDraftFt: numeric('override_yacht_draft_ft'),
    createdBy: text('created_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_docs_port').on(table.portId),
    index('idx_docs_interest').on(table.interestId),
    index('idx_docs_client').on(table.clientId),
    index('idx_documents_yacht').on(table.yachtId),
    index('idx_documents_company').on(table.companyId),
    index('idx_docs_tenancy').on(table.tenancyId),
    index('idx_docs_type').on(table.portId, table.documentType),
    index('idx_docs_status_port').on(table.portId, table.status),
    // Cover the file FKs Postgres doesn't auto-index. Without these,
    // deleting (or RESTRICT-checking) a referenced files row scans
    // the documents table fully.
    index('idx_docs_file_id').on(table.fileId),
    index('idx_docs_signed_file_id').on(table.signedFileId),
    index('idx_docs_documenso_numeric_id').on(table.documensoNumericId),
    index('idx_docs_folder').on(table.folderId),
    // Composite indexes for the aggregated-projection queries
    // (`listInflightWorkflowsAggregatedByEntity`) - every join carries a
    // defense-in-depth `port_id` filter so the leading column matters at scale.
    index('idx_docs_port_client').on(table.portId, table.clientId),
    index('idx_docs_port_company').on(table.portId, table.companyId),
    index('idx_docs_port_yacht').on(table.portId, table.yachtId),
  ],
);

export const documentSigners = pgTable(
  'document_signers',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    documentId: text('document_id')
      .notNull()
      .references(() => documents.id, { onDelete: 'cascade' }),
    signerName: text('signer_name').notNull(),
    signerEmail: text('signer_email').notNull(),
    signerRole: text('signer_role').notNull(), // client, developer, sales, approver, other
    signingOrder: integer('signing_order').notNull(),
    status: text('status').notNull().default('pending'), // pending, signed, declined
    signedAt: timestamp('signed_at', { withTimezone: true }),
    signingUrl: text('signing_url'),
    embeddedUrl: text('embedded_url'),
    /** Phase 1+2 lifecycle tracking - set by the send-invitation endpoint
     *  and the Documenso webhook handler respectively. */
    invitedAt: timestamp('invited_at', { withTimezone: true }),
    openedAt: timestamp('opened_at', { withTimezone: true }),
    lastReminderSentAt: timestamp('last_reminder_sent_at', { withTimezone: true }),
    /** Documenso recipient token - used for token-based lookup when the
     *  webhook fires (more robust than email match when one address
     *  serves multiple roles). */
    signingToken: text('signing_token'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_ds_doc').on(table.documentId),
    index('idx_ds_signing_token').on(table.signingToken),
  ],
);

export const documentEvents = pgTable(
  'document_events',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    documentId: text('document_id')
      .notNull()
      .references(() => documents.id, { onDelete: 'cascade' }),
    eventType: text('event_type').notNull(), // created, sent, viewed, signed, completed, expired, reminder_sent
    // H-01: events outlive their signer row so the audit trail stays
    // intact if a recipient is removed.
    signerId: text('signer_id').references(() => documentSigners.id, { onDelete: 'set null' }),
    // C.2: recipient_email captured at event time. Enables per-recipient
    // dedup (`(documenso_document_id, recipient_email, event_type)`) so
    // a RECIPIENT_SIGNED webhook for signer-A doesn't dedup against an
    // earlier RECIPIENT_SIGNED for signer-B on the same envelope.
    recipientEmail: text('recipient_email'),
    eventData: jsonb('event_data').default({}),
    signatureHash: text('signature_hash'), // deduplication (legacy: per-payload-hash)
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_de_doc').on(table.documentId),
    // Reverse-lookup signer→events without scanning the events table.
    index('idx_de_signer').on(table.signerId),
    uniqueIndex('idx_de_dedup')
      .on(table.documentId, table.signatureHash)
      .where(sql`${table.signatureHash} IS NOT NULL`),
    // C.2: per-recipient event dedup. Distinct event_type per (document,
    // recipient) so re-delivery of the same SIGNED webhook for the same
    // recipient is a no-op, while a different recipient's SIGNED still
    // lands.
    uniqueIndex('idx_de_per_recipient_dedup')
      .on(table.documentId, table.recipientEmail, table.eventType)
      .where(sql`${table.recipientEmail} IS NOT NULL`),
  ],
);

export const documentTemplates = pgTable(
  'document_templates',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    name: text('name').notNull(),
    description: text('description'),
    templateType: text('template_type').notNull(), // welcome_letter, handover_checklist, acknowledgment, correspondence, custom
    // Nullable: only required when template_format='html'.
    bodyHtml: text('body_html'),
    mergeFields: jsonb('merge_fields').notNull().default([]),
    // 'html' | 'pdf_form' | 'pdf_overlay' | 'documenso_render'
    templateFormat: text('template_format').notNull().default('html'),
    sourceFileId: text('source_file_id').references(() => files.id),
    documensoTemplateId: text('documenso_template_id'),
    // pdf_form: { acroFieldName: mergeToken }
    fieldMapping: jsonb('field_mapping').notNull().default({}),
    // pdf_overlay: [{ token, page, x, y, fontSize }]
    overlayPositions: jsonb('overlay_positions').notNull().default([]),
    // null = no auto-reminders
    reminderCadenceDays: integer('reminder_cadence_days'),
    isActive: boolean('is_active').notNull().default(true),
    createdBy: text('created_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_dt_port').on(table.portId),
    index('idx_dt_type').on(table.portId, table.templateType),
    index('idx_dt_source_file').on(table.sourceFileId),
  ],
);

export const documentWatchers = pgTable(
  'document_watchers',
  {
    documentId: text('document_id')
      .notNull()
      .references(() => documents.id, { onDelete: 'cascade' }),
    userId: text('user_id').notNull(),
    addedBy: text('added_by').notNull(),
    addedAt: timestamp('added_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    primaryKey({ columns: [table.documentId, table.userId] }),
    index('idx_doc_watchers_doc').on(table.documentId),
    index('idx_doc_watchers_user').on(table.userId),
  ],
);

export const formTemplates = pgTable(
  'form_templates',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    name: text('name').notNull(),
    description: text('description'),
    fields: jsonb('fields').notNull(),
    branding: jsonb('branding').default({}),
    isActive: boolean('is_active').notNull().default(true),
    createdBy: text('created_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [index('idx_ft_port').on(table.portId)],
);

export const formSubmissions = pgTable(
  'form_submissions',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    formTemplateId: text('form_template_id')
      .notNull()
      .references(() => formTemplates.id),
    clientId: text('client_id').references(() => clients.id),
    interestId: text('interest_id').references(() => interests.id, { onDelete: 'set null' }),
    token: text('token').notNull().unique(),
    prefilledData: jsonb('prefilled_data').default({}),
    submittedData: jsonb('submitted_data'),
    status: text('status').notNull().default('pending'), // pending, submitted, expired
    expiresAt: timestamp('expires_at', { withTimezone: true }),
    submittedAt: timestamp('submitted_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('idx_fs_token').on(table.token),
    index('idx_fs_template').on(table.formTemplateId),
    index('idx_fs_client').on(table.clientId),
  ],
);

/**
 * Per-port folder tree for organising documents. Self-referencing
 * via parent_id; null parent = root. Unlimited depth - the UI is the
 * gate (collapsed sidebar tree + breadcrumb header). Cycle prevention
 * happens in the service layer (parent_id chain walk on insert/move).
 *
 * On folder delete: children (both subfolders and documents) bubble
 * up to the deleted folder's parent. Never CASCADE.
 */
export const documentFolders = pgTable(
  'document_folders',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    // Null = root. ON DELETE NO ACTION on the FK (added by migration
    // 0050) - the service bubbles children up to the deleted folder's
    // parent in a transaction instead of cascading.
    parentId: text('parent_id'),
    name: text('name').notNull(),
    /** True = folder is managed by the system (one of the three roots
     *  Clients/Companies/Yachts, or an auto-created entity subfolder).
     *  System folders reject rename/move/delete at the API layer. Demoted
     *  to false when the owning entity is hard-deleted. */
    systemManaged: boolean('system_managed').notNull().default(false),
    /** null | 'root' | 'client' | 'company' | 'yacht'. 'root' is the
     *  three system roots; the entity values mark per-entity subfolders. */
    entityType: text('entity_type'),
    /** Null when entityType is null or 'root'; the entity's id otherwise.
     *  Combined with entityType to dedupe entity folders per port. */
    entityId: text('entity_id'),
    /** Mirrors the entity's archive state. Non-null = folder muted in UI
     *  and auto-deposit halted. Cleared on entity restore. */
    archivedAt: timestamp('archived_at', { withTimezone: true }),
    createdBy: text('created_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_document_folders_port').on(table.portId),
    index('idx_document_folders_parent').on(table.parentId),
    uniqueIndex('uniq_document_folders_sibling_name').on(
      table.portId,
      sql`COALESCE(${table.parentId}, '__root__')`,
      sql`LOWER(${table.name})`,
    ),
    // One subfolder per entity per port. Excludes 'root' folders (the
    // three system roots are deduped by sibling-name uniqueness).
    uniqueIndex('uniq_document_folders_entity')
      .on(table.portId, table.entityType, table.entityId)
      .where(sql`${table.entityId} IS NOT NULL`),
  ],
);

export type DocumentFolder = typeof documentFolders.$inferSelect;
export type NewDocumentFolder = typeof documentFolders.$inferInsert;

export type File = typeof files.$inferSelect;
export type NewFile = typeof files.$inferInsert;
export type Document = typeof documents.$inferSelect;
export type NewDocument = typeof documents.$inferInsert;
export type DocumentSigner = typeof documentSigners.$inferSelect;
export type NewDocumentSigner = typeof documentSigners.$inferInsert;
export type DocumentEvent = typeof documentEvents.$inferSelect;
export type NewDocumentEvent = typeof documentEvents.$inferInsert;
export type DocumentTemplate = typeof documentTemplates.$inferSelect;
export type NewDocumentTemplate = typeof documentTemplates.$inferInsert;
export type DocumentWatcher = typeof documentWatchers.$inferSelect;
export type NewDocumentWatcher = typeof documentWatchers.$inferInsert;
export type FormTemplate = typeof formTemplates.$inferSelect;
export type NewFormTemplate = typeof formTemplates.$inferInsert;
export type FormSubmission = typeof formSubmissions.$inferSelect;
export type NewFormSubmission = typeof formSubmissions.$inferInsert;
