/**
 * Onboarding canvas — the empty-workspace surface (spec §02 → First-Run & Empty
 * Workspace).
 *
 * Shown by `App` in place of the editor + preview whenever the library is empty
 * (first run, or after the last snippet is deleted). Rather than seeding a
 * placeholder snippet, it greets the user and offers two deliberate ways in: a
 * primary "Create your first snippet" (the sample template) and a gallery of
 * example snippets, each previewed live and addable on its own or all at once.
 *
 * The previews render through the shared `chart-renderer` — the one place that
 * touches vega-embed — so there is no parallel embed path. Each card's chart
 * lives in its own node and owns its handle's lifecycle (finalized on unmount),
 * independent of `LivePreview`'s single-host render serialization.
 */

import { useEffect, useRef, useState } from 'react';
import type { VisualizationSpec } from 'vega-embed';
import { CHART_EXAMPLES, exampleSpecText, type ChartExample } from '@core/examples';
import { createSnippet as createSnippetRecord, deriveSnippetName } from '@core/snippet';
import { chartConfigFor } from '@core/vega-themes';
import { openModal } from '../modals/ModalCoordinator';
import { renderSpec, type RenderHandle } from '../services/chart-renderer';
import { importWorkspace } from '../services/transfer';
import { useAppStore } from '../stores/AppStore';
import { usePanesStore } from '../stores/PanesStore';
import { useSnippetStore } from '../stores/SnippetStore';
import { Button } from './Button';
import { Icon } from './Icon';
import styles from './Onboarding.module.css';

/** Fixed thumbnail height; width fills the card via Vega's `container` sizing. */
const THUMB_HEIGHT = 140;

/**
 * Live preview of one example. Renders the example spec at card width through the
 * shared renderer, finalizing the Vega view on unmount or when the spec/theme
 * changes — a chart that isn't finalized leaks its timers and listeners.
 */
function ExampleThumbnail({ spec }: { spec: Record<string, unknown> }) {
  const hostRef = useRef<HTMLDivElement>(null);
  const uiTheme = useAppStore((s) => s.uiTheme);

  useEffect(() => {
    const node = hostRef.current;
    if (!node) return;
    let handle: RenderHandle | null = null;
    let cancelled = false;
    // `container` width fits the card; a fixed height keeps every card uniform.
    // Spread onto a copy so the shared example object is never mutated.
    const sized = { ...spec, width: 'container', height: THUMB_HEIGHT } as VisualizationSpec;
    void renderSpec(node, sized, chartConfigFor(uiTheme))
      .then((h) => {
        if (cancelled) h.destroy();
        else handle = h;
      })
      .catch(() => {
        // Examples are schema-validated (examples.test.ts), but a thumbnail that
        // somehow fails to render must never break onboarding — leave the card
        // image blank and let the name + description carry it.
      });
    // A `container`-width chart only re-reads its size on a window resize, so the
    // grid reflowing a card (window resize → new column width) needs the same
    // synthesized re-fit LivePreview uses. The observer reads `handle` lazily, so
    // it no-ops until the async render resolves.
    let ro: ResizeObserver | undefined;
    if (typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(() => handle?.resize());
      ro.observe(node);
    }
    return () => {
      cancelled = true;
      ro?.disconnect();
      handle?.destroy();
    };
  }, [spec, uiTheme]);

  // Decorative: the name, description, and Add button carry the meaning, so a
  // screen reader hears "Bar chart … Add", not a tree of chart SVG nodes (arch §10).
  return (
    <div
      ref={hostRef}
      className={styles.thumb}
      style={{ height: THUMB_HEIGHT }}
      aria-hidden="true"
    />
  );
}

export function Onboarding() {
  const createSnippet = useSnippetStore((s) => s.createSnippet);
  const addSnippets = useSnippetStore((s) => s.addSnippets);
  const applyOnboardingSplit = usePanesStore((s) => s.applyOnboardingSplit);

  // The paste door (spec §02): a disclosure (WAI-ARIA APG disclosure pattern —
  // button + aria-expanded/aria-controls, no focus trap), not a modal: the canvas
  // has the whole workspace to itself, so revealing the paste surface in place
  // costs no context. The panel stays mounted (`hidden`) so a draft paste
  // survives a collapse.
  const [pasteOpen, setPasteOpen] = useState(false);
  const [pasteText, setPasteText] = useState('');
  const pasteTriggerRef = useRef<HTMLButtonElement>(null);
  const pasteAreaRef = useRef<HTMLTextAreaElement>(null);
  const importInputRef = useRef<HTMLInputElement>(null);

  // The revealed panel exists only for immediate input, so focus follows the
  // expand; Cancel returns it to the trigger (NN/g #3 — a clearly marked exit).
  useEffect(() => {
    if (pasteOpen) pasteAreaRef.current?.focus();
  }, [pasteOpen]);

  // Leaving onboarding lays the workspace out at the default 25·25·50 split, so
  // the first chart opens with a generous preview (spec §02). The canvas fills
  // the window here, so its width is a good proxy for the panes container.
  const layoutWorkspace = () => applyOnboardingSplit(window.innerWidth);

  // Primary path and per-example add both go through the normal create action,
  // so an added example opens in the editor exactly like any new snippet.
  const handleCreate = () => {
    createSnippet();
    layoutWorkspace();
  };
  const handleAdd = (example: ChartExample) => {
    createSnippet({ name: example.name, spec: exampleSpecText(example) });
    layoutWorkspace();
  };

  // The data-first door (spec §02): straight into the Chart Builder, which picks
  // up the freshest dataset — or, with none yet, explains itself and offers "Add a
  // dataset". The workspace split is applied up front: if the builder creates the
  // first snippet, onboarding unmounts without another chance to lay the panes out.
  const handleBuild = () => {
    layoutWorkspace();
    openModal('chartBuilder');
  };

  // Pasted text is accepted as-is: the editor is the product's validator, and an
  // almost-right spec opening with live schema errors is the feature working.
  // The name derives from the spec (title → "Mark chart of y by x"), with
  // `nameSource: 'auto'` so it keeps tracking the spec until the user renames.
  const handlePasteAdd = () => {
    const text = pasteText.trim();
    if (!text) return;
    createSnippet({
      name: deriveSnippetName(text) ?? 'Pasted spec',
      nameSource: 'auto',
      spec: text,
    });
    layoutWorkspace();
  };
  const closePaste = () => {
    setPasteOpen(false);
    pasteTriggerRef.current?.focus();
  };

  // Same hidden-picker pattern as the header's Import (spec §08 — the browser
  // file dialog is the only chrome); reset so re-picking the same file re-fires.
  // TODO: third hidden-file-picker site (App.tsx header, TypeControls.tsx) — past
  // the shared-piece threshold; eng-council proposed a useFilePicker/HiddenFileInput
  // shape. Consult before building (new hook/component kind).
  const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    e.target.value = '';
    if (file) void importWorkspace(file);
  };

  const handleAddAll = () => {
    // Stagger the timestamps so the first example (the bar chart) is the newest:
    // it then sorts to the top of the library and `addSnippets` makes it active
    // (it selects the newest when nothing is active — true during onboarding).
    const base = Date.now();
    const records = CHART_EXAMPLES.map((example, i) =>
      createSnippetRecord({
        name: example.name,
        spec: exampleSpecText(example),
        now: new Date(base - i * 1000),
      }),
    );
    addSnippets(records);
    layoutWorkspace();
  };

  return (
    <div className={styles.onboarding}>
      <div className={styles.inner}>
        <h2 className={styles.title}>Welcome to Astrolabe</h2>
        <p className={styles.tagline}>
          A local library for your Vega-Lite charts — write the spec as JSON, watch it render live,
          and keep it all on your device.
        </p>

        <div className={styles.ctaRow}>
          <Button variant="primary" size="lg" onClick={handleCreate}>
            <Icon name="add" /> Create your first snippet
          </Button>
          <Button size="lg" onClick={handleBuild}>
            <Icon name="chart" /> Build a chart from your data
          </Button>
          <Button
            size="lg"
            ref={pasteTriggerRef}
            aria-expanded={pasteOpen}
            aria-controls="onboarding-paste-panel"
            onClick={() => setPasteOpen((open) => !open)}
          >
            <Icon name="import" /> Paste a spec you already have
          </Button>
        </div>

        <div id="onboarding-paste-panel" className={styles.pastePanel} hidden={!pasteOpen}>
          {/* Visible label above the field (GOV.UK textarea — placeholder text is
              not a substitute for a label). */}
          <label className={styles.pasteLabel} htmlFor="onboarding-paste-input">
            Paste a Vega-Lite spec
          </label>
          <p className={styles.pasteHint} id="onboarding-paste-hint">
            Vega-Lite JSON from anywhere — a notebook, the Vega editor, an AI chat. It becomes your
            first snippet and opens in the editor, live errors and all.
          </p>
          <textarea
            ref={pasteAreaRef}
            id="onboarding-paste-input"
            className={styles.pasteInput}
            aria-describedby="onboarding-paste-hint"
            rows={8}
            spellCheck={false}
            value={pasteText}
            onChange={(e) => setPasteText(e.target.value)}
          />
          <div className={styles.pasteActions}>
            <Button onClick={handlePasteAdd} disabled={pasteText.trim() === ''}>
              Add to library
            </Button>
            <Button variant="ghost" onClick={closePaste}>
              Cancel
            </Button>
          </div>
        </div>

        {/* Secondary paths as quiet links below the doors (Carbon empty-states —
            secondary calls to action are links, not more buttons). */}
        <p className={styles.altPaths}>
          Restoring from an export?{' '}
          <button
            type="button"
            className={styles.linkButton}
            onClick={() => importInputRef.current?.click()}
          >
            Import your workspace
          </button>
          <span aria-hidden="true"> · </span>
          New to Vega-Lite?{' '}
          <a className={styles.linkButton} href="/learn/" target="_blank" rel="noopener noreferrer">
            Read the deep dives
          </a>
        </p>
        <input
          ref={importInputRef}
          type="file"
          accept="application/json,.json"
          className={styles.hiddenInput}
          onChange={handleImportFile}
          aria-hidden="true"
          tabIndex={-1}
        />

        <div className={styles.galleryHead}>
          <h3 className={styles.galleryTitle}>Or start from an example</h3>
          <Button className={styles.addAll} onClick={handleAddAll}>
            Add all
          </Button>
        </div>

        <ul className={styles.gallery}>
          {CHART_EXAMPLES.map((example) => (
            <li key={example.id} className={styles.card}>
              <ExampleThumbnail spec={example.spec} />
              <div className={styles.cardBody}>
                <span className={styles.cardName}>{example.name}</span>
                <span className={styles.cardDesc}>{example.description}</span>
              </div>
              <Button
                className={styles.add}
                aria-label={`Add ${example.name}`}
                onClick={() => handleAdd(example)}
              >
                <Icon name="add" /> Add
              </Button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}
