import {
  pgTable,
  serial,
  integer,
  varchar,
  text,
  timestamp,
  date,
  time,
  jsonb,
  boolean,
  index,
  uniqueIndex,
  check,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { usersTable } from "./users";

// Single source of truth for the row-highlight palette used by the
// daily schedule overlay (#288). Imported by the API route's Zod
// whitelist AND pinned at the database layer via the CHECK constraint
// below (#293), so the same invariant survives any out-of-band write
// path (manual SQL, future bulk jobs, restored backups). NULL means
// "no tint" / default; any other value must be one of these keys.
export const EXECUTIVE_MEETING_ROW_COLOR_KEYS = [
  "red",
  "amber",
  "green",
  "blue",
  "violet",
  "gray",
] as const;
export type ExecutiveMeetingRowColor =
  (typeof EXECUTIVE_MEETING_ROW_COLOR_KEYS)[number];

export const executiveMeetingsTable = pgTable(
  "executive_meetings",
  {
    id: serial("id").primaryKey(),
    dailyNumber: integer("daily_number").notNull(),
    // titleAr/En hold sanitized rich-text HTML (Tiptap output) so they need
    // unbounded length. Plain-text rows from before the widening still
    // render correctly because they contain no HTML tags.
    titleAr: text("title_ar").notNull(),
    titleEn: text("title_en").notNull().default(""),
    meetingDate: date("meeting_date").notNull(),
    startTime: time("start_time", { withTimezone: false }),
    endTime: time("end_time", { withTimezone: false }),
    location: varchar("location", { length: 300 }),
    meetingUrl: text("meeting_url"),
    platform: varchar("platform", { length: 32 }).notNull().default("none"),
    status: varchar("status", { length: 32 }).notNull().default("scheduled"),
    isHighlighted: integer("is_highlighted").notNull().default(0),
    notes: text("notes"),
    attachments: jsonb("attachments").default([]),
    // Optional cell-merge overlay for the schedule grid. When set, the
    // schedule renders one merged cell spanning columns
    // [mergeStartColumn..mergeEndColumn] and shows mergeText (sanitized
    // HTML) instead of the original column values. The original
    // titleAr/En + attendees + start/endTime stay untouched in the DB,
    // so clearing the merge restores the original cells exactly.
    // Allowed values for the column ids match the frontend ColumnId
    // enum: "number" | "meeting" | "attendees" | "time".
    mergeStartColumn: varchar("merge_start_column", { length: 32 }),
    mergeEndColumn: varchar("merge_end_column", { length: 32 }),
    mergeText: text("merge_text"),
    // Optional row-highlight colour for the daily schedule grid. NULL =
    // "default" (no tint). Allowed non-null values come from the
    // ROW_COLOR_OPTIONS palette in the frontend (red, amber, green,
    // blue, violet, gray) and are validated app-side. Stored on the
    // meeting row itself (not per-user) so the colour is shared across
    // every viewer in real time, since it carries an editorial
    // signal about the meeting (urgent / VIP / etc.) rather than a
    // personal viewing preference.
    rowColor: varchar("row_color", { length: 16 }),
    // #635: External meetings (اجتماع خارجي). When true, the row is
    // tinted red in the schedule grid and is excluded from the
    // auto-postpone cascade (the alert "Postpone" button is disabled
    // and `computeCascadeShift` skips it as a follower). Default false
    // preserves legacy rows as "internal" meetings.
    isExternal: boolean("is_external").notNull().default(false),
    createdBy: integer("created_by").references(() => usersTable.id, {
      onDelete: "set null",
    }),
    updatedBy: integer("updated_by").references(() => usersTable.id, {
      onDelete: "set null",
    }),
    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
  },
  (t) => ({
    dateIdx: index("executive_meetings_date_idx").on(t.meetingDate),
    dateNumberUnique: uniqueIndex("executive_meetings_date_number_unique").on(
      t.meetingDate,
      t.dailyNumber,
    ),
    // Defense-in-depth (#293): mirror the API-side ROW_COLOR_KEYS
    // whitelist at the DB layer so any out-of-band write path
    // (manual SQL, future bulk jobs, restored backups) cannot smuggle
    // an unrenderable colour past the Zod guard. NULL is the "no tint"
    // sentinel, the six keys are the supported palette. Any other
    // value raises a check_violation (PG SQLSTATE 23514) that the
    // caller must explicitly handle.
    // Literal-SQL form: Postgres CHECK constraint definitions are
    // parsed as DDL and reject parameter placeholders ($1, $2, …),
    // so the palette keys are inlined as quoted literals via
    // `sql.raw`. This is safe because the keys come from a hardcoded
    // compile-time constant of single-word identifiers (no user
    // input ever reaches this string).
    rowColorPaletteCheck: check(
      "executive_meetings_row_color_palette_check",
      sql`${t.rowColor} IS NULL OR ${t.rowColor} IN (${sql.raw(
        EXECUTIVE_MEETING_ROW_COLOR_KEYS.map((k) => `'${k}'`).join(", "),
      )})`,
    ),
  }),
);

export const executiveMeetingAttendeesTable = pgTable(
  "executive_meeting_attendees",
  {
    id: serial("id").primaryKey(),
    meetingId: integer("meeting_id")
      .notNull()
      .references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
    // attendee name holds sanitized rich-text HTML so it needs unbounded
    // length. Plain-text rows still render correctly (no tags = no parse).
    // For `kind = "subheading"` rows, this column stores the user-written
    // section label (free text, single value rendered as-is in AR and EN).
    name: text("name").notNull(),
    title: varchar("title", { length: 200 }),
    attendanceType: varchar("attendance_type", { length: 32 })
      .notNull()
      .default("internal"),
    sortOrder: integer("sort_order").notNull().default(0),
    // "person"      — a real attendee row (default; matches all pre-existing
    //                 data after backfill).
    // "subheading"  — a user-defined section label inserted between attendees
    //                 inside the same attendance-type group. Numbering, count,
    //                 and virtual-platform extraction MUST ignore these rows.
    kind: varchar("kind", { length: 16 }).notNull().default("person"),
  },
  (t) => ({
    meetingIdx: index("executive_meeting_attendees_meeting_idx").on(t.meetingId),
  }),
);

export const executiveMeetingNotificationsTable = pgTable(
  "executive_meeting_notifications",
  {
    id: serial("id").primaryKey(),
    meetingId: integer("meeting_id").references(() => executiveMeetingsTable.id, {
      onDelete: "cascade",
    }),
    userId: integer("user_id").references(() => usersTable.id, { onDelete: "cascade" }),
    notificationType: varchar("notification_type", { length: 64 }).notNull(),
    scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
    sentAt: timestamp("sent_at", { withTimezone: true }),
    status: varchar("status", { length: 32 }).notNull().default("pending"),
    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
  },
);

/**
 * Per-user notification preferences for the Executive Meetings module.
 *
 * Each row toggles a single (userId, notificationType) pair on or off
 * for the in-app bell channel and the email channel independently.
 *
 * Default behaviour when no row exists: BOTH channels ON. This keeps the
 * legacy "fan out to everyone, every time" semantics intact for users
 * who never visit the preferences UI. Only an explicit opt-out (row
 * present + flag set to false) suppresses delivery.
 *
 * notificationType mirrors the keys passed to
 * `recordExecutiveMeetingNotifications` / `sendExecutiveMeetingEmail`:
 *   - meeting_created
 */
export const executiveMeetingNotificationPrefsTable = pgTable(
  "executive_meeting_notification_prefs",
  {
    id: serial("id").primaryKey(),
    userId: integer("user_id")
      .notNull()
      .references(() => usersTable.id, { onDelete: "cascade" }),
    notificationType: varchar("notification_type", { length: 64 }).notNull(),
    inApp: boolean("in_app").notNull().default(true),
    email: boolean("email").notNull().default(true),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
  },
  (t) => ({
    userTypeUnique: uniqueIndex(
      "executive_meeting_notification_prefs_user_type_unique",
    ).on(t.userId, t.notificationType),
  }),
);

export const executiveMeetingAuditLogsTable = pgTable(
  "executive_meeting_audit_logs",
  {
    id: serial("id").primaryKey(),
    action: varchar("action", { length: 100 }).notNull(),
    entityType: varchar("entity_type", { length: 50 }).notNull(),
    entityId: integer("entity_id"),
    oldValue: jsonb("old_value"),
    newValue: jsonb("new_value"),
    performedBy: integer("performed_by").references(() => usersTable.id, {
      onDelete: "set null",
    }),
    performedAt: timestamp("performed_at", { withTimezone: true }).notNull().defaultNow(),
  },
);

export const executiveMeetingPdfArchivesTable = pgTable(
  "executive_meeting_pdf_archives",
  {
    id: serial("id").primaryKey(),
    archiveDate: date("archive_date").notNull(),
    // `filePath` doubles as the storage URL for the archived PDF. When the
    // PDF was uploaded to object storage the value is a `/objects/<id>`
    // path that resolves through GET /api/storage/objects/*. Older rows
    // (browser-print snapshots) use the synthetic `print:<date>` form.
    filePath: text("file_path").notNull(),
    version: integer("version").notNull().default(1),
    // Bytes of the generated PDF on disk. Nullable because legacy rows
    // (created before the server-side generator) never had a real file.
    byteSize: integer("byte_size"),
    generatedBy: integer("generated_by").references(() => usersTable.id, {
      onDelete: "set null",
    }),
    generatedAt: timestamp("generated_at", { withTimezone: true }).notNull().defaultNow(),
  },
);

// #273: Per-user dismissal/acknowledgment state for the 5-minute
// upcoming-meeting alert. One row per (meetingId, userId). Absence of
// a row means the user has neither acknowledged ("Done") nor dismissed
// ("Delete alert") the alert for that meeting yet, so the alert is
// eligible to fire when the meeting falls inside the 5-minute window.
export const executiveMeetingAlertStateTable = pgTable(
  "executive_meeting_alert_state",
  {
    id: serial("id").primaryKey(),
    meetingId: integer("meeting_id")
      .notNull()
      .references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
    userId: integer("user_id")
      .notNull()
      .references(() => usersTable.id, { onDelete: "cascade" }),
    dismissed: boolean("dismissed").notNull().default(false),
    acknowledged: boolean("acknowledged").notNull().default(false),
    // #558: Set once by the server-side scheduler the moment a Web
    // Push reminder has been fired for this (meeting, user) pair, so a
    // single tick (or any later tick that re-runs the same query) does
    // not double-ring the iPad. NULL = no push has been sent yet.
    pushedAt: timestamp("pushed_at", { withTimezone: true }),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
  },
  (t) => ({
    perUserUnique: uniqueIndex(
      "executive_meeting_alert_state_meeting_user_unique",
    ).on(t.meetingId, t.userId),
  }),
);

export const executiveMeetingFontSettingsTable = pgTable(
  "executive_meeting_font_settings",
  {
    id: serial("id").primaryKey(),
    scope: varchar("scope", { length: 16 }).notNull().default("global"),
    userId: integer("user_id").references(() => usersTable.id, { onDelete: "cascade" }),
    fontFamily: varchar("font_family", { length: 100 }).default("system"),
    fontSize: integer("font_size").default(14),
    fontWeight: varchar("font_weight", { length: 16 }).default("regular"),
    alignment: varchar("alignment", { length: 16 }).default("start"),
    fontColor: varchar("font_color", { length: 16 }).default("#000000"),
    // Object-storage path (`/objects/<id>`) of the brand logo embedded
    // in the top-left of the generated PDF header. NULL = no logo. Only
    // meaningful on the global-scope row (the per-user row ignores it).
    logoObjectPath: text("logo_object_path"),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
  },
);

export type ExecutiveMeeting = typeof executiveMeetingsTable.$inferSelect;
export type ExecutiveMeetingAttendee = typeof executiveMeetingAttendeesTable.$inferSelect;
