// pattern: Mixed (unavoidable)
// Reason: Hono route file — JSX components, pure helpers (slugifyName, isHttpsUrl,
// sanitizeTokenValue), and route handlers (I/O) must coexist per this project's
// one-file-per-route-group convention (see other admin-*.tsx route files).
import { Hono } from "hono";
import { BaseLayout } from "../layouts/base.js";
import { PageHeader, EmptyState } from "../components/index.js";
import {
  getSessionWithPermissions,
  canManageThemes,
} from "../lib/session.js";
import { isProgrammingError } from "../lib/errors.js";
import { logger } from "../lib/logger.js";
import { tokensToCss } from "../lib/theme.js";
import { FALLBACK_THEME, type WebAppEnv } from "../lib/theme-resolution.js";
import neobrutalLight from "../styles/presets/neobrutal-light.json" with { type: "json" };
import neobrutalDark from "../styles/presets/neobrutal-dark.json" with { type: "json" };
import cleanLight from "../styles/presets/clean-light.json" with { type: "json" };
import cleanDark from "../styles/presets/clean-dark.json" with { type: "json" };
import classicBb from "../styles/presets/classic-bb.json" with { type: "json" };

// ─── Types ─────────────────────────────────────────────────────────────────

interface AdminThemeEntry {
  id: string;
  uri: string;
  name: string;
  colorScheme: string;
  tokens: Record<string, string>;
  cssOverrides: string | null;
  fontUrls: string[] | null;
  createdAt: string;
  indexedAt: string;
}

interface ThemePolicy {
  defaultLightThemeUri: string | null;
  defaultDarkThemeUri: string | null;
  allowUserChoice: boolean;
  availableThemes: Array<{ uri: string; cid?: string }>;
}

// Preset token maps — used by POST /admin/themes to seed tokens on creation
const THEME_PRESETS: Record<string, Record<string, string>> = {
  "neobrutal-light": neobrutalLight as Record<string, string>,
  "neobrutal-dark":  neobrutalDark  as Record<string, string>,
  "clean-light":     cleanLight     as Record<string, string>,
  "clean-dark":      cleanDark      as Record<string, string>,
  "classic-bb":      classicBb      as Record<string, string>,
  "blank": {},
};

// ─── Token Group Constants ──────────────────────────────────────────────────

export const COLOR_TOKENS = [
  "color-bg", "color-surface", "color-text", "color-text-muted",
  "color-primary", "color-primary-hover", "color-secondary", "color-border",
  "color-shadow", "color-success", "color-warning", "color-danger",
  "color-code-bg", "color-code-text",
] as const;

export const TYPOGRAPHY_TOKENS = [
  "font-body", "font-heading", "font-mono",
  "font-size-base", "font-size-sm", "font-size-xs", "font-size-lg",
  "font-size-xl", "font-size-2xl",
  "font-weight-normal", "font-weight-bold",
  "line-height-body", "line-height-heading",
] as const;

export const SPACING_TOKENS = [
  "space-xs", "space-sm", "space-md", "space-lg", "space-xl",
  "radius", "border-width", "shadow-offset", "content-width",
] as const;

export const COMPONENT_TOKENS = [
  "button-radius", "button-shadow",
  "card-radius", "card-shadow",
  "btn-press-hover", "btn-press-active",
  "input-radius", "input-border",
  "nav-height",
] as const;

export const ALL_KNOWN_TOKENS: readonly string[] = [
  ...COLOR_TOKENS, ...TYPOGRAPHY_TOKENS, ...SPACING_TOKENS, ...COMPONENT_TOKENS,
];

// ─── Helpers ────────────────────────────────────────────────────────────────

/**
 * Extracts the error message from an AppView error response.
 * Falls back to the provided default if JSON parsing fails.
 */
async function extractAppviewError(res: Response, fallback: string): Promise<string> {
  try {
    const data = (await res.json()) as { error?: string };
    return data.error ?? fallback;
  } catch (error) {
    logger.error("Failed to parse AppView error response body", {
      operation: "extractAppviewError",
      status: res.status,
      error: error instanceof Error ? error.message : String(error),
    });
    return fallback;
  }
}

/** Drop token values that could break the CSS style block. */
function sanitizeTokenValue(value: unknown): string | null {
  if (typeof value !== "string") return null;
  if (value.includes("<") || value.includes(";") || value.includes("}")) return null;
  return value;
}

/** Produce a URL-safe filename slug from a theme name, with a fallback. */
function slugifyName(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "") || "theme";
}

/** Returns true only for absolute HTTPS URLs. */
function isHttpsUrl(url: unknown): boolean {
  if (typeof url !== "string") return false;
  try {
    return new URL(url).protocol === "https:";
  } catch {
    return false;
  }
}

// ─── JSX Components ─────────────────────────────────────────────────────────

function ColorTokenInput({ name, value }: { name: string; value: string }) {
  const safeValue =
    !value.startsWith("var(") && !value.includes(";") && !value.includes("<")
      ? value
      : "#cccccc";
  return (
    <div class="token-input token-input--color">
      <label for={`token-${name}`}>{name}</label>
      <div class="token-input__controls">
        <input
          type="color"
          value={safeValue}
          aria-label={`${name} color picker`}
          oninput="this.nextElementSibling.value=this.value;this.nextElementSibling.dispatchEvent(new Event('change',{bubbles:true}))"
        />
        <input
          type="text"
          id={`token-${name}`}
          name={name}
          value={safeValue}
          oninput="if(/^#[0-9a-fA-F]{6}$/.test(this.value))this.previousElementSibling.value=this.value"
        />
      </div>
    </div>
  );
}

function TextTokenInput({ name, value }: { name: string; value: string }) {
  return (
    <div class="token-input">
      <label for={`token-${name}`}>{name}</label>
      <input type="text" id={`token-${name}`} name={name} value={value} />
    </div>
  );
}

function TokenFieldset({
  legend,
  tokens,
  effectiveTokens,
  isColor,
}: {
  legend: string;
  tokens: readonly string[];
  effectiveTokens: Record<string, string>;
  isColor: boolean;
}) {
  return (
    <fieldset class="token-group">
      <legend>{legend}</legend>
      {tokens.map((name) =>
        isColor ? (
          <ColorTokenInput name={name} value={effectiveTokens[name] ?? ""} />
        ) : (
          <TextTokenInput name={name} value={effectiveTokens[name] ?? ""} />
        )
      )}
    </fieldset>
  );
}

function ThemePreviewContent({ tokens }: { tokens: Record<string, string> }) {
  const css = tokensToCss(tokens);
  return (
    <>
      <style>{`.preview-pane-inner{${css}}`}</style>
      <div class="preview-pane-inner">
        <div
          style="background:var(--color-surface);border-bottom:var(--border-width) solid var(--color-border);padding:var(--space-sm) var(--space-md);display:flex;align-items:center;font-family:var(--font-heading);font-weight:var(--font-weight-bold);font-size:var(--font-size-lg);color:var(--color-text);"
          role="navigation"
          aria-label="Preview navigation"
        >
          atBB Forum Preview
        </div>
        <div style="padding:var(--space-md);">
          <div
            style="background:var(--color-surface);border:var(--border-width) solid var(--color-border);border-radius:var(--card-radius);box-shadow:var(--card-shadow);padding:var(--space-md);margin-bottom:var(--space-md);"
          >
            <h2
              style="font-family:var(--font-heading);font-size:var(--font-size-xl);font-weight:var(--font-weight-bold);line-height:var(--line-height-heading);color:var(--color-text);margin:0 0 var(--space-sm) 0;"
            >
              Sample Thread Title
            </h2>
            <p style="font-family:var(--font-body);font-size:var(--font-size-base);line-height:var(--line-height-body);color:var(--color-text);margin:0 0 var(--space-md) 0;">
              Body text showing font, color, and spacing at work.{" "}
              <a href="#" style="color:var(--color-primary);">A sample link</a>
            </p>
            <pre
              style="font-family:var(--font-mono);font-size:var(--font-size-sm);background:var(--color-code-bg);color:var(--color-code-text);padding:var(--space-sm) var(--space-md);border-radius:var(--radius);margin:0 0 var(--space-md) 0;overflow-x:auto;"
            >
              {`const greeting = "hello forum";`}
            </pre>
            <input
              type="text"
              placeholder="Reply…"
              style="font-family:var(--font-body);font-size:var(--font-size-base);border:var(--input-border);border-radius:var(--input-radius);padding:var(--space-sm) var(--space-md);width:100%;box-sizing:border-box;background:var(--color-bg);color:var(--color-text);margin-bottom:var(--space-sm);"
            />
            <div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
              <button
                type="button"
                style="background:var(--color-primary);color:var(--color-surface);border:var(--border-width) solid var(--color-border);border-radius:var(--button-radius);box-shadow:var(--button-shadow);font-family:var(--font-body);font-weight:var(--font-weight-bold);padding:var(--space-sm) var(--space-md);cursor:pointer;"
              >
                Post Reply
              </button>
              <button
                type="button"
                style="background:var(--color-surface);color:var(--color-text);border:var(--border-width) solid var(--color-border);border-radius:var(--button-radius);box-shadow:var(--button-shadow);font-family:var(--font-body);font-weight:var(--font-weight-bold);padding:var(--space-sm) var(--space-md);cursor:pointer;"
              >
                Cancel
              </button>
              <span
                style="display:inline-block;background:var(--color-success);color:var(--color-surface);border:var(--border-width) solid var(--color-border);padding:0 var(--space-sm);font-size:var(--font-size-sm);"
              >
                success
              </span>
              <span
                style="display:inline-block;background:var(--color-warning);color:var(--color-text);border:var(--border-width) solid var(--color-border);padding:0 var(--space-sm);font-size:var(--font-size-sm);"
              >
                warning
              </span>
              <span
                style="display:inline-block;background:var(--color-danger);color:var(--color-surface);border:var(--border-width) solid var(--color-border);padding:0 var(--space-sm);font-size:var(--font-size-sm);"
              >
                danger
              </span>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}


// ─── Route Factory ──────────────────────────────────────────────────────────

export function createAdminThemeRoutes(appviewUrl: string) {
  const app = new Hono<WebAppEnv>();

  // ── GET /admin/themes ──────────────────────────────────────────────────────

  app.get("/admin/themes", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));

    if (!auth.authenticated) {
      return c.redirect("/login");
    }

    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied — atBB Forum" auth={auth} resolvedTheme={resolvedTheme}>
          <PageHeader title="Themes" />
          <p>You don&apos;t have permission to manage themes.</p>
        </BaseLayout>,
        403
      );
    }

    const cookie = c.req.header("cookie") ?? "";
    const errorMsg = c.req.query("error") ?? null;

    let adminThemes: AdminThemeEntry[] = [];
    let policy: ThemePolicy | null = null;

    try {
      const [themesRes, policyRes] = await Promise.all([
        fetch(`${appviewUrl}/api/admin/themes`, { headers: { Cookie: cookie } }),
        fetch(`${appviewUrl}/api/theme-policy`, { headers: { Cookie: cookie } }),
      ]);

      if (themesRes.ok) {
        try {
          const data = (await themesRes.json()) as { themes: AdminThemeEntry[] };
          adminThemes = data.themes;
        } catch {
          logger.error("Failed to parse admin themes response", {
            operation: "GET /admin/themes",
            status: themesRes.status,
          });
        }
      } else {
        logger.error("Failed to fetch admin themes list", {
          operation: "GET /admin/themes",
          status: themesRes.status,
        });
      }

      if (policyRes.ok) {
        try {
          policy = (await policyRes.json()) as ThemePolicy;
        } catch {
          logger.error("Failed to parse theme policy response", {
            operation: "GET /admin/themes",
            status: policyRes.status,
          });
        }
      } else if (policyRes.status !== 404) {
        logger.error("Failed to fetch theme policy", {
          operation: "GET /admin/themes",
          status: policyRes.status,
        });
      }
      // 404 = no policy yet — render page with empty policy (not an error)
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error fetching themes data", {
        operation: "GET /admin/themes",
        error: error instanceof Error ? error.message : String(error),
      });
    }

    const availableUris = new Set((policy?.availableThemes ?? []).map((t) => t.uri));
    const lightThemes = adminThemes.filter((t) => t.colorScheme === "light");
    const darkThemes = adminThemes.filter((t) => t.colorScheme === "dark");

    return c.html(
      <BaseLayout title="Themes — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
        <PageHeader title="Themes" />

        {errorMsg && <div class="structure-error-banner">{errorMsg}</div>}

        {adminThemes.length === 0 ? (
          <EmptyState message="No themes yet. Create one below." />
        ) : (
          <div class="structure-list">
            {adminThemes.map((theme) => {
              const themeRkey = theme.uri.split("/").pop() ?? theme.id;
              const dialogId = `confirm-delete-theme-${themeRkey}`;
              const swatchTokens = [
                "color-bg",
                "color-surface",
                "color-primary",
                "color-secondary",
                "color-border",
              ] as const;

              return (
                <div class="structure-item">
                  <div class="structure-item__header">
                    <div class="structure-item__title">
                      <label>
                        <input
                          type="checkbox"
                          form="policy-form"
                          name="availableThemes"
                          value={theme.uri}
                          checked={availableUris.has(theme.uri)}
                        />
                        {" "}
                        {theme.name}
                      </label>
                      <span class={`badge badge--${theme.colorScheme}`}>
                        {theme.colorScheme}
                      </span>
                    </div>

                    <div class="theme-swatches" aria-hidden="true">
                      {swatchTokens.map((token) => {
                        const value = theme.tokens[token] ?? "#cccccc";
                        const safe =
                          !value.startsWith("var(") &&
                          !value.includes(";") &&
                          !value.includes("<");
                        return (
                          <span
                            class="theme-swatch"
                            style={safe ? `background:${value}` : "background:#cccccc"}
                            title={token}
                          />
                        );
                      })}
                    </div>

                    <div class="structure-item__actions">
                      <a href={`/admin/themes/${themeRkey}`} class="btn btn-secondary btn-sm">
                        Edit
                      </a>

                      <a href={`/admin/themes/${themeRkey}/export`} class="btn btn-secondary btn-sm">
                        Export
                      </a>

                      <form
                        method="post"
                        action={`/admin/themes/${themeRkey}/duplicate`}
                        style="display:inline"
                      >
                        <button type="submit" class="btn btn-secondary btn-sm">
                          Duplicate
                        </button>
                      </form>

                      <button
                        type="button"
                        class="btn btn-danger btn-sm"
                        onclick={`document.getElementById('${dialogId}').showModal()`}
                      >
                        Delete
                      </button>
                    </div>
                  </div>

                  <dialog id={dialogId} class="structure-confirm-dialog">
                    <p>
                      Delete theme &quot;{theme.name}&quot;? This cannot be undone.
                    </p>
                    <form
                      method="post"
                      action={`/admin/themes/${themeRkey}/delete`}
                      class="dialog-actions"
                    >
                      <button type="submit" class="btn btn-danger">
                        Delete
                      </button>
                      <button
                        type="button"
                        class="btn btn-secondary"
                        onclick={`document.getElementById('${dialogId}').close()`}
                      >
                        Cancel
                      </button>
                    </form>
                  </dialog>
                </div>
              );
            })}
          </div>
        )}

        {/* Policy form — availability checkboxes on cards associate via form="policy-form" */}
        <section class="admin-section">
          <h2>Theme Policy</h2>
          <form id="policy-form" method="post" action="/admin/theme-policy">
            <div class="form-group">
              <label for="defaultLightThemeUri">Default Light Theme</label>
              <select id="defaultLightThemeUri" name="defaultLightThemeUri">
                <option value="">— none —</option>
                {lightThemes.map((t) => (
                  <option
                    value={t.uri}
                    selected={policy?.defaultLightThemeUri === t.uri}
                  >
                    {t.name}
                  </option>
                ))}
              </select>
            </div>

            <div class="form-group">
              <label for="defaultDarkThemeUri">Default Dark Theme</label>
              <select id="defaultDarkThemeUri" name="defaultDarkThemeUri">
                <option value="">— none —</option>
                {darkThemes.map((t) => (
                  <option
                    value={t.uri}
                    selected={policy?.defaultDarkThemeUri === t.uri}
                  >
                    {t.name}
                  </option>
                ))}
              </select>
            </div>

            <div class="form-group">
              <label>
                <input
                  type="checkbox"
                  name="allowUserChoice"
                  checked={policy?.allowUserChoice ?? true}
                />
                {" "}Allow users to choose their own theme
              </label>
            </div>

            <p class="form-hint">
              Check themes above to make them available to users.
            </p>
            <button type="submit" class="btn btn-primary">
              Save Policy
            </button>
          </form>
        </section>

        {/* Create new theme */}
        <details class="structure-add-form">
          <summary class="structure-add-form__trigger">+ Create New Theme</summary>
          <form
            method="post"
            action="/admin/themes"
            class="structure-edit-form__body"
          >
            <div class="form-group">
              <label for="new-theme-name">Name</label>
              <input
                id="new-theme-name"
                type="text"
                name="name"
                required
                placeholder="My Custom Theme"
              />
            </div>
            <div class="form-group">
              <label for="new-theme-scheme">Color Scheme</label>
              <select id="new-theme-scheme" name="colorScheme">
                <option value="light">Light</option>
                <option value="dark">Dark</option>
              </select>
            </div>
            <div class="form-group">
              <label for="new-theme-preset">Start from Preset</label>
              <select id="new-theme-preset" name="preset">
                <option value="neobrutal-light">Neobrutal Light</option>
                <option value="neobrutal-dark">Neobrutal Dark</option>
                <option value="blank">Blank</option>
              </select>
            </div>
            <button type="submit" class="btn btn-primary">
              Create Theme
            </button>
          </form>
        </details>

        {/* Import theme from JSON */}
        <details class="structure-add-form">
          <summary class="structure-add-form__trigger">↑ Import Theme from JSON</summary>
          <form
            method="post"
            action="/admin/themes/import"
            enctype="multipart/form-data"
            class="structure-edit-form__body"
          >
            <div class="form-group">
              <label for="import-theme-file">Theme JSON file</label>
              <input
                id="import-theme-file"
                type="file"
                name="themeFile"
                accept=".json"
                required
              />
              <p class="form-hint">
                Imports name, colorScheme, tokens, and fontUrls.
                CSS overrides and unknown token keys are ignored.
              </p>
            </div>
            <button type="submit" class="btn btn-primary">
              Import Theme
            </button>
          </form>
        </details>
      </BaseLayout>
    );
  });

  // ── GET /admin/themes/:rkey/export ────────────────────────────────────────
  // Distinct from /:rkey — the 4-segment path cannot match the 3-segment /:rkey route.

  app.get("/admin/themes/:rkey/export", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
          <PageHeader title="Access Denied" />
          <p>You don&apos;t have permission to manage themes.</p>
        </BaseLayout>,
        403
      );
    }

    const themeRkey = c.req.param("rkey");

    let theme: AdminThemeEntry | null = null;
    try {
      const res = await fetch(`${appviewUrl}/api/themes/${themeRkey}`);
      if (res.status === 404) {
        return c.html(
          <BaseLayout title="Theme Not Found — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
            <PageHeader title="Theme Not Found" />
            <p>This theme does not exist.</p>
            <a href="/admin/themes" class="btn btn-secondary">← Back to themes</a>
          </BaseLayout>,
          404
        );
      }
      if (res.ok) {
        try {
          theme = (await res.json()) as AdminThemeEntry;
        } catch (error) {
          if (isProgrammingError(error)) throw error;
          logger.error("Failed to parse theme response for export", {
            operation: "GET /admin/themes/:rkey/export",
            themeRkey,
            error: error instanceof Error ? error.message : String(error),
          });
        }
      } else {
        logger.error("AppView returned error loading theme for export", {
          operation: "GET /admin/themes/:rkey/export",
          themeRkey,
          status: res.status,
        });
      }
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error loading theme for export", {
        operation: "GET /admin/themes/:rkey/export",
        themeRkey,
        error: error instanceof Error ? error.message : String(error),
      });
    }

    if (!theme) {
      return c.html(
        <BaseLayout title="Export Failed — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
          <PageHeader title="Export Failed" />
          <p>Unable to load theme data. Please try again.</p>
          <a href="/admin/themes" class="btn btn-secondary">← Back to themes</a>
        </BaseLayout>,
        500
      );
    }

    // cssOverrides excluded from export — it contains raw CSS that may reference
    // external resources and is tied to this forum's sanitization config.
    const exportData = {
      name: theme.name,
      colorScheme: theme.colorScheme,
      tokens: theme.tokens,
      fontUrls: theme.fontUrls ?? [],
    };

    const filename = `${slugifyName(theme.name)}-${slugifyName(theme.colorScheme)}.json`;
    c.header("Content-Type", "application/json");
    c.header("Content-Disposition", `attachment; filename="${filename}"`);
    return c.body(JSON.stringify(exportData, null, 2), 200);
  });

  // ── GET /admin/themes/:rkey ────────────────────────────────────────────────

  app.get("/admin/themes/:rkey", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
          <PageHeader title="Access Denied" />
          <p>You don&apos;t have permission to manage themes.</p>
        </BaseLayout>,
        403
      );
    }

    const themeRkey = c.req.param("rkey");
    const presetParam = c.req.query("preset") ?? null;
    const successMsg = c.req.query("success") === "1" ? "Theme saved successfully." : null;
    const errorMsg = c.req.query("error") ?? null;

    // Fetch theme from AppView
    let theme: AdminThemeEntry | null = null;
    try {
      const res = await fetch(`${appviewUrl}/api/themes/${themeRkey}`);
      if (res.status === 404) {
        return c.html(
          <BaseLayout title="Theme Not Found — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
            <PageHeader title="Theme Not Found" />
            <p>This theme does not exist.</p>
            <a href="/admin/themes" class="btn btn-secondary">← Back to themes</a>
          </BaseLayout>,
          404
        );
      }
      if (res.ok) {
        try {
          theme = (await res.json()) as AdminThemeEntry;
        } catch {
          logger.error("Failed to parse theme response", {
            operation: "GET /admin/themes/:rkey",
            themeRkey,
          });
        }
      } else {
        logger.error("AppView returned error loading theme", {
          operation: "GET /admin/themes/:rkey",
          themeRkey,
          status: res.status,
        });
      }
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error loading theme", {
        operation: "GET /admin/themes/:rkey",
        themeRkey,
        error: error instanceof Error ? error.message : String(error),
      });
    }

    if (!theme) {
      return c.html(
        <BaseLayout title="Theme Unavailable — atBB Admin" auth={auth} resolvedTheme={resolvedTheme}>
          <PageHeader title="Theme Unavailable" />
          <p>Unable to load theme data. Please try again.</p>
          <a href="/admin/themes" class="btn btn-secondary">← Back to themes</a>
        </BaseLayout>,
        500
      );
    }

    // If ?preset is set, override DB tokens with preset tokens
    const presetTokens = presetParam ? (THEME_PRESETS[presetParam] ?? null) : null;
    const effectiveTokens: Record<string, string> = presetTokens
      ? { ...theme.tokens, ...presetTokens }
      : { ...theme.tokens };

    const fontUrlsText = (theme.fontUrls ?? []).join("\n");

    return c.html(
      <BaseLayout title={`Edit Theme: ${theme.name} — atBB Admin`} auth={auth} resolvedTheme={resolvedTheme}>
        <PageHeader title={`Edit Theme: ${theme.name}`} />

        {successMsg && <div class="structure-success-banner">{successMsg}</div>}
        {errorMsg && <div class="structure-error-banner">{errorMsg}</div>}

        <a href="/admin/themes" class="btn btn-secondary btn-sm" style="margin-bottom: var(--space-md); display: inline-block;">
          ← Back to themes
        </a>

        {/* Metadata + tokens form */}
        <form
          id="editor-form"
          method="post"
          action={`/admin/themes/${themeRkey}/save`}
          class="theme-editor"
        >
          {/* Metadata */}
          <fieldset class="token-group">
            <legend>Theme Metadata</legend>
            <div class="token-input">
              <label for="theme-name">Name</label>
              <input type="text" id="theme-name" name="name" value={theme.name} required />
            </div>
            <div class="token-input">
              <label for="theme-scheme">Color Scheme</label>
              <select id="theme-scheme" name="colorScheme">
                <option value="light" selected={theme.colorScheme === "light" ? true : undefined}>Light</option>
                <option value="dark" selected={theme.colorScheme === "dark" ? true : undefined}>Dark</option>
              </select>
            </div>
            <div class="token-input">
              <label for="theme-font-urls">Font URLs (one per line)</label>
              <textarea id="theme-font-urls" name="fontUrls" rows={3} placeholder="https://fonts.googleapis.com/css2?family=...">
                {fontUrlsText}
              </textarea>
            </div>
          </fieldset>

          {/* Token editor + live preview layout */}
          <div class="theme-editor__layout">
            {/* Left: token controls */}
            <div
              class="theme-editor__controls"
              hx-post={`/admin/themes/${themeRkey}/preview`}
              hx-trigger="input delay:500ms"
              hx-target="#preview-pane"
              hx-include="#editor-form"
            >
              <TokenFieldset
                legend="Colors"
                tokens={COLOR_TOKENS}
                effectiveTokens={effectiveTokens}
                isColor={true}
              />
              <TokenFieldset
                legend="Typography"
                tokens={TYPOGRAPHY_TOKENS}
                effectiveTokens={effectiveTokens}
                isColor={false}
              />
              <TokenFieldset
                legend="Spacing & Layout"
                tokens={SPACING_TOKENS}
                effectiveTokens={effectiveTokens}
                isColor={false}
              />
              <TokenFieldset
                legend="Components"
                tokens={COMPONENT_TOKENS}
                effectiveTokens={effectiveTokens}
                isColor={false}
              />

              {/* CSS overrides */}
              <fieldset class="token-group">
                <legend>CSS Overrides</legend>
                <div class="token-input">
                  <label for="css-overrides">Custom CSS</label>
                  <textarea
                    id="css-overrides"
                    name="cssOverrides"
                    rows={6}
                    aria-describedby="css-overrides-hint"
                    placeholder="/* Structural overrides beyond what design tokens allow */"
                  >
                    {theme.cssOverrides ?? ""}
                  </textarea>
                  <p id="css-overrides-hint" class="form-hint">
                    Raw CSS for structural changes. Dangerous constructs (external
                    URLs, @import, expression()) are stripped automatically on save.
                  </p>
                </div>
              </fieldset>
            </div>

            {/* Right: live preview */}
            <div class="theme-editor__preview">
              <h3>Live Preview</h3>
              <div id="preview-pane" class="preview-pane">
                <ThemePreviewContent tokens={effectiveTokens} />
              </div>
            </div>
          </div>

          {/* Actions */}
          <div class="theme-editor__actions">
            <button type="submit" class="btn btn-primary">Save Theme</button>

            <button
              type="button"
              class="btn btn-secondary"
              onclick="document.getElementById('reset-dialog').showModal()"
            >
              Reset to Preset
            </button>
          </div>
        </form>

        {/* Reset to preset dialog */}
        <dialog id="reset-dialog" class="structure-confirm-dialog">
          <form method="post" action={`/admin/themes/${themeRkey}/reset-to-preset`}>
            <p>Reset all token values to a built-in preset? Your unsaved changes will be lost.</p>
            <div class="form-group">
              <label for="reset-preset-select">Reset to preset:</label>
              <select id="reset-preset-select" name="preset">
                <option value="neobrutal-light">Neobrutal Light</option>
                <option value="neobrutal-dark">Neobrutal Dark</option>
                <option value="blank">Blank (empty tokens)</option>
              </select>
            </div>
            <div class="dialog-actions">
              <button type="submit" class="btn btn-danger">Reset</button>
              <button
                type="button"
                class="btn btn-secondary"
                onclick="document.getElementById('reset-dialog').close()"
              >
                Cancel
              </button>
            </div>
          </form>
        </dialog>
      </BaseLayout>
    );
  });

  // ── POST /admin/themes ────────────────────────────────────────────────────

  app.post("/admin/themes", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(<BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}><p>Access denied.</p></BaseLayout>, 403);
    }

    const cookie = c.req.header("cookie") ?? "";

    let body: Record<string, string | File>;
    try {
      body = await c.req.parseBody();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Invalid form submission.")}`,
        302
      );
    }

    const name = typeof body.name === "string" ? body.name.trim() : "";
    const colorScheme = typeof body.colorScheme === "string" ? body.colorScheme : "light";
    const preset = typeof body.preset === "string" ? body.preset : "blank";

    if (!name) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Theme name is required.")}`,
        302
      );
    }

    const tokens = THEME_PRESETS[preset] ?? {};

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/themes`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Cookie: cookie },
        body: JSON.stringify({ name, colorScheme, tokens }),
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error creating theme", {
        operation: "POST /admin/themes",
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      const msg = await extractAppviewError(apiRes, "Failed to create theme. Please try again.");
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent(msg)}`,
        302
      );
    }

    return c.redirect("/admin/themes", 302);
  });

  // ── POST /admin/themes/import ─────────────────────────────────────────────
  // File upload: reads the JSON file, validates structure, strips unknown tokens
  // and cssOverrides, then delegates to POST /api/admin/themes.

  app.post("/admin/themes/import", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}>
          <p>Access denied.</p>
        </BaseLayout>,
        403
      );
    }

    const cookie = c.req.header("cookie") ?? "";

    let rawBody: Record<string, string | File>;
    try {
      rawBody = await c.req.parseBody();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Failed to parse import form body", {
        operation: "POST /admin/themes/import",
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Invalid form submission.")}`,
        302
      );
    }

    const uploaded = rawBody.themeFile;
    if (!uploaded || typeof uploaded === "string" || uploaded.size === 0) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Please select a JSON file to import.")}`,
        302
      );
    }

    const MAX_IMPORT_BYTES = 100 * 1024; // 100 KB
    if (uploaded.size > MAX_IMPORT_BYTES) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Import failed: file exceeds the 100 KB size limit.")}`,
        302
      );
    }

    // Read the file text and parse as JSON — two separate try blocks so encoding
    // failures and JSON syntax errors produce distinct log entries.
    let text: string;
    try {
      text = await uploaded.text();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Failed to read uploaded theme file", {
        operation: "POST /admin/themes/import",
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Import failed: could not read the uploaded file.")}`,
        302
      );
    }

    let parsed: unknown;
    try {
      parsed = JSON.parse(text);
    } catch {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Import failed: file is not valid JSON.")}`,
        302
      );
    }

    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Import failed: file must be a JSON object.")}`,
        302
      );
    }

    const obj = parsed as Record<string, unknown>;

    // Validate required: name
    const name = typeof obj.name === "string" ? obj.name.trim() : "";
    if (!name) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent('Import failed: missing required field "name".')}`,
        302
      );
    }

    // Validate required: colorScheme
    if (obj.colorScheme !== "light" && obj.colorScheme !== "dark") {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent(
          'Import failed: colorScheme must be "light" or "dark".'
        )}`,
        302
      );
    }
    const colorScheme = obj.colorScheme;

    // Validate required: tokens (must be a plain object)
    if (typeof obj.tokens !== "object" || obj.tokens === null || Array.isArray(obj.tokens)) {
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent('Import failed: missing required field "tokens".')}`,
        302
      );
    }

    // Strip unknown token keys — only recognized tokens pass through
    const rawTokens = obj.tokens as Record<string, unknown>;
    const tokens: Record<string, string> = {};
    for (const key of ALL_KNOWN_TOKENS) {
      const val = rawTokens[key];
      if (typeof val === "string") {
        tokens[key] = val;
      }
    }

    // Validate fontUrls — each must be an HTTPS URL
    let fontUrls: string[] | undefined;
    if (obj.fontUrls !== undefined) {
      if (!Array.isArray(obj.fontUrls)) {
        return c.redirect(
          `/admin/themes?error=${encodeURIComponent("Import failed: fontUrls must be an array.")}`,
          302
        );
      }
      for (const url of obj.fontUrls) {
        if (!isHttpsUrl(url)) {
          return c.redirect(
            `/admin/themes?error=${encodeURIComponent(
              `Import failed: font URL must be HTTPS: ${String(url)}`
            )}`,
            302
          );
        }
      }
      // Safe: every element has passed isHttpsUrl(), which verifies typeof === "string"
      fontUrls = obj.fontUrls as string[];
    }

    // cssOverrides is silently dropped — keeps the shared JSON schema portable
    // (no forum-specific CSS bleeding in) and avoids importing structural overrides
    // that may not make sense in the target forum's layout.

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/themes`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Cookie: cookie },
        body: JSON.stringify({
          name,
          colorScheme,
          tokens,
          ...(fontUrls !== undefined && { fontUrls }),
        }),
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error importing theme", {
        operation: "POST /admin/themes/import",
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      const msg = await extractAppviewError(apiRes, "Failed to import theme. Please try again.");
      return c.redirect(`/admin/themes?error=${encodeURIComponent(msg)}`, 302);
    }

    return c.redirect("/admin/themes", 302);
  });

  // ── POST /admin/themes/:rkey/duplicate ────────────────────────────────────

  app.post("/admin/themes/:rkey/duplicate", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(<BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}><p>Access denied.</p></BaseLayout>, 403);
    }

    const cookie = c.req.header("cookie") ?? "";
    const themeRkey = c.req.param("rkey");

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/themes/${themeRkey}/duplicate`, {
        method: "POST",
        headers: { Cookie: cookie },
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error duplicating theme", {
        operation: "POST /admin/themes/:rkey/duplicate",
        themeRkey,
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      const msg = await extractAppviewError(apiRes, "Failed to duplicate theme. Please try again.");
      return c.redirect(`/admin/themes?error=${encodeURIComponent(msg)}`, 302);
    }

    return c.redirect("/admin/themes", 302);
  });

  // ── POST /admin/themes/:rkey/delete ──────────────────────────────────────

  app.post("/admin/themes/:rkey/delete", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(<BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}><p>Access denied.</p></BaseLayout>, 403);
    }

    const cookie = c.req.header("cookie") ?? "";
    const themeRkey = c.req.param("rkey");

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/themes/${themeRkey}`, {
        method: "DELETE",
        headers: { Cookie: cookie },
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error deleting theme", {
        operation: "POST /admin/themes/:rkey/delete",
        themeRkey,
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      if (apiRes.status === 409) {
        return c.redirect(
          `/admin/themes?error=${encodeURIComponent("Cannot delete a theme that is currently set as a default.")}`,
          302
        );
      }
      const msg = await extractAppviewError(apiRes, "Failed to delete theme. Please try again.");
      return c.redirect(`/admin/themes?error=${encodeURIComponent(msg)}`, 302);
    }

    return c.redirect("/admin/themes", 302);
  });

  // ── POST /admin/theme-policy ──────────────────────────────────────────────

  app.post("/admin/theme-policy", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(<BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}><p>Access denied.</p></BaseLayout>, 403);
    }

    const cookie = c.req.header("cookie") ?? "";

    let rawBody: Record<string, string | string[] | File | File[]>;
    try {
      rawBody = await c.req.parseBody({ all: true });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Invalid form submission.")}`,
        302
      );
    }

    const defaultLightThemeUri =
      typeof rawBody.defaultLightThemeUri === "string" ? rawBody.defaultLightThemeUri : "";
    const defaultDarkThemeUri =
      typeof rawBody.defaultDarkThemeUri === "string" ? rawBody.defaultDarkThemeUri : "";
    // Checkbox: present with value "on" when checked, absent when unchecked
    const allowUserChoice = rawBody.allowUserChoice === "on";

    // availableThemes may be a single string, an array, or absent
    const rawAvailable = rawBody.availableThemes;
    const availableThemes =
      rawAvailable === undefined
        ? []
        : Array.isArray(rawAvailable)
          ? rawAvailable.filter((v): v is string => typeof v === "string")
          : typeof rawAvailable === "string"
            ? [rawAvailable]
            : [];

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/theme-policy`, {
        method: "PUT",
        headers: { "Content-Type": "application/json", Cookie: cookie },
        body: JSON.stringify({
          defaultLightThemeUri,
          defaultDarkThemeUri,
          allowUserChoice,
          availableThemes: availableThemes.map((uri) => ({ uri })),
        }),
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error updating theme policy", {
        operation: "POST /admin/theme-policy",
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      const msg = await extractAppviewError(apiRes, "Failed to update theme policy. Please try again.");
      return c.redirect(`/admin/themes?error=${encodeURIComponent(msg)}`, 302);
    }

    return c.redirect("/admin/themes", 302);
  });

  // ── POST /admin/themes/:rkey/preview ─────────────────────────────────────

  app.post("/admin/themes/:rkey/preview", async (c) => {
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html("", 403);
    }

    let rawBody: Record<string, string | File>;
    try {
      rawBody = await c.req.parseBody();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      // Return empty preview on parse error — don't break the HTMX swap
      return c.html(<ThemePreviewContent tokens={{}} />);
    }

    // Only accept known token names — ignore metadata fields like name/colorScheme
    const tokens: Record<string, string> = {};
    for (const tokenName of ALL_KNOWN_TOKENS) {
      const raw = rawBody[tokenName];
      if (typeof raw !== "string") continue;
      const safe = sanitizeTokenValue(raw);
      if (safe !== null) {
        tokens[tokenName] = safe;
      }
    }

    return c.html(<ThemePreviewContent tokens={tokens} />);
  });

  // ── POST /admin/themes/:rkey/save ─────────────────────────────────────────

  app.post("/admin/themes/:rkey/save", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}>
          <p>Access denied.</p>
        </BaseLayout>,
        403
      );
    }

    const themeRkey = c.req.param("rkey");
    const cookie = c.req.header("cookie") ?? "";

    let rawBody: Record<string, string | File>;
    try {
      rawBody = await c.req.parseBody();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid form submission.")}`,
        302
      );
    }

    const name = typeof rawBody.name === "string" ? rawBody.name.trim() : "";
    if (!name) {
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent("Theme name is required.")}`,
        302
      );
    }
    const colorScheme = typeof rawBody.colorScheme === "string" ? rawBody.colorScheme : "light";
    const fontUrlsRaw = typeof rawBody.fontUrls === "string" ? rawBody.fontUrls : "";
    const fontUrls = fontUrlsRaw
      .split("\n")
      .map((u) => u.trim())
      .filter(Boolean);
    const cssOverrides =
      typeof rawBody.cssOverrides === "string" ? rawBody.cssOverrides : undefined;

    // Extract token values from form fields
    const tokens: Record<string, string> = {};
    for (const tokenName of ALL_KNOWN_TOKENS) {
      const raw = rawBody[tokenName];
      if (typeof raw !== "string") continue;
      const safe = sanitizeTokenValue(raw.trim());
      if (safe !== null && safe !== "") {
        tokens[tokenName] = safe;
      }
    }

    let apiRes: Response;
    try {
      apiRes = await fetch(`${appviewUrl}/api/admin/themes/${themeRkey}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json", Cookie: cookie },
        body: JSON.stringify({ name, colorScheme, tokens, fontUrls, cssOverrides }),
      });
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      logger.error("Network error saving theme", {
        operation: "POST /admin/themes/:rkey/save",
        themeRkey,
        error: error instanceof Error ? error.message : String(error),
      });
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`,
        302
      );
    }

    if (!apiRes.ok) {
      const msg = await extractAppviewError(apiRes, "Failed to save theme. Please try again.");
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent(msg)}`,
        302
      );
    }

    return c.redirect(`/admin/themes/${themeRkey}?success=1`, 302);
  });

  // ── POST /admin/themes/:rkey/reset-to-preset ──────────────────────────────

  app.post("/admin/themes/:rkey/reset-to-preset", async (c) => {
    const resolvedTheme = c.get("theme") ?? FALLBACK_THEME;
    const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie"));
    if (!auth.authenticated) return c.redirect("/login");
    if (!canManageThemes(auth)) {
      return c.html(
        <BaseLayout title="Access Denied" auth={auth} resolvedTheme={resolvedTheme}>
          <p>Access denied.</p>
        </BaseLayout>,
        403
      );
    }

    const themeRkey = c.req.param("rkey");

    let body: Record<string, string | File>;
    try {
      body = await c.req.parseBody();
    } catch (error) {
      if (isProgrammingError(error)) throw error;
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid form submission.")}`,
        302
      );
    }

    const preset = typeof body.preset === "string" ? body.preset : "";
    if (!(preset in THEME_PRESETS)) {
      return c.redirect(
        `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid preset name.")}`,
        302
      );
    }

    return c.redirect(`/admin/themes/${themeRkey}?preset=${encodeURIComponent(preset)}`, 302);
  });

  return app;
}

