import { createRouter } from '@@/create-router';
import {
  getAllDashboardState,
  upsertDashboard,
  updateDashboard,
  deleteDashboard,
  upsertScreen,
  deleteScreen,
  upsertDefaults,
  setDashboardPanelState,
  setDefaultsPanelState,
} from 'officerdb';

export const dashboardsRouter = createRouter();

// Panel-scoped session maps with no column of their own, written by `CommandTerminalWrapper` as
// `ws-<statePrefix>-<dashboardId>`. This is an allow-list because `ws-<prefix>-<id>` cannot be split
// without one — both halves may contain dashes. Keep it in step with the `statePrefix` props in
// `apps/Terminal/index.tsx`; the 400 at the bottom of the PATCH loop is what tells you when it is not.
const PANEL_STATE_PREFIXES = ['tmux', 'nvim', 'claude-code'];

// A `LayoutNode` is an object. Everything from the PATCH body down to jsonb is `unknown`, so without this
// the only thing standing between a wrong-shaped body and the layout columns is the CHECK constraint —
// which does hold the line, but as a 500 that says nothing the caller can act on. Say what was wrong
// instead. Null is handled separately by both layout branches: there it means "delete", not "store".
const isLayout = (value: unknown): boolean => typeof value === 'object' && value !== null && !Array.isArray(value);

// GET /dashboards
dashboardsRouter.get('/', async (ctx) => {
  const userId = ctx.get('user').id;
  const state = await getAllDashboardState(userId);
  return ctx.json(state);
});

// PATCH /dashboards
dashboardsRouter.patch('/', async (ctx) => {
  const user = ctx.get('user');
  const userId = user.id;
  const body = ctx.get('body') as Record<string, unknown>;

  for (const [key, value] of Object.entries(body)) {
    // workspaces — array of dashboard definitions with ordering
    if (key === 'workspaces') {
      const workspaces = value as Array<{ id: string; name: string; [k: string]: unknown }>;
      for (let i = 0; i < workspaces.length; i++) {
        const ws = workspaces[i]!;
        const { id, name, ...config } = ws;
        await upsertDashboard(userId, id, { name, config, sortOrder: i });
      }
      continue;
    }

    // ws-layout-{id}
    const wsLayoutMatch = key.match(/^ws-layout-(.+)$/);
    if (wsLayoutMatch) {
      const id = wsLayoutMatch[1]!;
      if (value === null) {
        await deleteDashboard(userId, id);
      } else {
        if (!isLayout(value)) return ctx.json({ error: `"${key}" must be a layout object` }, 400);
        await upsertDashboard(userId, id, { layout: value });
      }
      continue;
    }

    // ws-terminals-default / ws-host-terminals-default
    if (key === 'ws-terminals-default') {
      await upsertDefaults(userId, { terminals: value });
      continue;
    }
    if (key === 'ws-host-terminals-default') {
      await upsertDefaults(userId, { hostTerminals: value });
      continue;
    }

    // ws-terminals-{id} / ws-host-terminals-{id}
    //
    // A null means "forget this key". It arrives paired with `ws-layout-{id}: null` on a rename, by which
    // point the row is already gone — so dropping it silently is the whole job. These branches used to
    // fall through to an upsert, which wrote NULL into a NOT NULL column on a live dashboard (23502) and
    // re-INSERTed a deleted one as a zombie named after its own slug.
    const wsTerminalsMatch = key.match(/^ws-terminals-(.+)$/);
    if (wsTerminalsMatch) {
      const id = wsTerminalsMatch[1]!;
      if (value !== null && !(await updateDashboard(userId, id, { terminals: value }))) {
        return ctx.json({ error: `no dashboard "${id}" to write "${key}" to` }, 404);
      }
      continue;
    }

    // ws-host-terminals-{id}
    const wsHostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/);
    if (wsHostTerminalsMatch) {
      const id = wsHostTerminalsMatch[1]!;
      if (value !== null && !(await updateDashboard(userId, id, { hostTerminals: value }))) {
        return ctx.json({ error: `no dashboard "${id}" to write "${key}" to` }, 404);
      }
      continue;
    }

    // ws-{prefix}-{id} for the prefixes that have no column of their own
    const prefix = PANEL_STATE_PREFIXES.find((p) => key.startsWith(`ws-${p}-`));
    if (prefix) {
      const id = key.slice(`ws-${prefix}-`.length);
      if (id === 'default') {
        await setDefaultsPanelState(userId, prefix, value);
      } else if (!(await setDashboardPanelState(userId, id, prefix, value))) {
        return ctx.json({ error: `no dashboard "${id}" to write "${key}" to` }, 404);
      }
      continue;
    }

    // screens/{name}
    const screensMatch = key.match(/^screens\/(.+)$/);
    if (screensMatch) {
      const name = screensMatch[1]!;
      if (value === null) {
        await deleteScreen(userId, name);
      } else {
        if (!isLayout(value)) return ctx.json({ error: `"${key}" must be a layout object` }, 400);
        await upsertScreen(userId, name, { layout: value });
      }
      continue;
    }

    // No silent drops. An unmatched key used to return 200 with a fresh state blob, so a whole key family
    // in active use could persist nowhere at all and look like it had — which is exactly what happened to
    // the three prefixes above.
    return ctx.json({ error: `unknown dashboard-state key "${key}"` }, 400);
  }

  // Three SELECTs per splitter release, thrown away — no caller has ever read this response body, and one
  // that did would be reading a blob assembled *before* the concurrent write it raced. The client
  // converges by refetching on focus instead (`useDashboardState`).
  return ctx.json({ ok: true });
});
