/**
 * Vega-Lite chart config per UI theme (docs/architecture/05 §3, 09 §6).
 *
 * Portable core: a Vega-Lite `Config` styles every chart globally so charts
 * visually belong to the app rather than looking like stock Vega-Lite. This is
 * the single source of truth mapping a `UiTheme` to a config; it is applied at
 * embed time (never baked into the user's stored spec). Adding a UI theme = one
 * base + expressive pair plus one map entry here.
 *
 * Each theme's config is two layers (docs/chart-theming-scope.md §1):
 *
 * - **Base** — the legibility/integration minimum: transparent background (the
 *   pane color shows through) and guide colors readable on the app's surfaces.
 *   Without this layer, stock black-on-white chart text is illegible on the
 *   dark pane. Colors match the app tokens (`--text`, `--text-secondary`,
 *   `--border`, `--border-strong`).
 * - **Expressive** — the house style: IBM Plex, the Carbon data-viz categorical
 *   palette (white theme for light, g100 for dark — see carbon-charts
 *   `packages/core/scss/_color-palette.scss`), dotted grid, bumped guide
 *   sizes/weights, no plot border. This is the "free color" layer (doc §3.5,
 *   §5); charts render fine without it, just stock-looking.
 *
 * The split exists so a non-house chart style (stock preview, future custom
 * themes) can keep the base layer while replacing the expressive one.
 */

import type { Config } from 'vega-lite';
// Preset chart styles from the vega-themes package (already in the dependency
// tree via vega-embed). Pure data — config objects only — so portable for core.
import * as presets from 'vega-themes';
import type { CustomTheme } from './custom-theme';
import { normalizeRangeSchemes } from './theme-controls';
import type { UiTheme } from './theme';

const PLEX = '"IBM Plex Sans", system-ui, -apple-system, sans-serif';

/** Carbon data-viz 14-color categorical palette — white (light) theme. */
const lightCategory = [
  '#6929c4', // purple 70
  '#1192e8', // cyan 50
  '#005d5d', // teal 70
  '#9f1853', // magenta 70
  '#fa4d56', // red 50
  '#520408', // red 90
  '#198038', // green 60
  '#002d9c', // blue 80
  '#ee5396', // magenta 50
  '#b28600', // yellow 50
  '#009d9a', // teal 50
  '#012749', // cyan 90
  '#8a3800', // orange 70
  '#a56eff', // purple 50
];

/** Carbon data-viz 14-color categorical palette — g100 (dark) theme. */
const darkCategory = [
  '#8a3ffc', // purple 60
  '#33b1ff', // cyan 40
  '#007d79', // teal 60
  '#ff7eb6', // magenta 40
  '#fa4d56', // red 50
  '#fff1f1', // red 10
  '#6fdc8c', // green 30
  '#4589ff', // blue 50
  '#d02670', // magenta 60
  '#d2a106', // yellow 40
  '#08bdba', // teal 40
  '#bae6ff', // cyan 20
  '#ba4e00', // orange 60
  '#d4bbff', // purple 30
];

/** Base layer — light: transparent background + guide colors on app tokens. */
export const lightBaseConfig: Config = {
  background: 'transparent',
  title: { color: '#161616' }, // --text (light)
  axis: {
    domainColor: '#c6c6c6', // axis line — deliberately lighter than UI --border-strong; chart guides stay subtle
    gridColor: '#e0e0e0', // --border (light)
    labelColor: '#525252', // --text-secondary (light)
    titleColor: '#161616', // --text (light)
  },
};

/** Base layer — dark: transparent background + guide colors on app tokens. */
export const darkBaseConfig: Config = {
  background: 'transparent',
  title: { color: '#f4f4f4' }, // --text (dark)
  axis: {
    domainColor: '#525252', // axis line — deliberately lighter than UI --border-strong; chart guides stay subtle
    gridColor: '#393939', // --border (dark)
    labelColor: '#a8a8a8', // --text-secondary (dark)
    titleColor: '#f4f4f4', // --text (dark)
  },
};

/** Expressive layer parts shared by both themes (everything but the palette). */
const sharedExpressive: Config = {
  font: PLEX,
  title: { fontSize: 16, fontWeight: 600 },
  axis: {
    gridDash: [2, 2],
    labelFontSize: 11,
    titleFontSize: 12,
    titleFontWeight: 600,
  },
  view: { stroke: 'transparent' },
};

/** Expressive layer — light: house style + the light categorical palette. */
export const lightExpressiveConfig: Config = {
  ...sharedExpressive,
  range: { category: lightCategory },
};

/** Expressive layer — dark: house style + the dark categorical palette. */
export const darkExpressiveConfig: Config = {
  ...sharedExpressive,
  range: { category: darkCategory },
};

/**
 * Merge a base and an expressive layer into one chart config. Shallow spread
 * plus the two nested objects both layers contribute to (`title`, `axis`);
 * the expressive layer wins on conflicts (there are none today — the layers
 * own disjoint properties).
 */
export function mergeChartLayers(base: Config, expressive: Config): Config {
  return {
    ...base,
    ...expressive,
    title: { ...base.title, ...expressive.title },
    axis: { ...base.axis, ...expressive.axis },
  };
}

export const lightChartConfig: Config = mergeChartLayers(lightBaseConfig, lightExpressiveConfig);

export const darkChartConfig: Config = mergeChartLayers(darkBaseConfig, darkExpressiveConfig);

const CHART_CONFIG: Record<UiTheme, Config> = {
  light: lightChartConfig,
  dark: darkChartConfig,
};

/** The Vega-Lite config for a UI theme — the only theme → config mapping. */
export function chartConfigFor(theme: UiTheme): Config {
  return CHART_CONFIG[theme];
}

/**
 * Selectable chart themes (docs/chart-theming-scope.md §4.2) — the user-facing
 * choice of how charts render, distinct from (and composed with) the UI theme:
 *
 * - `'astrolabe'` — the house style above; resolves per UI theme. Default.
 * - `'stock'` — no injected config at all: charts render exactly as Vega-Lite
 *   defaults would anywhere else (white background, tableau10, sans-serif).
 * - a `vega-themes` preset id — that preset's config verbatim, UI-theme
 *   independent, exactly as it would render in the Vega editor's theme dropdown.
 *
 * The spec's own `config` overrides whatever is selected, property by property
 * (vega-lite merges `opt.config` under `spec.config`), so a snippet can always
 * opt out locally.
 */
export type ChartThemeId = 'astrolabe' | 'stock' | ChartThemePresetId;

/** The vega-themes presets we surface, in display order. */
const PRESET_IDS = [
  'excel',
  'ggplot2',
  'quartz',
  'vox',
  'fivethirtyeight',
  'latimes',
  'urbaninstitute',
  'googlecharts',
  'powerbi',
  'carbonwhite',
  'carbong10',
  'carbong90',
  'carbong100',
  'dark',
] as const;

type ChartThemePresetId = (typeof PRESET_IDS)[number];

/** Empty config — the stock sentinel resolves to "inject nothing". */
const STOCK_CONFIG: Config = {};

export interface ChartThemeOption {
  value: ChartThemeSelection;
  label: string;
  /** Secondary line for pickers (what the choice means). */
  detail?: string;
  /**
   * Group boundary: this option starts the preset roster, visually separated
   * from the built-ins and the user's themes above it. Set here, where the list
   * order is decided — consumers must never recompute the boundary by index.
   */
  dividerBefore?: boolean;
}

/** Display metadata for every selectable chart theme, in display order. */
export const CHART_THEME_OPTIONS: ReadonlyArray<ChartThemeOption> = [
  { value: 'astrolabe', label: 'Astrolabe', detail: 'House style, follows light/dark' },
  { value: 'stock', label: 'Stock Vega-Lite', detail: 'No theme applied' },
  { value: 'excel', label: 'Excel' },
  { value: 'ggplot2', label: 'ggplot2' },
  { value: 'quartz', label: 'Quartz' },
  { value: 'vox', label: 'Vox' },
  { value: 'fivethirtyeight', label: 'FiveThirtyEight' },
  { value: 'latimes', label: 'LA Times' },
  { value: 'urbaninstitute', label: 'Urban Institute' },
  { value: 'googlecharts', label: 'Google Charts' },
  { value: 'powerbi', label: 'Power BI' },
  { value: 'carbonwhite', label: 'Carbon — White' },
  { value: 'carbong10', label: 'Carbon — G10' },
  { value: 'carbong90', label: 'Carbon — G90' },
  { value: 'carbong100', label: 'Carbon — G100' },
  { value: 'dark', label: 'Vega Dark' },
];

const CHART_THEME_IDS: ReadonlySet<string> = new Set(['astrolabe', 'stock', ...PRESET_IDS]);

/** Type guard for persisted values (load-with-fallback; unknown ids fall back). */
export function isChartThemeId(value: unknown): value is ChartThemeId {
  return typeof value === 'string' && CHART_THEME_IDS.has(value);
}

/**
 * A saved custom theme as a selection — `custom:<record id>`. The numeric id
 * (not the name) keys the selection so a rename never invalidates it.
 */
export type CustomThemeSelection = `custom:${number}`;

/** Everything the chart-theme picker can hold: built-ins, presets, or a custom theme. */
export type ChartThemeSelection = ChartThemeId | CustomThemeSelection;

/** The picker/persistence id for a custom theme record. */
export function customThemeSelection(id: number): CustomThemeSelection {
  return `custom:${id}`;
}

/** The record id inside a `custom:<id>` selection, or null for any other value. */
export function customThemeIdOf(selection: string): number | null {
  const match = /^custom:(\d+)$/.exec(selection);
  return match ? Number(match[1]) : null;
}

/**
 * Type guard for persisted selections (load-with-fallback). A `custom:<id>`
 * passes on shape alone — whether the record still exists is only knowable
 * after the async theme hydration, so resolution (not validation) handles a
 * deleted id by falling back to the house config.
 */
export function isChartThemeSelection(value: unknown): value is ChartThemeSelection {
  return isChartThemeId(value) || (typeof value === 'string' && customThemeIdOf(value) !== null);
}

/**
 * The full picker option list: built-ins, the user's saved themes (by name, in
 * library order), then the presets — with the first preset carrying the group
 * divider that bounds "ours" from the imported roster. Pure derivation —
 * callers memoize.
 */
export function chartThemeOptions(
  customThemes: ReadonlyArray<Pick<CustomTheme, 'id' | 'name'>>,
): ChartThemeOption[] {
  const custom: ChartThemeOption[] = customThemes.map((t) => ({
    value: customThemeSelection(t.id),
    label: t.name,
    detail: 'Custom theme',
  }));
  // Built-ins first, the user's own themes next, the preset roster last. The
  // divider rides on the first preset option itself, so it stays correct however
  // a consumer splices into the list (e.g. the picker's "Edit themes…" row).
  const [firstPreset, ...presets] = CHART_THEME_OPTIONS.slice(2);
  return [
    ...CHART_THEME_OPTIONS.slice(0, 2),
    ...custom,
    { ...firstPreset, dividerBefore: true },
    ...presets,
  ];
}

/**
 * Resolve the user's chart-theme selection to the config to inject at embed
 * time. `'astrolabe'` follows the UI theme; presets ignore it (their look is
 * fixed — that's the point of previewing a destination style). A `custom:<id>`
 * resolves to that saved theme's config; an id with no record (not yet
 * hydrated, or deleted elsewhere) falls back to the house config rather than
 * rendering unstyled.
 */
export function chartConfigForSelection(
  selection: ChartThemeSelection,
  uiTheme: UiTheme,
  customThemes: ReadonlyArray<Pick<CustomTheme, 'id' | 'config'>> = [],
): Config {
  const customId = customThemeIdOf(selection);
  if (customId !== null) {
    const theme = customThemes.find((t) => t.id === customId);
    // Heal a custom config saved/imported with a bare range scheme string, which
    // Vega rejects at render (see normalizeRangeSchemes); built-in configs below
    // are already well-formed.
    return theme ? normalizeRangeSchemes(theme.config) : CHART_CONFIG[uiTheme];
  }
  if (selection === 'astrolabe') return CHART_CONFIG[uiTheme];
  if (selection === 'stock') return STOCK_CONFIG;
  return presets[selection as ChartThemePresetId] as Config;
}
