import type { AnyPgColumn } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core';
import { users } from '../auth/schema';

/**
 * A `LayoutNode` is an object, and the whole write path to these columns is `unknown` — the PATCH body is
 * `Record<string, unknown>`, the query layer casts `as never` to satisfy drizzle, and jsonb accepts
 * anything. So the only place the rule can actually be enforced is here.
 *
 * `'[]'` reached both columns as a *default* and turned `normalizeLayout` into a white screen; the client
 * now refuses to hand over a non-object and the read omits one, but neither of those stops a bad value
 * being stored — they stop it being served. This does. NULL stays legal: it means "none stored", which is
 * the honest state of a dashboard created without a layout.
 */
const layoutIsObject = (column: AnyPgColumn) => sql`${column} IS NULL OR jsonb_typeof(${column}) = 'object'`;

export const dashboards = pgTable(
  'dashboards',
  {
    id: text('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    name: text('name').notNull(),
    config: jsonb('config').notNull().default({}),
    // Nullable with NO default, deliberately. It held `'[]'` — an empty *array* for a column whose only
    // legal contents are a `LayoutNode` object — so every upsert that omitted a layout wrote a value the
    // client then preferred over its own default and crashed traversing. NULL says "none stored", which is
    // the truth, and `getAllDashboardState` omits the key entirely so the caller's default wins.
    layout: jsonb('layout'),
    // `{}`, not `[]` — every writer treats these as a panelId → sessionId map, and `screens` and
    // `dashboard_defaults` already default them correctly. The array default only ever produced a shape
    // the client had to spread away on first write.
    terminals: jsonb('terminals').notNull().default({}),
    hostTerminals: jsonb('host_terminals').notNull().default({}),
    // Panel-scoped session maps that have no column of their own, keyed by the app's state prefix:
    // `{ tmux: { <panelId>: <sessionId> }, nvim: {…}, 'claude-code': {…} }`. They used to have no home at
    // all — the PATCH dispatcher dropped them and returned 200, so every reload minted a fresh uuid and
    // abandoned the running pty. Living on the dashboard row means they are deleted with it.
    panelState: jsonb('panel_state').notNull().default({}),
    sortOrder: integer('sort_order').notNull().default(0),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('uq_dashboards_user_id').on(table.userId, table.id),
    check('ck_dashboards_layout_object', layoutIsObject(table.layout)),
  ],
);

export const screens = pgTable(
  'screens',
  {
    id: serial('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    name: text('name').notNull(),
    // Same as `dashboards.layout` above — nullable, no default.
    layout: jsonb('layout'),
    terminals: jsonb('terminals').notNull().default({}),
    hostTerminals: jsonb('host_terminals').notNull().default({}),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    uniqueIndex('uq_screens_user_name').on(table.userId, table.name),
    check('ck_screens_layout_object', layoutIsObject(table.layout)),
  ],
);

export const dashboardDefaults = pgTable('dashboard_defaults', {
  id: serial('id').primaryKey(),
  userId: integer('user_id')
    .notNull()
    .references(() => users.id, { onDelete: 'cascade' })
    .unique(),
  terminals: jsonb('terminals').notNull().default({}),
  hostTerminals: jsonb('host_terminals').notNull().default({}),
  panelState: jsonb('panel_state').notNull().default({}),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
