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

// ─── Permission Types ─────────────────────────────────────────────────────────

export type RolePermissions = {
  clients: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    merge: boolean;
    export: boolean;
    /** Trigger + download a GDPR data export for a client. Toggleable so it
     *  can be hidden from a user (e.g. a sales rep) when withheld. */
    gdpr_export: boolean;
  };
  interests: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    change_stage: boolean;
    /** Bypass the canTransitionStage table (e.g. jump a deal straight to
     *  Contract without going through Deposit Paid first when the data
     *  was entered out of order). Audit-logged with the reason the rep
     *  gives. Sales-team-restricted. */
    override_stage: boolean;
    generate_eoi: boolean;
    export: boolean;
  };
  berths: {
    view: boolean;
    edit: boolean;
    import: boolean;
    manage_waiting_list: boolean;
    /**
     * Update berth `price` / `priceCurrency` via the dedicated single
     * (`PATCH /api/v1/berths/[id]/price`) and bulk
     * (`POST /api/v1/berths/bulk-update-prices`) endpoints. Carved out
     * from generic `berths.edit` so admins can grant sales reps the
     * ability to retune prices without exposing the full berth-edit
     * surface (dimensions, mooring type, etc.). Always audited.
     */
    update_prices: boolean;
  };
  documents: {
    view: boolean;
    create: boolean;
    edit: boolean;
    send_for_signing: boolean;
    upload_signed: boolean;
    delete: boolean;
    manage_folders: boolean;
  };
  expenses: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    export: boolean;
    scan_receipt: boolean;
  };
  invoices: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    send: boolean;
    record_payment: boolean;
    export: boolean;
  };
  /**
   * Standalone payments resource (deposit / balance / refund records on
   * an interest). Carved out from `invoices.record_payment` so a port
   * that does not use the invoicing module at all can still grant
   * payment-recording rights to sales reps. `view` follows interests.view
   * at the route level - this gate only governs the UI affordance.
   */
  payments: {
    view: boolean;
    record: boolean;
    delete: boolean;
  };
  files: {
    view: boolean;
    upload: boolean;
    edit: boolean;
    delete: boolean;
    manage_folders: boolean;
  };
  email: {
    view: boolean;
    send: boolean;
    configure_account: boolean;
  };
  reminders: {
    view_own: boolean;
    view_all: boolean;
    create: boolean;
    edit_own: boolean;
    edit_all: boolean;
    assign_others: boolean;
  };
  calendar: {
    connect: boolean;
    view_events: boolean;
  };
  reports: {
    view_dashboard: boolean;
    view_analytics: boolean;
    export: boolean;
  };
  document_templates: {
    view: boolean;
    generate: boolean;
    manage: boolean;
  };
  yachts: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    transfer: boolean;
  };
  companies: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
  };
  memberships: {
    view: boolean;
    manage: boolean;
  };
  tenancies: {
    view: boolean;
    manage: boolean;
    cancel: boolean;
  };
  admin: {
    manage_users: boolean;
    view_audit_log: boolean;
    manage_settings: boolean;
    manage_webhooks: boolean;
    manage_reports: boolean;
    manage_custom_fields: boolean;
    manage_forms: boolean;
    manage_tags: boolean;
    system_backup: boolean;
    // Permanent client deletion is gated separately from admin.manage_users
    // because it bypasses archive/restore. Requires email-code confirmation.
    permanently_delete_clients: boolean;
  };
  residential_clients: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
  };
  residential_interests: {
    view: boolean;
    create: boolean;
    edit: boolean;
    delete: boolean;
    change_stage: boolean;
  };
  inquiries: {
    view: boolean;
    manage: boolean;
  };
  client_groups: {
    view: boolean;
    manage: boolean;
  };
};

/**
 * Per-table column visibility - drives the `<ColumnPicker>` and the
 * DataTable `columnVisibility` state. `hiddenColumns` is the source of
 * truth; an entry's absence means "show this column" (so newly-added
 * columns show by default for existing users without us having to
 * migrate stored preferences).
 */
export type TablePreferences = {
  hiddenColumns?: string[];
  /** Row density. Absent = comfortable (the default; matches shadcn's
   *  default cell padding). 'compact' drops vertical padding so reps
   *  can scan more rows per viewport. */
  density?: 'comfortable' | 'compact';
  /** Dimension unit preference for tables that render boat / berth
   *  measurements. Absent = inferred from the user's locale; 'ft' for
   *  US/UK defaults, 'm' elsewhere. Per-table override via the column
   *  header toggle. */
  dimensionUnit?: 'ft' | 'm';
};

export type UserPreferences = {
  dark_mode?: boolean;
  locale?: string;
  timezone?: string;
  /** ISO-3166-1 alpha-2. Drives the default timezone when the rep
   *  hasn't picked one explicitly, and lets the auto-detect banner
   *  spot a mismatch when they're travelling. */
  country?: string;
  /** Keyed by entity type: `clients`, `yachts`, `interests`, etc. */
  tablePreferences?: Record<string, TablePreferences>;
  /**
   * Dashboard widget visibility, keyed by widget id from the registry
   * in `src/components/dashboard/widget-registry.ts`. Missing keys fall
   * back to `defaultVisible` from the registry - so adding a new widget
   * surfaces it for everyone without a migration. `false` hides it.
   */
  dashboardWidgets?: Record<string, boolean>;
  /**
   * Ordered list of widget ids for the **desktop / xl layout** (charts
   * column + rails aside + feed row, side-by-side). Drives the render
   * order at viewport widths >= 1280px. Missing widgets fall through to
   * registry order so newly-added widgets always surface.
   */
  dashboardWidgetOrder?: string[];
  /**
   * Ordered list of widget ids for the **stacked layout** (single
   * column at < xl). Reps reasonably want a different order on mobile
   * vs desktop - Reminders + Activity top on the phone, Pipeline Funnel
   * top on a 27" monitor. When unset, the dashboard falls back to
   * `dashboardWidgetOrder` (then registry order) so a rep who only
   * customized desktop sees the same order on a phone until they
   * customize there too.
   */
  dashboardWidgetOrderMobile?: string[];
  [key: string]: unknown;
};

// ─── Better Auth Core Tables ─────────────────────────────────────────────────

/**
 * Core user table managed by Better Auth.
 * Do NOT modify directly - Better Auth handles CRUD via its adapter.
 */
export const user = pgTable('user', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  emailVerified: boolean('email_verified').notNull().default(false),
  image: text('image'),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});

export const account = pgTable('account', {
  id: text('id').primaryKey(),
  accountId: text('account_id').notNull(),
  providerId: text('provider_id').notNull(),
  userId: text('user_id')
    .notNull()
    .references(() => user.id),
  accessToken: text('access_token'),
  refreshToken: text('refresh_token'),
  idToken: text('id_token'),
  accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
  refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
  scope: text('scope'),
  password: text('password'),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});

export const verification = pgTable('verification', {
  id: text('id').primaryKey(),
  identifier: text('identifier').notNull(),
  value: text('value').notNull(),
  expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }),
  updatedAt: timestamp('updated_at', { withTimezone: true }),
});

// ─── CRM Extension Tables ───────────────────────────────────────────────────

/**
 * Extension table for Better Auth users.
 * Better Auth manages the core `user` table.
 * We extend with CRM-specific fields here.
 */
export const userProfiles = pgTable(
  'user_profiles',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text('user_id').notNull().unique(), // references Better Auth user ID
    /**
     * Canonical first/last name pair. Added 2026-05-09 as the primary
     * source for greetings, invoicing, and DocSign field-merging - the
     * older `displayName` is now kept around as a derived/optional
     * override (e.g. for nicknames or vanity formatting). When migrating
     * production, backfill these columns from displayName by splitting
     * on the first space and zero-pad the trailing column with NULL so
     * single-token names don't fail the not-null assumption.
     */
    firstName: text('first_name'),
    lastName: text('last_name'),
    displayName: text('display_name').notNull(),
    /**
     * Optional sign-in alias. Lowercase a-z0-9 plus dot/underscore/hyphen,
     * 2–30 chars (shape enforced in-app by `USERNAME_REGEX` in
     * `@/lib/validators/username`). Case-insensitive uniqueness is enforced
     * by a partial unique index on LOWER(username); NULL allows the column to
     * coexist with users who still sign in by email. See migration 0054.
     */
    username: text('username'),
    /**
     * Optional signing-identity override. When set, this is the address the
     * system uses to represent the user in SIGNING contexts only — the EOI
     * developer/approver signer slot (`resolveCrmUser`), the in-CRM "your turn
     * to sign" notification target, and the "awaiting my signature" hub tab.
     * It is NEVER a login credential (better-auth stays single-email via
     * `user.email` / `account.account_id`). NULL → the user signs as their
     * own login email. Lets a person (e.g. login `abbie@`) sign on behalf of a
     * shared role mailbox (e.g. `sales@`). See migration 0100.
     */
    signingEmail: text('signing_email'),
    avatarUrl: text('avatar_url'),
    /** FK into the polymorphic `files` table - the avatar is stored
     *  via getStorageBackend() so an S3↔filesystem swap carries it
     *  without breaking the URL. The legacy `avatarUrl` column is
     *  kept for any external photo sources but the file pointer wins
     *  when both are set. */
    avatarFileId: text('avatar_file_id'),
    phone: text('phone'),
    isSuperAdmin: boolean('is_super_admin').notNull().default(false),
    isActive: boolean('is_active').notNull().default(true),
    lastLoginAt: timestamp('last_login_at', { withTimezone: true }),
    preferences: jsonb('preferences').$type<UserPreferences>().notNull().default({}),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [uniqueIndex('user_profiles_user_id_idx').on(table.userId)],
);

export const roles = pgTable('roles', {
  id: text('id')
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull(),
  description: text('description'),
  permissions: jsonb('permissions')
    .$type<RolePermissions>()
    .notNull()
    .default({} as RolePermissions),
  isGlobal: boolean('is_global').notNull().default(true),
  isSystem: boolean('is_system').notNull().default(false),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});

/**
 * Per-user permission overrides layered on top of the role's baseline for
 * a specific port. Each row carries a `Partial<RolePermissions>` map; any
 * explicitly-set leaf wins over the role + port-role-override chain. Most
 * users will never have a row here - it exists for the rare "give Alice
 * the same role as her team but let her run permanent deletes" case.
 *
 * Effective permission resolution lives in `getEffectivePermissions` in
 * src/lib/services/permissions.service.ts.
 */
export const userPermissionOverrides = pgTable(
  'user_permission_overrides',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    // onDelete: 'cascade' is intentional here (not 'set null' as a stale 2026-05-12
    // audit item suggested). A permission override has no semantic value without
    // the user it grants permissions to - preserving a row with user_id=NULL
    // would be an orphan with no audit value, since the override is per-user
    // additive permissions, not a historical event we need to retain.
    userId: text('user_id')
      .notNull()
      .references(() => user.id, { onDelete: 'cascade' }),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id, { onDelete: 'cascade' }),
    permissionOverrides: jsonb('permission_overrides')
      .$type<Partial<RolePermissions>>()
      .notNull()
      .default({}),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('idx_user_perm_overrides_user_port').on(table.userId, table.portId),
    index('idx_user_perm_overrides_user').on(table.userId),
  ],
);

export type UserPermissionOverride = typeof userPermissionOverrides.$inferSelect;
export type NewUserPermissionOverride = typeof userPermissionOverrides.$inferInsert;

export const portRoleOverrides = pgTable(
  'port_role_overrides',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id, { onDelete: 'cascade' }),
    roleId: text('role_id')
      .notNull()
      .references(() => roles.id, { onDelete: 'cascade' }),
    permissionOverrides: jsonb('permission_overrides')
      .$type<Partial<RolePermissions>>()
      .notNull()
      .default({}),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('port_role_overrides_port_role_idx').on(table.portId, table.roleId),
    index('port_role_overrides_port_idx').on(table.portId),
  ],
);

/**
 * Pending email-change records for the verify-old-and-new flow.
 * The CRM's `/api/v1/me/email` endpoint creates a row here, emails
 * the OLD address with a cancel link and the NEW address with a
 * confirm link, and applies the change only when the new address
 * confirms (or auto-cancels at `expiresAt`).
 *
 * `confirmTokenHash` stores a sha256 of the random confirmation
 * token; the raw token is only present in the email body.
 */
export const userEmailChanges = pgTable(
  'user_email_changes',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text('user_id').notNull(),
    oldEmail: text('old_email').notNull(),
    newEmail: text('new_email').notNull(),
    confirmTokenHash: text('confirm_token_hash').notNull(),
    expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
    appliedAt: timestamp('applied_at', { withTimezone: true }),
    cancelledAt: timestamp('cancelled_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    index('idx_uec_user').on(table.userId),
    index('idx_uec_token').on(table.confirmTokenHash),
  ],
);

export const userPortRoles = pgTable(
  'user_port_roles',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text('user_id')
      .notNull()
      .references(() => user.id, { onDelete: 'cascade' }),
    portId: text('port_id')
      .notNull()
      .references(() => ports.id, { onDelete: 'cascade' }),
    roleId: text('role_id')
      .notNull()
      .references(() => roles.id, { onDelete: 'cascade' }),
    /**
     * Per-user per-port toggle that grants full residential domain access
     * (residential_clients.* and residential_interests.*) on top of the
     * user's primary role. Lets admins flip residential access for sales
     * staff individually without minting a second role.
     */
    residentialAccess: boolean('residential_access').notNull().default(false),
    assignedBy: text('assigned_by'), // user ID of who assigned this
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('user_port_roles_user_port_role_idx').on(table.userId, table.portId, table.roleId),
    index('idx_upr_user').on(table.userId),
    index('idx_upr_port').on(table.portId),
  ],
);

/**
 * Sessions table - Better Auth compatibility.
 * Better Auth manages session creation/validation.
 */
export const session = pgTable(
  'session',
  {
    id: text('id').primaryKey(),
    userId: text('user_id').notNull(),
    token: text('token').notNull().unique(),
    expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
    ipAddress: text('ip_address'),
    userAgent: text('user_agent'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('sessions_token_idx').on(table.token),
    index('sessions_user_id_idx').on(table.userId),
  ],
);

export type UserProfile = typeof userProfiles.$inferSelect;
export type NewUserProfile = typeof userProfiles.$inferInsert;
export type Role = typeof roles.$inferSelect;
export type NewRole = typeof roles.$inferInsert;
export type PortRoleOverride = typeof portRoleOverrides.$inferSelect;
export type NewPortRoleOverride = typeof portRoleOverrides.$inferInsert;
export type UserPortRole = typeof userPortRoles.$inferSelect;
export type NewUserPortRole = typeof userPortRoles.$inferInsert;
