import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { dashboards, screens, dashboardDefaults } from './schema';

// ── Full state read ──

export async function getAllDashboardState(userId: number): Promise<Record<string, unknown>> {
  const result: Record<string, unknown> = {};

  const [dashRows, screenRows, defaultsRow] = await Promise.all([
    db.select().from(dashboards).where(eq(dashboards.userId, userId)),
    db.select().from(screens).where(eq(screens.userId, userId)),
    db
      .select()
      .from(dashboardDefaults)
      .where(eq(dashboardDefaults.userId, userId))
      .then((rows) => rows[0]),
  ]);

  // Dashboards → workspaces array + per-dashboard keys
  if (dashRows.length > 0) {
    const sorted = [...dashRows].sort((a, b) => a.sortOrder - b.sortOrder);
    result['workspaces'] = sorted.map((d) => ({ id: d.id, name: d.name, ...(d.config as object) }));

    for (const d of dashRows) {
      if (isLayoutish(d.layout)) result[`ws-layout-${d.id}`] = d.layout;
      result[`ws-terminals-${d.id}`] = d.terminals;
      result[`ws-host-terminals-${d.id}`] = d.hostTerminals;
      for (const [prefix, value] of Object.entries(asRecord(d.panelState))) {
        result[`ws-${prefix}-${d.id}`] = value;
      }
    }
  }

  // Defaults
  if (defaultsRow) {
    result['ws-terminals-default'] = defaultsRow.terminals;
    result['ws-host-terminals-default'] = defaultsRow.hostTerminals;
    for (const [prefix, value] of Object.entries(asRecord(defaultsRow.panelState))) {
      result[`ws-${prefix}-default`] = value;
    }
  }

  // Screens
  for (const s of screenRows) {
    if (isLayoutish(s.layout)) result[`screens/${s.name}`] = s.layout;
  }

  return result;
}

const asRecord = (value: unknown): Record<string, unknown> =>
  value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};

/**
 * Omit a layout the client cannot use rather than hand it over.
 *
 * A `LayoutNode` is an object. The columns used to default to `'[]'`, and every path that upserts without
 * one wrote it — so the key was present, the client's `key in state` check preferred it over the caller's
 * default, and `normalizeLayout` called `.children.map` on an array of nothing and threw during render.
 * Dropping the key instead lets the caller's default win and the next write repairs the row. The column is
 * nullable now, so this only catches rows written before that; keep it anyway — it is the read-side half of
 * a rule nothing else enforces.
 */
const isLayoutish = (value: unknown): boolean => !!value && typeof value === 'object' && !Array.isArray(value);

// ── Dashboard CRUD ──

type UpsertDashboardData = {
  name?: string;
  config?: unknown;
  layout?: unknown;
  terminals?: unknown;
  hostTerminals?: unknown;
  sortOrder?: number;
};

function buildDashboardSet(data: UpsertDashboardData, now: Date): Record<string, unknown> {
  const set: Record<string, unknown> = { updatedAt: now };
  if (data.name !== undefined) set.name = data.name;
  if (data.config !== undefined) set.config = data.config;
  if (data.layout !== undefined) set.layout = data.layout;
  if (data.terminals !== undefined) set.terminals = data.terminals;
  if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;
  if (data.sortOrder !== undefined) set.sortOrder = data.sortOrder;
  return set;
}

export async function upsertDashboard(userId: number, id: string, data: UpsertDashboardData): Promise<void> {
  const now = new Date();
  if (await updateDashboard(userId, id, data)) return;

  await db.insert(dashboards).values({
    id,
    userId,
    name: data.name ?? id,
    config: data.config ?? {},
    layout: data.layout ?? null,
    terminals: data.terminals ?? {},
    hostTerminals: data.hostTerminals ?? {},
    sortOrder: data.sortOrder ?? 0,
    createdAt: now,
    updatedAt: now,
  });
}

/**
 * Update a dashboard only if it already exists, and report whether it did.
 *
 * `upsertDashboard` INSERTs on an unknown id, which is right for the two create paths and wrong for every
 * other key: renaming a dashboard PATCHes `ws-layout-old: null` and `ws-terminals-old: null` together, and
 * the second key used to re-INSERT the row the first had just deleted — named after its own slug, so the
 * old name reappeared in the dashboard list as a zombie.
 */
export async function updateDashboard(userId: number, id: string, data: UpsertDashboardData): Promise<boolean> {
  const updated = await db
    .update(dashboards)
    .set(buildDashboardSet(data, new Date()))
    .where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)))
    .returning({ id: dashboards.id });
  return updated.length > 0;
}

/** Write one prefix of a dashboard's `panelState` bag, leaving its siblings alone. */
export async function setDashboardPanelState(
  userId: number,
  id: string,
  prefix: string,
  value: unknown,
): Promise<boolean> {
  const existing = await db
    .select({ panelState: dashboards.panelState })
    .from(dashboards)
    .where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)))
    .then((rows) => rows[0]);
  if (!existing) return false;

  const next = { ...asRecord(existing.panelState) };
  if (value === null) delete next[prefix];
  else next[prefix] = value;

  await db
    .update(dashboards)
    .set({ panelState: next, updatedAt: new Date() })
    .where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)));
  return true;
}

export async function deleteDashboard(userId: number, id: string): Promise<void> {
  await db.delete(dashboards).where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)));
}

// ── Screen CRUD ──

// No `terminals`/`hostTerminals` here, unlike `UpsertDefaultsData` below. A screen's terminal state
// resolves to the `-default` key — the `dashboard_defaults` row — so the two same-named columns on
// `screens` have never been read by anything, and the only caller (`api/dashboards`) has never passed
// them: every one of the 15 rows holds the `{}` the column default wrote. The columns still exist; they
// are not dropped here because `db:push` diffs the whole schema and there is another agent's uncommitted
// schema file in this tree.
type UpsertScreenData = {
  layout?: unknown;
};

export async function upsertScreen(userId: number, name: string, data: UpsertScreenData): Promise<void> {
  const now = new Date();
  const set: Record<string, unknown> = { updatedAt: now };
  if (data.layout !== undefined) set.layout = data.layout;

  await db
    .insert(screens)
    .values({
      userId,
      name,
      layout: (data.layout ?? null) as never,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: [screens.userId, screens.name],
      set,
    });
}

export async function deleteScreen(userId: number, name: string): Promise<void> {
  await db.delete(screens).where(and(eq(screens.userId, userId), eq(screens.name, name)));
}

// ── Defaults ──

type UpsertDefaultsData = {
  terminals?: unknown;
  hostTerminals?: unknown;
};

// No `getDefaults`. Nothing read the defaults row on its own: `getAllDashboardState` above already folds
// it into the one state payload the client fetches, which is why the single-row getter never acquired a
// caller. `upsertDefaults` below is live (`api/dashboards`).
export async function upsertDefaults(userId: number, data: UpsertDefaultsData): Promise<void> {
  const now = new Date();
  const set: Record<string, unknown> = { updatedAt: now };
  if (data.terminals !== undefined) set.terminals = data.terminals;
  if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;

  await db
    .insert(dashboardDefaults)
    .values({
      userId,
      terminals: (data.terminals ?? {}) as never,
      hostTerminals: (data.hostTerminals ?? {}) as never,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: dashboardDefaults.userId,
      set,
    });
}

/** The `panelState` equivalent of `upsertDefaults` — for panels living on a screen rather than a dashboard. */
export async function setDefaultsPanelState(userId: number, prefix: string, value: unknown): Promise<void> {
  const now = new Date();
  const [row] = await db.select().from(dashboardDefaults).where(eq(dashboardDefaults.userId, userId));

  const next = { ...asRecord(row?.panelState) };
  if (value === null) delete next[prefix];
  else next[prefix] = value;

  await db
    .insert(dashboardDefaults)
    .values({ userId, panelState: next as never, updatedAt: now })
    .onConflictDoUpdate({ target: dashboardDefaults.userId, set: { panelState: next, updatedAt: now } });
}
