export type TypeScaleTriple = { size: number; line: number; tracking: number };
export type TypeScaleBody = { size: number; line: number };

export type ThemeKnobs = {
    accent: { l: number; c: number; h: number };
    /** `'custom'` reads `neutral_custom_hex` for an arbitrary background tone.
     *  The presets keep the chroma low so the bg-fg pair stays readable;
     *  `'custom'` puts the responsibility on the admin (ContrastChecker
     *  flags WCAG fails next to the swatch). */
    neutral_temp: 'warm' | 'cool' | 'true' | 'custom';
    /** Hex used when `neutral_temp === 'custom'`. Converted to OKLCH at
     *  derive() time; only consulted for the custom path so the presets
     *  ignore it. */
    neutral_custom_hex: string;
    /** Free-form Google Fonts family name (e.g. 'Inter', 'Manrope').
     *  Sentinel 'system' (or empty) skips the Google Fonts <link> and uses the OS stack.
     *  `font_sans` = Body font, `font_display` = Heading font, `font_subheading` = Subheading font.
     *  Keys retain their internal names for backward compat with stored themes; UI labels
     *  use the buyer-friendly names. */
    font_sans: string;
    font_display: string;
    font_subheading: string;
    radius: 'tight' | 'default' | 'rounded';
    density: 'compact' | 'default' | 'comfortable';
    elevation: 'flat' | 'layered' | 'dramatic';
    button_shape: 'pill' | 'rounded' | 'square';
    mode_pref: 'light' | 'dark' | 'system';

    /** Marketing design-system selection. `'default'` = knob-driven styling on
     *  every surface (today's behavior). Non-default values name a scoped CSS
     *  skin that re-styles the PUBLIC MARKETING SITE ONLY (paired with
     *  `data-chrome='marketing'`); app surfaces always follow the knobs.
     *  DELIBERATELY NOT READ BY `derive()` — the PHP/TS derivers are byte-parity
     *  twins guarded by tests/Fixtures/theme-palette-cases.json. The skin rides
     *  the knobs JSON through store/draft/apply/export/import for free. */
    skin?: string;

    // Typography weights (numeric Google Fonts axis values).
    weight_body: 400 | 500;
    weight_subheading: 500 | 600 | 700;
    weight_heading: 600 | 700 | 800;
    weight_display: 700 | 800 | 900;

    // Type scale — h1/h2/h3 carry tracking, body does not.
    scale_h1: TypeScaleTriple;
    scale_h2: TypeScaleTriple;
    scale_h3: TypeScaleTriple;
    scale_body: TypeScaleBody;
};

export type DerivedTheme = {
    css_vars_light: Record<string, string>;
    css_vars_dark: Record<string, string>;
    fonts: { sans: string; display: string };
    meta: { token_count: number; version: number };
};

export const DEFAULT_KNOBS: ThemeKnobs = {
    accent: { l: 0.484, c: 0.235, h: 264 },
    neutral_temp: 'warm',
    neutral_custom_hex: '#f5f0e6',
    font_sans: 'Inter',
    font_display: 'Inter',
    font_subheading: 'Inter',
    radius: 'default',
    density: 'default',
    elevation: 'layered',
    button_shape: 'rounded',
    mode_pref: 'system',
    // `skin` is deliberately ABSENT here: DEFAULT_KNOBS is the deriver's
    // default input shape and the parity spec asserts it matches the
    // fixture's default-case knobs. The skin default ('default') lives in
    // Appearance.tsx's withKnobDefaults instead — UI-side only.
    weight_body: 400,
    weight_subheading: 600,
    weight_heading: 700,
    weight_display: 800,
    scale_h1: { size: 48, line: 1.1, tracking: -0.02 },
    scale_h2: { size: 36, line: 1.15, tracking: -0.015 },
    scale_h3: { size: 24, line: 1.25, tracking: 0 },
    scale_body: { size: 16, line: 1.5 },
};

const LIGHTNESS: Record<number, number | null> = {
    50: 0.97,
    100: 0.93,
    200: 0.86,
    300: 0.77,
    400: 0.65,
    500: null,
    600: 0.42,
    700: 0.34,
    800: 0.26,
    900: 0.18,
    950: 0.12,
};

const CHROMA_FALLOFF: Record<number, number> = {
    50: 0.35,
    100: 0.5,
    200: 0.7,
    300: 0.85,
    400: 0.95,
    500: 1.0,
    600: 0.95,
    700: 0.85,
    800: 0.7,
    900: 0.55,
    950: 0.4,
};

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

const NEUTRAL_PRESETS: Record<
    Exclude<ThemeKnobs['neutral_temp'], 'custom'>,
    { l: number; c: number; h: number }
> = {
    warm: { l: 0.5, c: 0.008, h: 75 },
    cool: { l: 0.5, c: 0.008, h: 240 },
    true: { l: 0.5, c: 0, h: 0 },
};

/**
 * Convert a hex string to an OKLCH base { l, c, h }. Returns null on
 * unparseable input so callers can fall back to a preset. Uses the
 * standard sRGB → linear → LMS → OKLab → OKLCH pipeline (D65, sRGB
 * primaries). Output matches what the browser computes for the same
 * hex when set via `color: #xxxxxx` and queried for the OKLCH form.
 */
function hexToOklch(
    hex: string,
): { l: number; c: number; h: number } | null {
    const m = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i.exec(hex.trim());
    if (!m) return null;
    let raw = m[1];
    if (raw.length === 3) {
        raw = raw[0] + raw[0] + raw[1] + raw[1] + raw[2] + raw[2];
    }
    const r8 = parseInt(raw.slice(0, 2), 16) / 255;
    const g8 = parseInt(raw.slice(2, 4), 16) / 255;
    const b8 = parseInt(raw.slice(4, 6), 16) / 255;

    // sRGB → linear sRGB
    const lin = (c: number): number =>
        c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    const r = lin(r8);
    const g = lin(g8);
    const b = lin(b8);

    // Linear sRGB → LMS (Björn Ottosson's matrix)
    const lLms = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
    const mLms = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
    const sLms = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;

    // LMS → OKLab (cube root)
    const lp = Math.cbrt(lLms);
    const mp = Math.cbrt(mLms);
    const sp = Math.cbrt(sLms);

    const L = 0.2104542553 * lp + 0.793617785 * mp - 0.0040720468 * sp;
    const a = 1.9779984951 * lp - 2.428592205 * mp + 0.4505937099 * sp;
    const bLab = 0.0259040371 * lp + 0.7827717662 * mp - 0.808675766 * sp;

    // OKLab → OKLCH
    const c = Math.sqrt(a * a + bLab * bLab);
    let h = (Math.atan2(bLab, a) * 180) / Math.PI;
    if (h < 0) h += 360;
    return { l: L, c, h };
}

function neutralBase(knobs: ThemeKnobs): { l: number; c: number; h: number } {
    if (knobs.neutral_temp === 'custom') {
        const parsed = hexToOklch(knobs.neutral_custom_hex);
        if (parsed) return parsed;
        // Unparseable hex → fall back to warm so the rest of the theme
        // generator never receives NaN values.
        return NEUTRAL_PRESETS.warm;
    }
    return NEUTRAL_PRESETS[knobs.neutral_temp];
}

const SYSTEM_STACK = "system-ui, -apple-system, 'Segoe UI', sans-serif";
const MONO_STACK = "'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace";

function stackFor(family: string, isDisplay: boolean): string {
    const clean = (family ?? '').trim();
    if (clean === '' || clean.toLowerCase() === 'system') return SYSTEM_STACK;
    const serifFallback = isDisplay ? ', Georgia, serif' : '';
    const quoted = `'${clean.replace(/'/g, "\\'")}'`;
    return `${quoted}, ${SYSTEM_STACK}${serifFallback}`;
}

const RADIUS_BASE_REM: Record<ThemeKnobs['radius'], number> = {
    tight: 0.25,
    default: 0.5,
    rounded: 0.75,
};

const DENSITY: Record<
    ThemeKnobs['density'],
    { spaceUnit: string; textScale: string }
> = {
    compact: { spaceUnit: '0.875rem', textScale: '0.9375' },
    default: { spaceUnit: '1rem', textScale: '1' },
    comfortable: { spaceUnit: '1.125rem', textScale: '1.0625' },
};

const ELEVATION: Record<
    ThemeKnobs['elevation'],
    { sm: string; md: string; lg: string; xl: string }
> = {
    flat: {
        sm: '0 1px 2px rgb(0 0 0 / 0.04)',
        md: '0 1px 3px rgb(0 0 0 / 0.06)',
        lg: '0 2px 6px rgb(0 0 0 / 0.08)',
        xl: '0 4px 12px rgb(0 0 0 / 0.10)',
    },
    layered: {
        sm: '0 1px 2px rgb(0 0 0 / 0.06)',
        md: '0 4px 8px rgb(0 0 0 / 0.08)',
        lg: '0 8px 16px rgb(0 0 0 / 0.10)',
        xl: '0 16px 32px rgb(0 0 0 / 0.12)',
    },
    dramatic: {
        sm: '0 1px 3px rgb(0 0 0 / 0.10)',
        md: '0 6px 14px rgb(0 0 0 / 0.14)',
        lg: '0 14px 28px rgb(0 0 0 / 0.18)',
        xl: '0 24px 56px rgb(0 0 0 / 0.22)',
    },
};

function fmt(n: number): string {
    return Number(Math.round(n * 1000) / 1000).toString();
}

function oklch(l: number, c: number, h: number): string {
    return `oklch(${fmt(l)} ${fmt(c)} ${fmt(h)})`;
}

function buildScale(base: { l: number; c: number; h: number }): Record<number, string> {
    const out: Record<number, string> = {};
    for (const step of SCALE_STEPS) {
        const lOverride = LIGHTNESS[step];
        const l = lOverride === null ? base.l : (lOverride as number);
        const c = base.c * CHROMA_FALLOFF[step];
        out[step] = oklch(l, c, base.h);
    }
    return out;
}

function buttonRadius(shape: ThemeKnobs['button_shape']): string {
    if (shape === 'pill') return '9999px';
    if (shape === 'square') return '0';
    return 'var(--radius-md)';
}

function pickTriple(
    triple: TypeScaleTriple | undefined,
    sizeDefault: number,
    lineDefault: number,
    trackingDefault: number,
): TypeScaleTriple {
    return {
        size: triple?.size ?? sizeDefault,
        line: triple?.line ?? lineDefault,
        tracking: triple?.tracking ?? trackingDefault,
    };
}

function pickBody(
    body: TypeScaleBody | undefined,
    sizeDefault: number,
    lineDefault: number,
): TypeScaleBody {
    return {
        size: body?.size ?? sizeDefault,
        line: body?.line ?? lineDefault,
    };
}

function typographyVars(knobs: ThemeKnobs): Record<string, string> {
    // Defensive defaults for fixture cases that predate the typography pivot —
    // the PHP twin already does this via `?? 400` etc. Mirroring keeps
    // byte-equivalent output when knobs from old fixtures hit derive().
    const weightBody = knobs.weight_body ?? 400;
    const weightSubheading = knobs.weight_subheading ?? 600;
    const weightHeading = knobs.weight_heading ?? 700;
    const weightDisplay = knobs.weight_display ?? 800;
    const h1 = pickTriple(knobs.scale_h1, 48, 1.1, -0.02);
    const h2 = pickTriple(knobs.scale_h2, 36, 1.15, -0.015);
    const h3 = pickTriple(knobs.scale_h3, 24, 1.25, 0);
    const body = pickBody(knobs.scale_body, 16, 1.5);
    return {
        '--font-weight-body': String(weightBody),
        '--font-weight-subheading': String(weightSubheading),
        '--font-weight-heading': String(weightHeading),
        '--font-weight-display': String(weightDisplay),
        '--text-h1': `${fmt(h1.size)}px`,
        '--text-h1-line': fmt(h1.line),
        '--text-h1-tracking': `${fmt(h1.tracking)}em`,
        '--text-h2': `${fmt(h2.size)}px`,
        '--text-h2-line': fmt(h2.line),
        '--text-h2-tracking': `${fmt(h2.tracking)}em`,
        '--text-h3': `${fmt(h3.size)}px`,
        '--text-h3-line': fmt(h3.line),
        '--text-h3-tracking': `${fmt(h3.tracking)}em`,
        '--text-body': `${fmt(body.size)}px`,
        '--text-body-line': fmt(body.line),
    };
}

export function derive(knobs: ThemeKnobs): DerivedTheme {
    const accentScale = buildScale(knobs.accent);
    const neutralBaseTriple = neutralBase(knobs);
    const neutralScale = buildScale(neutralBaseTriple);

    const sansStack = stackFor(knobs.font_sans, false);
    const displayStack = stackFor(knobs.font_display, true);
    const subheadingStack = stackFor(knobs.font_subheading ?? knobs.font_display, true);

    const radiusBase = RADIUS_BASE_REM[knobs.radius];
    const density = DENSITY[knobs.density];
    const elevation = ELEVATION[knobs.elevation];
    const buttonRad = buttonRadius(knobs.button_shape);

    const radiusVars: Record<string, string> = {
        '--radius-base': `${fmt(radiusBase)}rem`,
        '--radius-sm': `${fmt(radiusBase * 0.5)}rem`,
        '--radius-md': `${fmt(radiusBase)}rem`,
        '--radius-lg': `${fmt(radiusBase * 1.5)}rem`,
        '--radius-xl': `${fmt(radiusBase * 2)}rem`,
        '--radius-full': '9999px',
    };

    const densityVars: Record<string, string> = {
        '--space-unit': density.spaceUnit,
        '--text-scale': density.textScale,
    };

    const shadowVars: Record<string, string> = {
        '--shadow-sm': elevation.sm,
        '--shadow-md': elevation.md,
        '--shadow-lg': elevation.lg,
        '--shadow-xl': elevation.xl,
    };

    const fontVars: Record<string, string> = {
        '--font-sans': sansStack,
        '--font-display': displayStack,
        '--font-subheading': subheadingStack,
        '--font-mono': MONO_STACK,
    };

    const buttonVars: Record<string, string> = {
        '--button-radius': buttonRad,
    };

    const typeVars = typographyVars(knobs);

    const accentVarsLight: Record<string, string> = {};
    const accentVarsDark: Record<string, string> = {};
    for (const step of SCALE_STEPS) {
        accentVarsLight[`--color-accent-${step}`] = accentScale[step];
        accentVarsDark[`--color-accent-${step}`] = accentScale[step];
    }

    const neutralVarsLight: Record<string, string> = {};
    const neutralVarsDark: Record<string, string> = {};
    for (const step of SCALE_STEPS) {
        neutralVarsLight[`--color-neutral-${step}`] = neutralScale[step];
        neutralVarsDark[`--color-neutral-${step}`] = neutralScale[step];
    }

    const aliasesLight: Record<string, string> = {
        '--color-background': neutralScale[50],
        '--color-foreground': neutralScale[900],
        '--color-muted': neutralScale[100],
        '--color-muted-foreground': neutralScale[500],
        '--color-border': neutralScale[200],
        '--color-card': 'oklch(1 0 0)',
        '--color-card-foreground': neutralScale[900],
        '--color-ring': accentScale[500],
        '--color-accent': accentScale[500],
        '--color-accent-secondary': accentScale[400],
        '--color-accent-foreground': 'oklch(1 0 0)',
    };

    const aliasesDark: Record<string, string> = {
        '--color-background': neutralScale[950],
        '--color-foreground': neutralScale[100],
        '--color-muted': neutralScale[900],
        '--color-muted-foreground': neutralScale[400],
        '--color-border': neutralScale[800],
        '--color-card': oklch(0.22, 0.005, neutralBaseTriple.h),
        '--color-card-foreground': neutralScale[100],
        '--color-ring': accentScale[400],
        '--color-accent': accentScale[400],
        '--color-accent-secondary': accentScale[300],
        '--color-accent-foreground': 'oklch(0.1 0 0)',
    };

    const systemLight: Record<string, string> = {
        '--color-success': 'oklch(0.65 0.16 145)',
        '--color-success-foreground': 'oklch(1 0 0)',
        '--color-warning': 'oklch(0.78 0.18 75)',
        '--color-warning-foreground': 'oklch(0.2 0 0)',
        '--color-danger': 'oklch(0.62 0.21 25)',
        '--color-danger-foreground': 'oklch(1 0 0)',
        '--color-info': 'oklch(0.65 0.15 240)',
        '--color-info-foreground': 'oklch(1 0 0)',
        '--color-destructive': 'oklch(0.62 0.21 25)',
        '--color-destructive-foreground': 'oklch(1 0 0)',
    };

    const systemDark: Record<string, string> = {
        '--color-success': 'oklch(0.7 0.14 145)',
        '--color-success-foreground': 'oklch(1 0 0)',
        '--color-warning': 'oklch(0.82 0.16 75)',
        '--color-warning-foreground': 'oklch(0.2 0 0)',
        '--color-danger': 'oklch(0.68 0.19 25)',
        '--color-danger-foreground': 'oklch(1 0 0)',
        '--color-info': 'oklch(0.7 0.13 240)',
        '--color-info-foreground': 'oklch(1 0 0)',
        '--color-destructive': 'oklch(0.68 0.19 25)',
        '--color-destructive-foreground': 'oklch(1 0 0)',
    };

    const cssVarsLight: Record<string, string> = {
        ...accentVarsLight,
        ...neutralVarsLight,
        ...aliasesLight,
        ...systemLight,
        ...radiusVars,
        ...densityVars,
        ...shadowVars,
        ...fontVars,
        ...buttonVars,
        ...typeVars,
    };

    const cssVarsDark: Record<string, string> = {
        ...accentVarsDark,
        ...neutralVarsDark,
        ...aliasesDark,
        ...systemDark,
        ...radiusVars,
        ...densityVars,
        ...shadowVars,
        ...fontVars,
        ...buttonVars,
        ...typeVars,
    };

    return {
        css_vars_light: cssVarsLight,
        css_vars_dark: cssVarsDark,
        fonts: { sans: sansStack, display: displayStack },
        meta: { token_count: Object.keys(cssVarsLight).length, version: 1 },
    };
}
