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

import { ports } from './ports';
import { clients } from './clients';
import { interests } from './interests';
import { berths } from './berths';
import { user } from './users';

/**
 * Port-wide brochures (Phase 7 - see plan §3.3 / §4.8).
 *
 * Each port can have multiple brochures (e.g. "General", "Investor Pack")
 * with one marked as `isDefault`. Archived brochures stay queryable for
 * audit purposes but are hidden from the send-out picker.
 */
export const brochures = pgTable(
  'brochures',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id, { onDelete: 'cascade' }),
    label: text('label').notNull(),
    description: text('description'),
    isDefault: boolean('is_default').notNull().default(false),
    archivedAt: timestamp('archived_at', { withTimezone: true }),
    createdBy: text('created_by').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_brochures_port').on(table.portId),
    // At most one default brochure per port (excluding archived rows).
    // Service-layer "demote prior default then insert" is correct in the
    // single-writer case, but two concurrent createBrochure(isDefault:true)
    // calls without this index race past the read-then-write check and
    // both win.
    uniqueIndex('idx_brochures_one_default_per_port')
      .on(table.portId)
      .where(sql`${table.isDefault} = true AND ${table.archivedAt} IS NULL`),
  ],
);

/**
 * Versioned brochure files. Identical lifecycle to `berth_pdf_versions`:
 * each upload creates a new immutable row with a monotonic version number
 * per brochure. `storageKey` follows the §4.7a renamed convention.
 */
export const brochureVersions = pgTable(
  'brochure_versions',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    brochureId: text('brochure_id')
      .notNull()
      .references(() => brochures.id, { onDelete: 'cascade' }),
    versionNumber: integer('version_number').notNull(),
    /** Object key in the active storage backend (renamed from `s3_key` per §4.7a). */
    storageKey: text('storage_key').notNull(),
    fileName: text('file_name').notNull(),
    fileSizeBytes: integer('file_size_bytes').notNull(),
    contentSha256: text('content_sha256').notNull(),
    uploadedBy: text('uploaded_by').notNull(),
    uploadedAt: timestamp('uploaded_at', { withTimezone: true }).notNull().defaultNow(),
    /** Cached signed-URL expiry per §11.1 - re-sign only when within 1h of expiry. */
    downloadUrlExpiresAt: timestamp('download_url_expires_at', { withTimezone: true }),
  },
  (table) => [index('idx_brochure_versions_brochure').on(table.brochureId, table.uploadedAt)],
);

/**
 * Send-out audit log for berth PDFs and brochures (Phase 7 - plan §3.3).
 *
 * One row per recipient per send. `documentKind` discriminates between
 * `'berth_pdf'` and `'brochure'`; the corresponding `*_version_id` column
 * pins the exact version sent.
 *
 * `berthPdfVersionId` is intentionally a plain text column (no FK) - the
 * referenced table `berth_pdf_versions` is owned by Phase 6b. Loose-coupling
 * keeps the two phases independent (per Phase 7 task brief).
 *
 * `failedAt` and `errorReason` capture send failures (SMTP auth, transport
 * errors). Failed sends are still written so reps can see "I clicked send
 * but it didn't go" in the timeline (per §14.7).
 */
export const documentSends = pgTable(
  'document_sends',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    /**
     * Either client_id or interest_id is set (or both). All five FKs use
     * `onDelete: 'set null'` so the audit row survives if the parent
     * client/interest/berth/brochure is deleted - `recipient_email`,
     * `document_kind`, `body_markdown`, and `from_address` are denormalized
     * onto the row precisely so the audit trail outlasts the source.
     */
    clientId: text('client_id').references(() => clients.id, { onDelete: 'set null' }),
    interestId: text('interest_id').references(() => interests.id, { onDelete: 'set null' }),
    recipientEmail: text('recipient_email').notNull(),
    /** 'berth_pdf' | 'brochure' */
    documentKind: text('document_kind').notNull(),
    berthId: text('berth_id').references(() => berths.id, { onDelete: 'set null' }),
    /** Forward FK ref - berth_pdf_versions defined in Phase 6b. Loose-coupled. */
    berthPdfVersionId: text('berth_pdf_version_id'),
    brochureId: text('brochure_id').references(() => brochures.id, { onDelete: 'set null' }),
    brochureVersionId: text('brochure_version_id').references(() => brochureVersions.id, {
      onDelete: 'set null',
    }),
    /** Exact body used (after merge-field expansion + sanitization). */
    bodyMarkdown: text('body_markdown'),
    /**
     * better-auth user id of the sender. SET NULL on user delete so the
     * audit row keeps `recipientEmail` + timestamp + body for compliance
     * even when the originating user is removed from the system.
     */
    sentByUserId: text('sent_by_user_id').references(() => user.id, { onDelete: 'set null' }),
    fromAddress: text('from_address').notNull(),
    sentAt: timestamp('sent_at', { withTimezone: true }).notNull().defaultNow(),
    /** SMTP provider message-id for deliverability tracking. */
    messageId: text('message_id'),
    /** When the initial send had its attachment dropped because the SMTP server
     *  rejected the size (552 etc.) and the system retried with a download
     *  link, this captures the rejection reason for ops visibility. Null when
     *  the original send went through as-is. */
    fallbackToLinkReason: text('fallback_to_link_reason'),
    /** Set when the SMTP send transaction itself failed (auth/transport/etc). */
    failedAt: timestamp('failed_at', { withTimezone: true }),
    /** Human-readable failure reason; only meaningful when failedAt is non-null. */
    errorReason: text('error_reason'),
    // Phase 6 - async bounce tracking. Populated by the IMAP NDR
    // poller (`src/jobs/processors/imap-bounce-poller.ts`) when a
    // delivery failure message arrives in the configured mailbox and
    // matches this send via recipient_email + sent_at window.
    bounceStatus: text('bounce_status'), // 'hard' | 'soft' | 'ooo'
    bounceReason: text('bounce_reason'),
    bounceDetectedAt: timestamp('bounce_detected_at', { withTimezone: true }),
    // Phase 4b - email open tracking. When `trackOpens` is true the send
    // includes a 1×1 pixel pointing at /api/public/email-pixel/[sendId].
    // `firstOpenedAt` + `openCount` are denormalised aggregates so the
    // sends list can render an "opened" pill without a JOIN.
    trackOpens: boolean('track_opens').notNull().default(false),
    firstOpenedAt: timestamp('first_opened_at', { withTimezone: true }),
    openCount: integer('open_count').notNull().default(0),
  },
  (t) => [
    index('idx_ds_client').on(t.clientId, t.sentAt),
    index('idx_ds_interest').on(t.interestId, t.sentAt),
    index('idx_ds_berth').on(t.berthId, t.sentAt),
    index('idx_ds_port').on(t.portId, t.sentAt),
    // Reverse-lookups: "what sends used this brochure / version" and
    // FK-RESTRICT scans on brochure delete.
    index('idx_ds_brochure').on(t.brochureId),
    index('idx_ds_brochure_version').on(t.brochureVersionId),
    index('idx_ds_sent_by').on(t.sentByUserId),
  ],
);

/**
 * Per-open log for emails with `trackOpens=true`. The 1×1 pixel
 * endpoint inserts here on every fetch (Apple Mail privacy proxy will
 * over-count; most other clients under-count when images are blocked -
 * this is the universal email-tracking caveat). Cached aggregates on
 * `document_sends` keep list rendering fast.
 */
export const documentSendOpens = pgTable(
  'document_send_opens',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id),
    sendId: text('send_id')
      .notNull()
      .references(() => documentSends.id, { onDelete: 'cascade' }),
    openedAt: timestamp('opened_at', { withTimezone: true }).notNull().defaultNow(),
    userAgent: text('user_agent'),
    referer: text('referer'),
  },
  (t) => [
    index('idx_dso_send').on(t.sendId, t.openedAt),
    index('idx_dso_port').on(t.portId, t.openedAt),
  ],
);

export type Brochure = typeof brochures.$inferSelect;
export type NewBrochure = typeof brochures.$inferInsert;
export type BrochureVersion = typeof brochureVersions.$inferSelect;
export type NewBrochureVersion = typeof brochureVersions.$inferInsert;
export type DocumentSend = typeof documentSends.$inferSelect;
export type NewDocumentSend = typeof documentSends.$inferInsert;
export type DocumentSendOpen = typeof documentSendOpens.$inferSelect;
export type NewDocumentSendOpen = typeof documentSendOpens.$inferInsert;
