import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';

export async function getSetting<T = unknown>(
  sql: Sql,
  key: string
): Promise<T | null> {
  const rows = await sql<{ value: T }[]>`SELECT value FROM settings WHERE key = ${key}`;
  return rows[0]?.value ?? null;
}

export async function setSetting(
  sql: Sql,
  key: string,
  value: unknown
): Promise<void> {
  await sql`
    INSERT INTO settings (key, value)
    VALUES (${key}, ${sql.json(value as never)})
    ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
  `;
}

// themes-v1: whitelist of the preset theme ids. Kept in sync with
// docs/themes_v1.md §1 and apps/web/src/lib/theme.ts THEMES.
// (+ 'ember' — the BooCode 2.0 signature, now the default.)
const THEME_IDS = [
  'obsidian',
  'gunmetal',
  'espresso',
  'volcanic-brown',
  'copper',
  'gold',
  'oxblood',
  'crimson',
  'elderflower',
  'plum',
  'steel-pink',
  'fuchsia-noir',
  'matrix',
  'sage',
  'ivory',
  'chalk',
  'cobalt',
  'midnight-sapphire',
  'ember',
  // futuristic ladder (opt-in) — kept in sync with apps/web/src/lib/theme.ts THEMES
  'boocode-plus',
  'boocode-classic',
  'boocode-override',
] as const;

const THEME_MODES = ['dark', 'light', 'system'] as const;

// PATCH body is still a free-form key/value bag for everything except the
// two theme keys, which carry strict per-key validation. Anything outside
// THEME_IDS / THEME_MODES on those keys is rejected with 400.
function validateThemeKeys(body: Record<string, unknown>): string | null {
  if ('theme_id' in body) {
    const v = body.theme_id;
    if (typeof v !== 'string' || !(THEME_IDS as readonly string[]).includes(v)) {
      return `theme_id must be one of: ${THEME_IDS.join(', ')}`;
    }
  }
  if ('theme_mode' in body) {
    const v = body.theme_mode;
    if (typeof v !== 'string' || !(THEME_MODES as readonly string[]).includes(v)) {
      return `theme_mode must be one of: ${THEME_MODES.join(', ')}`;
    }
  }
  return null;
}

const PatchBody = z.record(z.string(), z.unknown());

// Normalize favorite_models on write: must be an array of non-empty
// composite "provider/model" strings. Drops malformed entries, dedupes
// preserving insertion order.
const FAVORITE_MODELS_KEY = 'favorite_models';

export function normalizeFavoriteModels(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  const seen = new Set<string>();
  const out: string[] = [];
  for (const entry of value) {
    if (typeof entry !== 'string') continue;
    const trimmed = entry.trim();
    if (!trimmed || !trimmed.includes('/')) continue;
    if (seen.has(trimmed)) continue;
    seen.add(trimmed);
    out.push(trimmed);
  }
  return out;
}

export function registerSettingsRoutes(app: FastifyInstance, sql: Sql): void {
  app.get('/api/settings', async () => {
    const rows = await sql<{ key: string; value: unknown }[]>`SELECT key, value FROM settings`;
    const out: Record<string, unknown> = {};
    for (const r of rows) out[r.key] = r.value;
    return out;
  });

  app.patch('/api/settings', async (req, reply) => {
    const parsed = PatchBody.safeParse(req.body);
    if (!parsed.success) {
      reply.code(400);
      return { error: 'invalid body', details: parsed.error.flatten() };
    }
    const themeError = validateThemeKeys(parsed.data);
    if (themeError) {
      reply.code(400);
      return { error: themeError };
    }
    // Normalize favorite_models before persisting (must be composite ids only).
    if (FAVORITE_MODELS_KEY in parsed.data) {
      parsed.data[FAVORITE_MODELS_KEY] = normalizeFavoriteModels(
        parsed.data[FAVORITE_MODELS_KEY],
      );
    }

    for (const [k, v] of Object.entries(parsed.data)) {
      await setSetting(sql, k, v);
    }
    const rows = await sql<{ key: string; value: unknown }[]>`SELECT key, value FROM settings`;
    const out: Record<string, unknown> = {};
    for (const r of rows) out[r.key] = r.value;
    return out;
  });
}
