import { FontFamilyPicker } from '@/Components/FontFamilyPicker';
import { Button, Checkbox } from '@/Components/ui';
import { cn } from '@/lib/cn';
import { derive, type ThemeKnobs } from '@/lib/themePalette';
import { ChevronDown, LayoutGrid, RotateCcw } from 'lucide-react';
import { useMemo, useState, type ReactNode } from 'react';

export type PresetSummary = {
    key: string;
    name: string;
    description: string;
    knobs: ThemeKnobs;
};

type Props = {
    knobs: ThemeKnobs;
    onChange: (next: ThemeKnobs) => void;
    onReset: () => void;
    presets: PresetSummary[];
    onOpenPresets: () => void;
};

const SCALE_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;

// Each option carries its own preview bg/fg (matching what themePalette's
// buildScale produces for the 50/900 steps) so the swatch shows the actual
// background + foreground combo the admin will get, not a mid-gray that's
// indistinguishable across options.
const NEUTRAL_TEMPS: Array<{
    key: Exclude<ThemeKnobs['neutral_temp'], 'custom'>;
    label: string;
    hint: string;
    bg: string;
    fg: string;
}> = [
    {
        key: 'warm',
        label: 'Warm',
        hint: 'Cream-tinted',
        // L=0.97 c=0.008*0.35 h=75 — matches buildScale[50] for the warm base.
        bg: 'oklch(0.97 0.0028 75)',
        // L=0.18 c=0.008*0.55 h=75 — matches buildScale[900].
        fg: 'oklch(0.18 0.0044 75)',
    },
    {
        key: 'cool',
        label: 'Cool',
        hint: 'Slate-tinted',
        bg: 'oklch(0.97 0.0028 240)',
        fg: 'oklch(0.18 0.0044 240)',
    },
    {
        key: 'true',
        label: 'True',
        hint: 'Pure neutral',
        bg: 'oklch(0.97 0 0)',
        fg: 'oklch(0.18 0 0)',
    },
];

function fontStackPreview(family: string): string {
    const clean = family.trim();
    if (!clean || clean.toLowerCase() === 'system') {
        return 'system-ui, -apple-system, sans-serif';
    }
    return `'${clean}', system-ui, sans-serif`;
}

const RADIUS_OPTIONS: Array<{ key: ThemeKnobs['radius']; label: string; rem: number }> = [
    { key: 'tight', label: 'Tight', rem: 0.25 },
    { key: 'default', label: 'Default', rem: 0.5 },
    { key: 'rounded', label: 'Rounded', rem: 0.75 },
];

const BUTTON_SHAPE_OPTIONS: Array<{ key: ThemeKnobs['button_shape']; label: string; radius: string }> = [
    { key: 'pill', label: 'Pill', radius: '9999px' },
    { key: 'rounded', label: 'Rounded', radius: '0.5rem' },
    { key: 'square', label: 'Square', radius: '0' },
];

const DENSITY_OPTIONS: Array<{ key: ThemeKnobs['density']; label: string; gap: string }> = [
    { key: 'compact', label: 'Compact', gap: '4px' },
    { key: 'default', label: 'Default', gap: '8px' },
    { key: 'comfortable', label: 'Comfortable', gap: '14px' },
];

const ELEVATION_OPTIONS: Array<{ key: ThemeKnobs['elevation']; label: string; shadow: string }> = [
    { key: 'flat', label: 'Flat', shadow: '0 1px 2px rgb(0 0 0 / 0.06)' },
    { key: 'layered', label: 'Layered', shadow: '0 4px 8px rgb(0 0 0 / 0.10)' },
    { key: 'dramatic', label: 'Dramatic', shadow: '0 14px 28px rgb(0 0 0 / 0.20)' },
];

const MODE_OPTIONS: Array<{ key: ThemeKnobs['mode_pref']; label: string }> = [
    { key: 'light', label: 'Light' },
    { key: 'dark', label: 'Dark' },
    { key: 'system', label: 'System (auto)' },
];

// Design-system skins re-style the public marketing site only (paired with
// data-chrome='marketing'); the value rides the knobs JSON but never enters
// derive(). `default` = today's knob-driven styling everywhere.
const SKIN_OPTIONS: Array<{
    key: string;
    label: string;
    hint: string;
    swatch: ReactNode;
}> = [
    {
        key: 'default',
        label: 'Default',
        hint: 'Knob-driven styling on every surface',
        swatch: (
            <span className="h-8 w-full rounded-sm border border-border/60 bg-muted" />
        ),
    },
    {
        key: 'geometry',
        label: 'Geometry',
        hint: 'Colorful warm flat — white canvas, ink CTAs, yellow accents, bold geometric type. Marketing pages only.',
        swatch: (
            <span className="flex h-8 w-full items-center justify-center gap-1.5 rounded-sm border border-border/60 bg-white">
                {['#FFD86F', '#C9FF85', '#7CE1FF', '#9886FE'].map((c) => (
                    <span
                        key={c}
                        className="h-2.5 w-2.5 rounded-full"
                        style={{ background: c }}
                    />
                ))}
            </span>
        ),
    },
];

const WEIGHT_OPTIONS_BODY: Array<{ value: ThemeKnobs['weight_body']; label: string }> = [
    { value: 400, label: '400 Regular' },
    { value: 500, label: '500 Medium' },
];

const WEIGHT_OPTIONS_SUBHEADING: Array<{ value: ThemeKnobs['weight_subheading']; label: string }> = [
    { value: 500, label: '500 Medium' },
    { value: 600, label: '600 SemiBold' },
    { value: 700, label: '700 Bold' },
];

const WEIGHT_OPTIONS_HEADING: Array<{ value: ThemeKnobs['weight_heading']; label: string }> = [
    { value: 600, label: '600 SemiBold' },
    { value: 700, label: '700 Bold' },
    { value: 800, label: '800 ExtraBold' },
];

const WEIGHT_OPTIONS_DISPLAY: Array<{ value: ThemeKnobs['weight_display']; label: string }> = [
    { value: 700, label: '700 Bold' },
    { value: 800, label: '800 ExtraBold' },
    { value: 900, label: '900 Black' },
];

function Section({ title, children }: { title: string; children: ReactNode }) {
    return (
        <div className="flex flex-col gap-3 border-b border-border px-4 py-4 last:border-b-0">
            <h3 className="text-[11px] font-medium text-muted-foreground">{title}</h3>
            {children}
        </div>
    );
}

function Row({
    label,
    htmlFor,
    hint,
    children,
}: {
    label: string;
    htmlFor?: string;
    hint?: string;
    children: ReactNode;
}) {
    return (
        <div className="flex flex-col gap-1.5">
            <label
                htmlFor={htmlFor}
                className="text-[11px] font-medium text-foreground/80"
            >
                {label}
            </label>
            {hint && (
                <p className="text-[10.5px] leading-snug text-muted-foreground">
                    {hint}
                </p>
            )}
            {children}
        </div>
    );
}

function clamp(n: number, lo: number, hi: number): number {
    return Math.max(lo, Math.min(hi, n));
}

function oklchToHex(l: number, c: number, h: number): string {
    // Approximate OKLCH → sRGB hex via OKLab → linear sRGB → gamma. Rough but good
    // enough for a cosmetic readout next to the picker; canonical store stays OKLCH.
    const hr = (h * Math.PI) / 180;
    const a = c * Math.cos(hr);
    const b = c * Math.sin(hr);

    const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
    const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
    const s_ = l - 0.0894841775 * a - 1.291485548 * b;

    const L = l_ * l_ * l_;
    const M = m_ * m_ * m_;
    const S = s_ * s_ * s_;

    let r = 4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S;
    let g = -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S;
    let bl = -0.0041960863 * L - 0.7034186147 * M + 1.707614701 * S;

    const toSRGB = (v: number) => {
        const x = clamp(v, 0, 1);
        return x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
    };
    r = toSRGB(r);
    g = toSRGB(g);
    bl = toSRGB(bl);

    const toHex = (v: number) =>
        Math.round(clamp(v, 0, 1) * 255)
            .toString(16)
            .padStart(2, '0');

    return `#${toHex(r)}${toHex(g)}${toHex(bl)}`.toUpperCase();
}

function hexToOklch(hex: string): { l: number; c: number; h: number } | null {
    const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
    if (!m) return null;
    const v = m[1];
    const r8 = parseInt(v.slice(0, 2), 16) / 255;
    const g8 = parseInt(v.slice(2, 4), 16) / 255;
    const b8 = parseInt(v.slice(4, 6), 16) / 255;

    const fromSRGB = (x: number) =>
        x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
    const r = fromSRGB(r8);
    const g = fromSRGB(g8);
    const b = fromSRGB(b8);

    const L = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
    const M = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
    const S = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;

    const l_ = Math.cbrt(L);
    const m_ = Math.cbrt(M);
    const s_ = Math.cbrt(S);

    const l = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
    const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
    const bl = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;

    const c = Math.sqrt(a * a + bl * bl);
    let h = (Math.atan2(bl, a) * 180) / Math.PI;
    if (h < 0) h += 360;

    return { l: clamp(l, 0, 1), c: clamp(c, 0, 0.4), h: clamp(h, 0, 360) };
}

function OklchPicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['accent'];
    onChange: (v: ThemeKnobs['accent']) => void;
}) {
    const swatch = `oklch(${value.l.toFixed(3)} ${value.c.toFixed(3)} ${value.h.toFixed(1)})`;
    const hex = useMemo(() => oklchToHex(value.l, value.c, value.h), [value]);

    return (
        <div className="flex flex-col gap-2.5">
            <div className="flex items-center gap-3">
                {/* Native color picker — clicking the swatch opens the OS
                    color wheel. The <input type="color"> sits stretched
                    behind the swatch (opacity 0, full-cover) so the swatch
                    itself acts as the trigger. RGB-only, so we round-trip
                    through hex → OKLCH on change; sliders stay the source
                    of truth for chroma above sRGB gamut. */}
                <label
                    className="relative h-10 w-10 shrink-0 cursor-pointer rounded-md border border-border transition-transform hover:scale-105"
                    style={{ background: swatch }}
                    aria-label="Pick accent color"
                    data-testid="knobs-accent-swatch"
                >
                    <input
                        type="color"
                        value={hex}
                        onChange={(e) => {
                            const next = hexToOklch(e.target.value);
                            if (next) onChange(next);
                        }}
                        data-testid="knobs-accent-color-input"
                        className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
                    />
                </label>
                <div className="flex-1 text-[11px] font-mono text-muted-foreground">
                    {swatch}
                </div>
            </div>
            <div className="flex flex-col gap-1.5">
                <label className="flex items-center justify-between text-[11px] font-medium text-foreground/80">
                    <span>Lightness</span>
                    <span className="font-mono text-muted-foreground">{value.l.toFixed(3)}</span>
                </label>
                <input
                    type="range"
                    min={0}
                    max={1}
                    step={0.001}
                    value={value.l}
                    onChange={(e) => onChange({ ...value, l: Number(e.target.value) })}
                    data-testid="knobs-accent-l-slider"
                    className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-accent"
                />
            </div>
            <div className="flex flex-col gap-1.5">
                <label className="flex items-center justify-between text-[11px] font-medium text-foreground/80">
                    <span>Chroma</span>
                    <span className="font-mono text-muted-foreground">{value.c.toFixed(3)}</span>
                </label>
                <input
                    type="range"
                    min={0}
                    max={0.4}
                    step={0.001}
                    value={value.c}
                    onChange={(e) => onChange({ ...value, c: Number(e.target.value) })}
                    data-testid="knobs-accent-c-slider"
                    className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-accent"
                />
            </div>
            <div className="flex flex-col gap-1.5">
                <label className="flex items-center justify-between text-[11px] font-medium text-foreground/80">
                    <span>Hue</span>
                    <span className="font-mono text-muted-foreground">{value.h.toFixed(1)}°</span>
                </label>
                <input
                    type="range"
                    min={0}
                    max={360}
                    step={0.5}
                    value={value.h}
                    onChange={(e) => onChange({ ...value, h: Number(e.target.value) })}
                    data-testid="knobs-accent-h-slider"
                    className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-accent"
                />
            </div>
            <div className="flex items-center gap-2">
                <span className="text-[11px] font-medium text-foreground/80">Hex</span>
                <input
                    type="text"
                    value={hex}
                    spellCheck={false}
                    onChange={(e) => {
                        const next = hexToOklch(e.target.value);
                        if (next) onChange(next);
                    }}
                    data-testid="knobs-accent-hex-input"
                    className="flex-1 rounded-md border border-border bg-card px-2 py-1 text-[11px] font-mono text-foreground shadow-sm focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
                />
            </div>
        </div>
    );
}

function AccentScalePreview({ knobs }: { knobs: ThemeKnobs }) {
    const derived = useMemo(() => derive(knobs), [knobs]);
    return (
        <div className="flex flex-col gap-1.5">
            <span className="text-[11px] font-medium text-foreground/80">Derived scale</span>
            <div className="flex h-6 w-full overflow-hidden rounded-md border border-border">
                {SCALE_STEPS.map((step) => (
                    <div
                        key={step}
                        className="flex-1"
                        style={{ background: derived.css_vars_light[`--color-accent-${step}`] }}
                        title={`accent-${step}`}
                    />
                ))}
            </div>
        </div>
    );
}

function NeutralTempPicker({
    knobs,
    onChange,
}: {
    knobs: ThemeKnobs;
    onChange: (next: Partial<ThemeKnobs>) => void;
}) {
    const value = knobs.neutral_temp;
    const customHex = knobs.neutral_custom_hex || '#f5f0e6';

    return (
        <div className="flex flex-col gap-2">
            <div className="grid grid-cols-4 gap-2">
                {NEUTRAL_TEMPS.map((opt) => {
                    const selected = value === opt.key;
                    return (
                        <button
                            key={opt.key}
                            type="button"
                            onClick={() => onChange({ neutral_temp: opt.key })}
                            data-testid={`knobs-neutral-temp-${opt.key}`}
                            aria-pressed={selected}
                            className={cn(
                                'flex flex-col items-stretch gap-1.5 rounded-md border p-1.5 transition-colors',
                                selected
                                    ? 'border-accent ring-2 ring-accent/30'
                                    : 'border-border hover:border-accent/40',
                            )}
                        >
                            <span
                                className="flex h-12 w-full flex-col items-start justify-end rounded-sm border border-border/40 px-1.5 py-1"
                                style={{ background: opt.bg, color: opt.fg }}
                            >
                                <span className="text-[14px] font-semibold leading-none">
                                    Aa
                                </span>
                                <span className="mt-0.5 text-[8px] opacity-60">
                                    Body
                                </span>
                            </span>
                            <span className="flex flex-col items-center">
                                <span className="text-[11px] font-medium text-foreground">
                                    {opt.label}
                                </span>
                                <span className="text-[9px] text-muted-foreground">
                                    {opt.hint}
                                </span>
                            </span>
                        </button>
                    );
                })}
                <CustomNeutralTile
                    selected={value === 'custom'}
                    customHex={customHex}
                    onSelect={() => onChange({ neutral_temp: 'custom' })}
                />
            </div>
            {value === 'custom' && (
                <CustomNeutralPicker
                    hex={customHex}
                    onChange={(hex) => onChange({ neutral_custom_hex: hex })}
                />
            )}
        </div>
    );
}

function CustomNeutralTile({
    selected,
    customHex,
    onSelect,
}: {
    selected: boolean;
    customHex: string;
    onSelect: () => void;
}) {
    // The custom tile mirrors the preset tile shape: a bg/fg preview swatch
    // with an "Aa / Body" sample. The bg uses the picked hex directly; fg is
    // resolved client-side via ContrastChecker's contrast-derived value on
    // publish — for the swatch we just pick black or white based on the
    // hex's perceived lightness so the preview reads cleanly.
    const fg = perceptualFg(customHex);
    return (
        <button
            type="button"
            onClick={onSelect}
            data-testid="knobs-neutral-temp-custom"
            aria-pressed={selected}
            className={cn(
                'flex flex-col items-stretch gap-1.5 rounded-md border p-1.5 transition-colors',
                selected
                    ? 'border-accent ring-2 ring-accent/30'
                    : 'border-border hover:border-accent/40',
            )}
        >
            <span
                className="flex h-12 w-full flex-col items-start justify-end rounded-sm border border-border/40 px-1.5 py-1"
                style={{ background: customHex, color: fg }}
            >
                <span className="text-[14px] font-semibold leading-none">
                    Aa
                </span>
                <span className="mt-0.5 text-[8px] opacity-60">Body</span>
            </span>
            <span className="flex flex-col items-center">
                <span className="text-[11px] font-medium text-foreground">
                    Custom
                </span>
                <span className="text-[9px] text-muted-foreground">
                    Any color
                </span>
            </span>
        </button>
    );
}

function CustomNeutralPicker({
    hex,
    onChange,
}: {
    hex: string;
    onChange: (hex: string) => void;
}) {
    // Live contrast preview against the foreground the theme will derive for
    // this background (buildScale step 900 — approximated here as a near-
    // black with the same hue). When grade falls below AA, we warn inline
    // so admin sees the readability cost of their pick before publish.
    const fgPreview = perceptualFg(hex);
    const contrast = contrastRatio(hex, fgPreview);
    const grade = contrastGrade(contrast);

    return (
        <div className="flex flex-col gap-2 rounded-md border border-border bg-muted/30 p-2">
            <div className="flex items-center gap-2">
                <input
                    type="color"
                    value={normalizeHex(hex)}
                    onChange={(e) => onChange(e.target.value)}
                    className="h-9 w-12 cursor-pointer rounded border border-border bg-card p-0.5"
                    data-testid="knobs-neutral-custom-color"
                    aria-label="Background color picker"
                />
                <input
                    type="text"
                    value={hex}
                    onChange={(e) => onChange(e.target.value)}
                    placeholder="#f5f0e6"
                    className="h-9 flex-1 rounded border border-border bg-card px-2 font-mono text-[12px] focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
                    data-testid="knobs-neutral-custom-hex"
                    aria-label="Background color hex"
                />
            </div>
            <div className="flex items-center justify-between gap-2 text-[10.5px]">
                <span className="text-muted-foreground">
                    Contrast vs derived foreground:{' '}
                    <span className="font-mono text-foreground/80">
                        {contrast.toFixed(2)}:1
                    </span>
                </span>
                <span
                    className={cn(
                        'rounded-full border px-1.5 py-0.5 font-medium',
                        grade === 'fail'
                            ? 'border-destructive/40 bg-destructive/10 text-destructive'
                            : grade === 'AA-large'
                              ? 'border-warning/40 bg-warning/10 text-warning'
                              : 'border-success/40 bg-success/10 text-success',
                    )}
                    title={GRADE_TOOLTIPS[grade]}
                >
                    {GRADE_LABELS[grade]}
                </span>
            </div>
            {grade === 'fail' && (
                <p className="text-[10.5px] text-destructive">
                    Below WCAG AA — body text on this background will be hard
                    to read. Try a lighter or more desaturated hue.
                </p>
            )}
        </div>
    );
}

const GRADE_LABELS: Record<ReturnType<typeof contrastGrade>, string> = {
    AAA: 'AAA',
    AA: 'AA',
    'AA-large': 'AA Large',
    fail: 'Fail',
};

const GRADE_TOOLTIPS: Record<ReturnType<typeof contrastGrade>, string> = {
    AAA: 'WCAG AAA — passes the strictest readability bar',
    AA: 'WCAG AA — body + headings',
    'AA-large': 'AA for large text only (≥ 18px / 14px bold)',
    fail: 'Below AA — readability risk',
};

// Lightweight contrast helpers — inline so the picker doesn't have to
// import from themePalette (avoids a cycle). The WCAG ratio uses the
// standard sRGB luminance formula; perceptualFg picks black or white
// based on relative lightness so the swatch preview always has a
// readable label even on saturated picks.

function normalizeHex(input: string): string {
    const m = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i.exec(input.trim());
    if (!m) return '#f5f0e6';
    return '#' + m[1].toLowerCase().padStart(6, m[1]);
}

function hexToRgb(hex: string): [number, number, number] {
    const m = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i.exec(hex.trim());
    if (!m) return [245, 240, 230];
    let raw = m[1];
    if (raw.length === 3)
        raw = raw[0] + raw[0] + raw[1] + raw[1] + raw[2] + raw[2];
    return [
        parseInt(raw.slice(0, 2), 16),
        parseInt(raw.slice(2, 4), 16),
        parseInt(raw.slice(4, 6), 16),
    ];
}

function relativeLuminance([r, g, b]: [number, number, number]): number {
    const channel = (c: number): number => {
        const s = c / 255;
        return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
    };
    return (
        0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
    );
}

function contrastRatio(hexA: string, hexB: string): number {
    const la = relativeLuminance(hexToRgb(hexA));
    const lb = relativeLuminance(hexToRgb(hexB));
    const [lo, hi] = la < lb ? [la, lb] : [lb, la];
    return (hi + 0.05) / (lo + 0.05);
}

function contrastGrade(r: number): 'AAA' | 'AA' | 'AA-large' | 'fail' {
    if (r >= 7) return 'AAA';
    if (r >= 4.5) return 'AA';
    if (r >= 3) return 'AA-large';
    return 'fail';
}

function perceptualFg(hex: string): string {
    return relativeLuminance(hexToRgb(hex)) > 0.5 ? '#1c1917' : '#fafaf9';
}
function RadiusPicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['radius'];
    onChange: (v: ThemeKnobs['radius']) => void;
}) {
    return (
        <div className="grid grid-cols-3 gap-2">
            {RADIUS_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-radius-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'flex flex-col items-center gap-1.5 border p-2 transition-colors',
                            selected
                                ? 'border-accent ring-2 ring-accent/30'
                                : 'border-border hover:border-accent/40',
                        )}
                        style={{ borderRadius: `${opt.rem}rem` }}
                    >
                        <span
                            className="h-7 w-full bg-muted"
                            style={{ borderRadius: `${opt.rem}rem` }}
                        />
                        <span className="text-[11px] font-medium text-foreground">{opt.label}</span>
                    </button>
                );
            })}
        </div>
    );
}

function ButtonShapePicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['button_shape'];
    onChange: (v: ThemeKnobs['button_shape']) => void;
}) {
    return (
        <div className="grid grid-cols-3 gap-2">
            {BUTTON_SHAPE_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-button-shape-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'flex flex-col items-center gap-1.5 rounded-md border p-2 transition-colors',
                            selected
                                ? 'border-accent ring-2 ring-accent/30'
                                : 'border-border hover:border-accent/40',
                        )}
                    >
                        <span
                            className="flex h-7 w-full items-center justify-center bg-accent text-[10px] font-medium text-accent-foreground"
                            style={{ borderRadius: opt.radius }}
                        >
                            Button
                        </span>
                        <span className="text-[11px] font-medium text-foreground">{opt.label}</span>
                    </button>
                );
            })}
        </div>
    );
}

function DensityPicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['density'];
    onChange: (v: ThemeKnobs['density']) => void;
}) {
    return (
        <div className="grid grid-cols-3 gap-2">
            {DENSITY_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-density-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'flex flex-col items-center gap-1.5 rounded-md border p-2 transition-colors',
                            selected
                                ? 'border-accent ring-2 ring-accent/30'
                                : 'border-border hover:border-accent/40',
                        )}
                    >
                        <span
                            className="flex h-7 w-full items-center justify-center"
                            style={{ gap: opt.gap }}
                        >
                            <span className="h-3 w-3 rounded-sm bg-muted" />
                            <span className="h-3 w-3 rounded-sm bg-muted" />
                            <span className="h-3 w-3 rounded-sm bg-muted" />
                        </span>
                        <span className="text-[11px] font-medium text-foreground">{opt.label}</span>
                    </button>
                );
            })}
        </div>
    );
}

function ElevationPicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['elevation'];
    onChange: (v: ThemeKnobs['elevation']) => void;
}) {
    return (
        <div className="grid grid-cols-3 gap-2">
            {ELEVATION_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-elevation-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'flex flex-col items-center gap-1.5 rounded-md border p-2 transition-colors',
                            selected
                                ? 'border-accent ring-2 ring-accent/30'
                                : 'border-border hover:border-accent/40',
                        )}
                    >
                        <span
                            className="h-7 w-full rounded-sm bg-card"
                            style={{ boxShadow: opt.shadow }}
                        />
                        <span className="text-[11px] font-medium text-foreground">{opt.label}</span>
                    </button>
                );
            })}
        </div>
    );
}

function ModePicker({
    value,
    onChange,
}: {
    value: ThemeKnobs['mode_pref'];
    onChange: (v: ThemeKnobs['mode_pref']) => void;
}) {
    return (
        <div className="grid grid-cols-3 gap-2">
            {MODE_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-mode-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'rounded-md border px-2 py-1.5 text-[11px] font-medium transition-colors',
                            selected
                                ? 'border-accent bg-accent/10 text-accent'
                                : 'border-border text-foreground hover:border-accent/40',
                        )}
                    >
                        {opt.label}
                    </button>
                );
            })}
        </div>
    );
}

function SkinPicker({
    value,
    onChange,
}: {
    value: string;
    onChange: (v: string) => void;
}) {
    return (
        <div className="grid grid-cols-2 gap-2">
            {SKIN_OPTIONS.map((opt) => {
                const selected = value === opt.key;
                return (
                    <button
                        key={opt.key}
                        type="button"
                        onClick={() => onChange(opt.key)}
                        data-testid={`knobs-skin-${opt.key}`}
                        aria-pressed={selected}
                        className={cn(
                            'flex flex-col items-stretch gap-1.5 rounded-md border p-1.5 text-left transition-colors',
                            selected
                                ? 'border-accent ring-2 ring-accent/30'
                                : 'border-border hover:border-accent/40',
                        )}
                    >
                        {opt.swatch}
                        <span className="flex flex-col gap-0.5 px-0.5">
                            <span className="text-[11px] font-medium text-foreground">
                                {opt.label}
                            </span>
                            <span className="text-[9.5px] leading-snug text-muted-foreground">
                                {opt.hint}
                            </span>
                        </span>
                    </button>
                );
            })}
        </div>
    );
}

function SubHeading({ children }: { children: ReactNode }) {
    return (
        <h4 className="mt-1 text-[11px] font-medium text-muted-foreground">{children}</h4>
    );
}

function WeightPicker<V extends number>({
    value,
    options,
    onChange,
    fontFamily,
    testIdPrefix,
}: {
    value: V;
    options: Array<{ value: V; label: string }>;
    onChange: (v: V) => void;
    fontFamily: string;
    testIdPrefix: string;
}) {
    return (
        <div
            className={cn(
                'grid gap-2',
                options.length === 2 ? 'grid-cols-2' : 'grid-cols-3',
            )}
        >
            {options.map((opt) => {
                const selected = value === opt.value;
                return (
                    <button
                        key={opt.value}
                        type="button"
                        onClick={() => onChange(opt.value)}
                        data-testid={`${testIdPrefix}-${opt.value}`}
                        aria-pressed={selected}
                        className={cn(
                            'rounded-md border px-2 py-1.5 text-[11px] transition-colors',
                            selected
                                ? 'border-accent bg-accent/10 text-accent'
                                : 'border-border text-foreground hover:border-accent/40',
                        )}
                        style={{
                            fontFamily: fontStackPreview(fontFamily),
                            fontWeight: opt.value,
                        }}
                    >
                        {opt.label}
                    </button>
                );
            })}
        </div>
    );
}

function NumberField({
    value,
    onChange,
    min,
    max,
    step,
    suffix,
    testId,
    ariaLabel,
}: {
    value: number;
    onChange: (v: number) => void;
    min: number;
    max: number;
    step: number;
    suffix?: string;
    testId: string;
    ariaLabel: string;
}) {
    return (
        <label className="flex flex-1 items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-[11px] text-foreground shadow-sm focus-within:border-accent focus-within:ring-2 focus-within:ring-accent/30">
            <input
                type="number"
                value={value}
                min={min}
                max={max}
                step={step}
                aria-label={ariaLabel}
                onChange={(e) => {
                    const next = Number(e.target.value);
                    if (Number.isFinite(next)) {
                        onChange(clamp(next, min, max));
                    }
                }}
                data-testid={testId}
                className="w-full bg-transparent font-mono text-[11px] text-foreground focus:outline-none"
            />
            {suffix ? (
                <span className="shrink-0 text-[10px] font-medium text-muted-foreground">
                    {suffix}
                </span>
            ) : null}
        </label>
    );
}

function ScaleHeadingRow({
    role,
    label,
    knobs,
    onSize,
    onLine,
    onTracking,
    fontFamily,
    fontWeight,
    sample,
}: {
    role: 'h1' | 'h2' | 'h3';
    label: string;
    knobs: ThemeKnobs;
    onSize: (v: number) => void;
    onLine: (v: number) => void;
    onTracking: (v: number) => void;
    fontFamily: string;
    fontWeight: number;
    sample: string;
}) {
    const scale = knobs[`scale_${role}`];
    return (
        <div className="flex flex-col gap-1.5">
            <div className="flex items-center justify-between">
                <span className="text-[11px] font-medium text-foreground/80">{label}</span>
                <span className="text-[10px] text-muted-foreground">Size · Line · Tracking</span>
            </div>
            <div className="flex items-center gap-1.5">
                <NumberField
                    value={scale.size}
                    onChange={onSize}
                    min={12}
                    max={120}
                    step={1}
                    suffix="px"
                    testId={`knobs-scale-${role}-size`}
                    ariaLabel={`${label} size`}
                />
                <NumberField
                    value={scale.line}
                    onChange={onLine}
                    min={0.9}
                    max={2.0}
                    step={0.05}
                    testId={`knobs-scale-${role}-line`}
                    ariaLabel={`${label} line height`}
                />
                <NumberField
                    value={scale.tracking}
                    onChange={onTracking}
                    min={-0.1}
                    max={0.1}
                    step={0.005}
                    suffix="em"
                    testId={`knobs-scale-${role}-tracking`}
                    ariaLabel={`${label} tracking`}
                />
            </div>
            <div className="mt-1 max-h-16 overflow-hidden">
                <p
                    aria-hidden
                    style={{
                        fontFamily: fontStackPreview(fontFamily),
                        fontWeight,
                        fontSize: `${scale.size}px`,
                        lineHeight: scale.line,
                        letterSpacing: `${scale.tracking}em`,
                    }}
                    className="truncate text-foreground"
                >
                    {sample}
                </p>
            </div>
        </div>
    );
}

function ScaleBodyRow({
    knobs,
    onSize,
    onLine,
    fontFamily,
    fontWeight,
}: {
    knobs: ThemeKnobs;
    onSize: (v: number) => void;
    onLine: (v: number) => void;
    fontFamily: string;
    fontWeight: number;
}) {
    const scale = knobs.scale_body;
    return (
        <div className="flex flex-col gap-1.5">
            <div className="flex items-center justify-between">
                <span className="text-[11px] font-medium text-foreground/80">Body</span>
                <span className="text-[10px] text-muted-foreground">Size · Line</span>
            </div>
            <div className="flex items-center gap-1.5">
                <NumberField
                    value={scale.size}
                    onChange={onSize}
                    min={12}
                    max={24}
                    step={1}
                    suffix="px"
                    testId="knobs-scale-body-size"
                    ariaLabel="Body size"
                />
                <NumberField
                    value={scale.line}
                    onChange={onLine}
                    min={1.0}
                    max={2.0}
                    step={0.05}
                    testId="knobs-scale-body-line"
                    ariaLabel="Body line height"
                />
            </div>
            <div className="mt-1 max-h-16 overflow-hidden">
                <p
                    aria-hidden
                    style={{
                        fontFamily: fontStackPreview(fontFamily),
                        fontWeight,
                        fontSize: `${scale.size}px`,
                        lineHeight: scale.line,
                    }}
                    className="truncate text-foreground"
                >
                    The quick brown fox jumps over the lazy dog.
                </p>
            </div>
        </div>
    );
}

const KNOBS_COLLAPSED_KEY = 'appshot:admin-appearance-knobs-collapsed';

export default function KnobsPanel({ knobs, onChange, onReset, onOpenPresets }: Props) {
    const set = <K extends keyof ThemeKnobs>(field: K, value: ThemeKnobs[K]) =>
        onChange({ ...knobs, [field]: value });

    const skin = knobs.skin ?? 'default';
    // A non-default design system pins the marketing fonts + type scale, so the
    // Font/Type-scale knobs only affect app surfaces while a skin is active.
    // Soft-dim (not disable) those sections + show an inline note.
    const skinPinsFonts = skin !== 'default';

    const setScale = <K extends 'h1' | 'h2' | 'h3' | 'body'>(
        role: K,
        field: keyof ThemeKnobs[`scale_${K}`],
        value: number,
    ) => {
        const key = `scale_${role}` as const;
        onChange({ ...knobs, [key]: { ...knobs[key], [field]: value } });
    };

    // Collapse state persisted to localStorage so admins who fold the
    // panel keep it folded across page refreshes / Inertia visits.
    const [collapsed, setCollapsed] = useState(() => {
        if (typeof window === 'undefined') return false;
        try {
            return window.localStorage.getItem(KNOBS_COLLAPSED_KEY) === '1';
        } catch {
            return false;
        }
    });
    const toggleCollapsed = () => {
        setCollapsed((prev) => {
            const next = !prev;
            try {
                window.localStorage.setItem(KNOBS_COLLAPSED_KEY, next ? '1' : '0');
            } catch {
                /* storage unavailable — silent fail keeps the toggle interactive */
            }
            return next;
        });
    };

    return (
        <div
            data-testid="appearance-knobs-panel"
            className={`flex w-full flex-col overflow-hidden rounded-lg border border-border bg-card ${
                collapsed ? '' : 'h-full'
            }`}
        >
            <button
                type="button"
                onClick={toggleCollapsed}
                aria-expanded={!collapsed}
                data-testid="appearance-knobs-collapse-toggle"
                className={`flex items-center justify-between px-4 py-3 text-left transition-colors hover:bg-muted/30 ${
                    collapsed ? '' : 'border-b border-border'
                }`}
            >
                <span className="flex items-center gap-1.5">
                    <ChevronDown
                        className={`h-3 w-3 text-muted-foreground transition-transform ${
                            collapsed ? '-rotate-90' : 'rotate-0'
                        }`}
                        strokeWidth={2.5}
                        aria-hidden
                    />
                    <h2 className="text-[13px] font-semibold tracking-tight text-foreground">
                        Theme knobs
                    </h2>
                </span>
                <span className="text-[11px] font-medium text-muted-foreground">10 controls</span>
            </button>
            {!collapsed && (
            <>
            <div className="flex shrink-0 items-center justify-between gap-2 border-b border-border bg-muted/30 px-3 py-1.5">
                <Button
                    variant="secondary"
                    size="sm"
                    onClick={onOpenPresets}
                    data-testid="knobs-open-presets"
                    className="h-7 gap-1 px-2 text-[11px]"
                >
                    <LayoutGrid className="h-3 w-3" strokeWidth={2} />
                    Browse presets
                </Button>
                <Button
                    variant="ghost"
                    size="sm"
                    onClick={onReset}
                    data-testid="knobs-reset"
                    className="h-7 gap-1 px-2 text-[11px]"
                >
                    <RotateCcw className="h-3 w-3" strokeWidth={2} />
                    Reset to default
                </Button>
            </div>
            <div className="flex-1 overflow-y-auto">
                <Section title="Brand identity">
                    <Row label="Accent">
                        <OklchPicker value={knobs.accent} onChange={(v) => set('accent', v)} />
                    </Row>
                    <AccentScalePreview knobs={knobs} />
                    <Row
                        label="Background tone"
                        hint="Tints the background + foreground family for the whole app. Pick the option whose Aa preview reads most natural to you."
                    >
                        <NeutralTempPicker
                            knobs={knobs}
                            onChange={(patch) =>
                                onChange({ ...knobs, ...patch })
                            }
                        />
                    </Row>
                </Section>

                <Section title="Design system">
                    <SkinPicker value={skin} onChange={(v) => set('skin', v)} />
                    <p className="text-[10.5px] leading-snug text-muted-foreground">
                        Design systems re-style the public marketing site only.
                        Dashboard, editor and admin always follow the knobs
                        below.
                    </p>
                </Section>

                <Section title="Typography">
                    <Row label="Body font">
                        <FontFamilyPicker
                            value={knobs.font_sans}
                            onCommit={(v) => set('font_sans', v)}
                            testIdPrefix="knobs-font-sans"
                        />
                    </Row>
                    <div
                        className={cn(
                            'flex flex-col gap-3 transition-opacity',
                            skinPinsFonts && 'opacity-60',
                        )}
                        data-testid="knobs-heading-fonts-group"
                    >
                        {skinPinsFonts && (
                            <p
                                className="text-[10.5px] leading-snug text-muted-foreground"
                                data-testid="knobs-skin-fonts-note"
                            >
                                The selected design system pins marketing fonts
                                &amp; type scale. These settings still apply to
                                app surfaces.
                            </p>
                        )}
                        <Row label="Heading font">
                            <FontFamilyPicker
                                value={knobs.font_display}
                                onCommit={(v) => set('font_display', v)}
                                testIdPrefix="knobs-font-display"
                            />
                        </Row>
                        <Row label="Subheading font">
                            <FontFamilyPicker
                                value={knobs.font_subheading ?? knobs.font_display}
                                onCommit={(v) => set('font_subheading', v)}
                                testIdPrefix="knobs-font-subheading"
                            />
                        </Row>
                    </div>

                    <SubHeading>Weights</SubHeading>
                    <Row label="Body">
                        <WeightPicker
                            value={knobs.weight_body}
                            options={WEIGHT_OPTIONS_BODY}
                            onChange={(v) => set('weight_body', v)}
                            fontFamily={knobs.font_sans}
                            testIdPrefix="knobs-weight-body"
                        />
                    </Row>
                    <Row label="Subheading">
                        <WeightPicker
                            value={knobs.weight_subheading}
                            options={WEIGHT_OPTIONS_SUBHEADING}
                            onChange={(v) => set('weight_subheading', v)}
                            fontFamily={knobs.font_display}
                            testIdPrefix="knobs-weight-subheading"
                        />
                    </Row>
                    <Row label="Heading">
                        <WeightPicker
                            value={knobs.weight_heading}
                            options={WEIGHT_OPTIONS_HEADING}
                            onChange={(v) => set('weight_heading', v)}
                            fontFamily={knobs.font_display}
                            testIdPrefix="knobs-weight-heading"
                        />
                    </Row>
                    <Row label="Display">
                        <WeightPicker
                            value={knobs.weight_display}
                            options={WEIGHT_OPTIONS_DISPLAY}
                            onChange={(v) => set('weight_display', v)}
                            fontFamily={knobs.font_display}
                            testIdPrefix="knobs-weight-display"
                        />
                    </Row>

                    <div
                        className={cn(
                            'flex flex-col gap-3 transition-opacity',
                            skinPinsFonts && 'opacity-60',
                        )}
                        data-testid="knobs-type-scale-group"
                    >
                        <SubHeading>Type scale</SubHeading>
                        <ScaleHeadingRow
                            role="h1"
                            label="H1"
                            knobs={knobs}
                            onSize={(v) => setScale('h1', 'size', v)}
                            onLine={(v) => setScale('h1', 'line', v)}
                            onTracking={(v) => setScale('h1', 'tracking', v)}
                            fontFamily={knobs.font_display}
                            fontWeight={knobs.weight_display}
                            sample="Sample heading"
                        />
                        <ScaleHeadingRow
                            role="h2"
                            label="H2"
                            knobs={knobs}
                            onSize={(v) => setScale('h2', 'size', v)}
                            onLine={(v) => setScale('h2', 'line', v)}
                            onTracking={(v) => setScale('h2', 'tracking', v)}
                            fontFamily={knobs.font_display}
                            fontWeight={knobs.weight_heading}
                            sample="Sample heading"
                        />
                        <ScaleHeadingRow
                            role="h3"
                            label="H3"
                            knobs={knobs}
                            onSize={(v) => setScale('h3', 'size', v)}
                            onLine={(v) => setScale('h3', 'line', v)}
                            onTracking={(v) => setScale('h3', 'tracking', v)}
                            fontFamily={knobs.font_display}
                            fontWeight={knobs.weight_subheading}
                            sample="Sample subheading"
                        />
                        <ScaleBodyRow
                            knobs={knobs}
                            onSize={(v) => setScale('body', 'size', v)}
                            onLine={(v) => setScale('body', 'line', v)}
                            fontFamily={knobs.font_sans}
                            fontWeight={knobs.weight_body}
                        />
                    </div>
                </Section>

                <Section title="Shape">
                    <Row label="Radius">
                        <RadiusPicker value={knobs.radius} onChange={(v) => set('radius', v)} />
                    </Row>
                    <Row label="Button shape">
                        <ButtonShapePicker
                            value={knobs.button_shape}
                            onChange={(v) => set('button_shape', v)}
                        />
                    </Row>
                </Section>

                <Section title="Density">
                    <Row label="Spacing scale">
                        <DensityPicker value={knobs.density} onChange={(v) => set('density', v)} />
                    </Row>
                </Section>

                <Section title="Elevation">
                    <Row label="Shadow stack">
                        <ElevationPicker
                            value={knobs.elevation}
                            onChange={(v) => set('elevation', v)}
                        />
                    </Row>
                </Section>

                <Section title="Mode tuning">
                    <Row label="Mode preference">
                        <ModePicker value={knobs.mode_pref} onChange={(v) => set('mode_pref', v)} />
                    </Row>
                    <label className="flex items-center gap-2 text-[11px] font-medium text-foreground/80">
                        <Checkbox
                            defaultChecked
                            data-testid="knobs-auto-tune-dark"
                        />
                        <span>Auto-tune dark mode</span>
                    </label>
                </Section>
            </div>
            </>
            )}
        </div>
    );
}
