/**
 * Theme Builder — create and edit custom chart themes (spec §04 → Chart theme;
 * docs/chart-theming-scope.md §4.4).
 *
 * Left pane: the saved-theme list plus "New theme" (seeded from whatever chart
 * theme is currently selected, so any preset or the house style can be a
 * starting point). Main pane: the draft's name, its config as editable JSON,
 * a font control that writes one family across every font slot in the config,
 * and a live gallery of small charts re-rendered from the draft config — the
 * same config-injection path the Live Preview uses, so what the gallery shows
 * is exactly what selecting the theme will do.
 */

import { useEffect, useRef, useState } from 'react';
import type { JsonObject } from '@core/spec-config';
import { THEME_PREVIEW_SPECS, type ThemePreviewSpec } from '@core/theme-preview-specs';
import { normalizeRangeSchemes } from '@core/theme-controls';
import { chartConfigForSelection, chartThemeOptions } from '@core/vega-themes';
import { renderSpec, type RenderHandle } from '../services/chart-renderer';
import { useAppStore } from '../stores/AppStore';
import { confirm } from '../stores/ConfirmStore';
import {
  selectIsDraftDirty,
  selectSelectedTheme,
  useCustomThemeStore,
} from '../stores/CustomThemeStore';
import { notify } from '../stores/NotificationStore';
import { resnapshot } from '../modals/ModalCoordinator';
import type { ComponentType } from 'react';
import { AxesControls } from './AxesControls';
import { Button } from './Button';
import { ColorControls } from './ColorControls';
import { FormatControls } from './FormatControls';
import { HeaderControls } from './HeaderControls';
import { LayoutControls } from './LayoutControls';
import { MarksControls } from './MarksControls';
import { LegendControls } from './LegendControls';
import { TitleControls } from './TitleControls';
import { TypeControls } from './TypeControls';
import styles from './ThemeBuilderModal.module.css';

/**
 * Structured-control tabs, in display order (docs/chart-theming-scope.md §5).
 * Each panel takes the parsed draft config and writes through the shared
 * `useConfigSetter`; the map is the single registry of id → label → panel.
 */
const THEME_TABS = [
  { id: 'color', label: 'Color', Panel: ColorControls },
  { id: 'marks', label: 'Marks', Panel: MarksControls },
  { id: 'type', label: 'Type', Panel: TypeControls },
  { id: 'title', label: 'Title', Panel: TitleControls },
  { id: 'layout', label: 'Layout', Panel: LayoutControls },
  { id: 'axes', label: 'Axes & grid', Panel: AxesControls },
  { id: 'legend', label: 'Legend', Panel: LegendControls },
  { id: 'headers', label: 'Headers', Panel: HeaderControls },
  { id: 'formats', label: 'Formats', Panel: FormatControls },
] as const satisfies ReadonlyArray<{
  id: string;
  label: string;
  Panel: ComponentType<{ config: JsonObject }>;
}>;
type ThemeTab = (typeof THEME_TABS)[number]['id'];

/** Debounce for gallery re-renders while the config text is edited (ms). */
const GALLERY_DEBOUNCE = 250;

/**
 * One gallery card: a fixed sample spec rendered with the draft config. Canvas
 * renderer — seven concurrent SVG charts would put thousands of nodes in a
 * modal; raster is invisible at swatch size (same trade-off as the Chart
 * Builder preview). Renders are serialized per card with the LivePreview
 * chain-lock pattern so a slow embed never interleaves with a newer one on the
 * shared host node. A render failure surfaces in the card as its message (the
 * same fail-loud treatment LivePreview gives the editor — arch 02), never a
 * silent blank: a config the user is editing that Vega can't render is exactly
 * the feedback they need.
 */
function GalleryCard({ card, config }: { card: ThemePreviewSpec; config: JsonObject }) {
  const hostRef = useRef<HTMLDivElement>(null);
  const handleRef = useRef<RenderHandle | null>(null);
  const generationRef = useRef(0);
  const chainRef = useRef<Promise<void>>(Promise.resolve());
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const node = hostRef.current;
    if (!node) return;
    const timer = setTimeout(() => {
      const mine = ++generationRef.current;
      const prior = chainRef.current;
      let release!: () => void;
      chainRef.current = new Promise<void>((r) => {
        release = r;
      });
      void (async () => {
        try {
          await prior;
          if (mine !== generationRef.current) return;
          handleRef.current?.destroy();
          handleRef.current = null;
          // Heal a bare range scheme string (rejected by Vega at render) the same
          // way the live preview does — so a theme loaded with the old form still
          // previews. New control writes already use the `{ scheme }` object.
          const handle = await renderSpec(node, card.spec, normalizeRangeSchemes(config), {
            renderer: 'canvas',
          });
          if (mine !== generationRef.current) {
            handle.destroy();
            return;
          }
          handleRef.current = handle;
          setError(null);
        } catch (err) {
          // Don't bury it (arch 02 fail-loud): show the message in the card so a
          // config that renders on valid JSON but Vega rejects at runtime is
          // visible, not a silent blank.
          if (mine === generationRef.current) setError((err as Error).message);
        } finally {
          release();
        }
      })();
    }, GALLERY_DEBOUNCE);
    return () => clearTimeout(timer);
  }, [card, config]);

  useEffect(
    () => () => {
      generationRef.current++;
      handleRef.current?.destroy();
      handleRef.current = null;
    },
    [],
  );

  return (
    <figure className={styles.card}>
      {error !== null && (
        <pre className={styles.cardError} role="alert">
          {error}
        </pre>
      )}
      <div className={styles.cardHost} ref={hostRef} hidden={error !== null} />
      <figcaption className={styles.cardCaption}>{card.caption}</figcaption>
    </figure>
  );
}

export function ThemeBuilderModal() {
  const themes = useCustomThemeStore((s) => s.themes);
  const selectedId = useCustomThemeStore((s) => s.selectedId);
  const draft = useCustomThemeStore((s) => s.draft);
  const draftConfig = useCustomThemeStore((s) => s.draftConfig);
  const parseError = useCustomThemeStore((s) => s.parseError);
  const saveError = useCustomThemeStore((s) => s.saveError);
  const dirty = useCustomThemeStore(selectIsDraftDirty);
  const selectedTheme = useCustomThemeStore(selectSelectedTheme);
  const [activeTab, setActiveTab] = useState<ThemeTab>('color');
  // The raw JSON is the advanced/escape-hatch surface, collapsed off the first
  // screen so the structured controls own it; a parse error forces it open
  // (it's the only place to fix the JSON).
  const [jsonExpanded, setJsonExpanded] = useState(false);
  const jsonOpen = jsonExpanded || parseError !== null;
  const ActivePanel = THEME_TABS.find((t) => t.id === activeTab)!.Panel;

  // APG tabs (vertical orientation): Up/Down + Home/End move selection, which
  // follows focus (automatic activation — the panel swap is cheap). The portaled
  // SelectControl popovers inside a panel manage their own focus.
  const onTabKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    const ids = THEME_TABS.map((t) => t.id);
    const i = ids.indexOf(activeTab);
    let next = -1;
    if (e.key === 'ArrowDown') next = (i + 1) % ids.length;
    else if (e.key === 'ArrowUp') next = (i - 1 + ids.length) % ids.length;
    else if (e.key === 'Home') next = 0;
    else if (e.key === 'End') next = ids.length - 1;
    if (next < 0) return;
    e.preventDefault();
    setActiveTab(ids[next]);
    (e.currentTarget.children[next] as HTMLElement | undefined)?.focus();
  };

  const handleNew = () => {
    const app = useAppStore.getState();
    const store = useCustomThemeStore.getState();
    // Seed from whatever the picker currently shows — duplicating a preset (or
    // the house style, or another custom theme) is the creation path (scope §4.4).
    const seed = chartConfigForSelection(app.chartTheme, app.uiTheme, store.themes) as JsonObject;
    const sourceLabel =
      chartThemeOptions(store.themes).find((o) => o.value === app.chartTheme)?.label ?? 'Theme';
    store.createTheme(`${sourceLabel} copy`, seed);
    // The fresh draft is the new baseline — creating then closing isn't a loss.
    resnapshot();
  };

  const handleSave = () => {
    if (useCustomThemeStore.getState().saveDraft()) {
      resnapshot();
      // The saved name in the list is visible, but the commit itself has no
      // other on-screen change (the draft stays open) — confirm it.
      notify({ kind: 'success', title: 'Theme saved', message: 'Your chart theme was updated.' });
    }
  };

  const handleDelete = async () => {
    if (!selectedTheme) return;
    const ok = await confirm({
      title: 'Delete theme',
      message: `Delete "${selectedTheme.name}"? This cannot be undone.`,
      confirmLabel: 'Delete',
      danger: true,
    });
    if (!ok) return;
    const removedName = selectedTheme.name;
    useCustomThemeStore.getState().remove(selectedTheme.id);
    resnapshot();
    notify({
      kind: 'success',
      title: 'Theme deleted',
      message: `"${removedName}" was permanently removed.`,
    });
  };

  return (
    <div className={styles.builder}>
      <div className={styles.listPane}>
        <Button variant="primary" size="lg" className={styles.newButton} onClick={handleNew}>
          New theme
        </Button>
        <ul className={styles.list}>
          {themes.length === 0 && (
            <li className={styles.empty}>
              No custom themes yet. A new theme starts as a copy of the chart theme currently
              selected in the preview, so pick a preset you like as the starting point.
            </li>
          )}
          {themes.map((theme) => (
            <li
              key={theme.id}
              className={`${styles.item} ${theme.id === selectedId ? styles.itemActive : ''}`}
            >
              <button
                type="button"
                className={styles.itemButton}
                aria-current={theme.id === selectedId || undefined}
                onClick={() => useCustomThemeStore.getState().select(theme.id)}
              >
                {theme.name}
              </button>
            </li>
          ))}
        </ul>
      </div>

      {draft === null ? (
        <div className={styles.detailEmpty}>Create a theme to start editing.</div>
      ) : (
        <div className={styles.main}>
          <div className={styles.toolbar}>
            <div className={styles.nameField}>
              <label className={styles.label} htmlFor="theme-name">
                Name
              </label>
              <input
                id="theme-name"
                className={styles.input}
                value={draft.name}
                onChange={(e) =>
                  useCustomThemeStore.getState().updateDraft({ name: e.target.value })
                }
              />
            </div>
            <div className={styles.toolbarEnd}>
              <Button variant="danger-outline" onClick={() => void handleDelete()}>
                Delete
              </Button>
              <Button
                variant="primary"
                disabled={!dirty || parseError !== null}
                onClick={handleSave}
              >
                Save theme
              </Button>
            </div>
          </div>

          <div className={styles.body}>
            <div className={styles.controlsCol}>
              {(saveError ?? parseError) !== null && (
                <p className={styles.errorMessage} role="alert">
                  {saveError ?? parseError}
                </p>
              )}

              <div className={styles.vsplit}>
                <div
                  className={styles.tabs}
                  role="tablist"
                  aria-label="Theme controls"
                  aria-orientation="vertical"
                  onKeyDown={onTabKeyDown}
                >
                  {THEME_TABS.map((t) => (
                    <button
                      key={t.id}
                      type="button"
                      role="tab"
                      id={`theme-tab-${t.id}`}
                      aria-selected={activeTab === t.id}
                      aria-controls={`theme-tabpanel-${t.id}`}
                      tabIndex={activeTab === t.id ? 0 : -1}
                      className={`${styles.tab} ${activeTab === t.id ? styles.tabActive : ''}`}
                      onClick={() => setActiveTab(t.id)}
                    >
                      {t.label}
                    </button>
                  ))}
                </div>

                <div
                  className={styles.tabpanel}
                  role="tabpanel"
                  id={`theme-tabpanel-${activeTab}`}
                  aria-labelledby={`theme-tab-${activeTab}`}
                >
                  {parseError !== null ? (
                    <p className={styles.controlsDisabled}>
                      Fix the JSON below to use these controls.
                    </p>
                  ) : draftConfig === null ? null : (
                    <ActivePanel config={draftConfig} />
                  )}
                </div>
              </div>

              {/* Raw JSON — collapsed by default (the structured controls are the
                  first-screen surface); forced open while a parse error is the
                  only thing that needs the textarea. */}
              <div className={styles.jsonSection}>
                <button
                  type="button"
                  className={styles.jsonToggle}
                  aria-expanded={jsonOpen}
                  aria-controls="theme-config"
                  onClick={() => setJsonExpanded((o) => !o)}
                >
                  <span className={styles.jsonCaret} aria-hidden="true">
                    {jsonOpen ? '▾' : '▸'}
                  </span>
                  Config (Vega-Lite JSON)
                </button>
                {jsonOpen && (
                  <div className={styles.editorPane}>
                    {/* A plain textarea, deliberately not Monaco: a second Monaco mount
                      is heavy inside a modal for an occasional surface, and the parse
                      error above is the feedback channel that matters here. Revisit
                      only if real usage asks for config completions. */}
                    <textarea
                      id="theme-config"
                      className={styles.configText}
                      spellCheck={false}
                      value={draft.configText}
                      onChange={(e) =>
                        useCustomThemeStore.getState().updateDraft({ configText: e.target.value })
                      }
                    />
                  </div>
                )}
              </div>
            </div>
            <div className={styles.gallery} aria-label="Theme preview gallery">
              {draftConfig !== null &&
                THEME_PREVIEW_SPECS.map((card) => (
                  <GalleryCard key={card.id} card={card} config={draftConfig} />
                ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
