/**
 * Chart export — the per-chart "Export" affordance in the Live Preview header
 * (spec §08 → Per-chart export). Distinct from the header's *workspace* Export
 * (whole-library backup): this gets *one* chart out — its spec to the clipboard or
 * a `.vl.json` file, or its rendered image as PNG/SVG.
 *
 * It lives in the preview header on purpose: the image formats need the live Vega
 * `view` (held by LivePreview), and "export this chart" reads most naturally beside
 * the chart you're looking at. The spec actions export the *shown* text (draft or
 * published), so every format matches what's on screen.
 *
 * A few options ride along, because export has real choices to make:
 *   - **Resolution** (PNG) — a device-pixel-ratio-aware multiplier, so the default
 *     "1×" is already Retina-crisp (a naive 1× export looks soft on a 2× display).
 *   - **Background** — the chart config is transparent (to show the pane colour),
 *     which would make a naive export transparent; default to the theme colour, with
 *     White and Transparent on offer.
 *   - **Referenced data** (shown only when the spec references saved datasets) —
 *     inline the data so the exported spec renders standalone, or keep the reference.
 *
 * Widget: a **disclosure**, not an ARIA menu — like `SettingsPopover`. The container
 * is a labelled `group` of option controls + action `<button>`s (Tab/Shift+Tab move
 * between them); the trigger carries `aria-expanded` + `aria-controls`; Esc closes
 * and restores focus; an outside click closes. It shares the single-open popover
 * registry, and is portaled to `<body>` (the panes clip their overflow).
 */

import { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { useShallow } from 'zustand/react/shallow';
import {
  chartExportFilename,
  inlineReferencedDatasets,
  referencedDatasetNames,
} from '@core/chart-export';
import { DatasetNotFoundError } from '@core/rendering';
import { copyText, downloadJson, downloadUrl } from '../infrastructure/file-transfer';
import { usePopover } from '../hooks/usePopover';
import { useDatasetStore } from '../stores/DatasetStore';
import { notify } from '../stores/NotificationStore';
import { selectActiveSnippet, selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { Button } from './Button';
import { Icon } from './Icon';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { SettingRow } from './SettingsPopover';
import styles from './ChartExport.module.css';

/** Shared registry id (single popover open at a time across the panes). */
const POP_ID = 'chart-export';

/** Focus the first action button or option radio on open. */
const INITIAL_FOCUS = ['button, [role="radio"]'] as const;

type ScaleChoice = '1' | '2' | '3';
type BackgroundChoice = 'theme' | 'white' | 'transparent';

/** PNG resolution multipliers (× device pixel ratio — see ImageExportOptions). */
const SCALE_OPTIONS: ReadonlyArray<SegmentedOption<ScaleChoice>> = [
  { value: '1', label: '1×', title: '1× — matches your screen (Retina-aware)' },
  { value: '2', label: '2×', title: '2× — double resolution, for print or zoom' },
  { value: '3', label: '3×', title: '3× — triple resolution' },
];

const BACKGROUND_OPTIONS: ReadonlyArray<SegmentedOption<BackgroundChoice>> = [
  { value: 'theme', label: 'Theme', title: 'Match the current theme’s background' },
  { value: 'white', label: 'White', title: 'White background' },
  { value: 'transparent', label: 'None', title: 'Transparent background' },
];

const REFDATA_OPTIONS: ReadonlyArray<SegmentedOption<'inline' | 'ref'>> = [
  { value: 'inline', label: 'Inline', title: 'Embed the data so the file renders standalone' },
  {
    value: 'ref',
    label: 'Keep refs',
    title: 'Keep the dataset reference (renders only in Astrolabe)',
  },
];

/** Resolve a background choice to a CSS colour, or null for transparent. The
 *  "theme" colour is read live from the page so it always tracks the active theme. */
function resolveBackground(choice: BackgroundChoice): string | null {
  if (choice === 'transparent') return null;
  if (choice === 'white') return '#ffffff';
  const bg = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim();
  return bg || '#ffffff';
}

export interface ChartExportProps {
  /** Whether a chart is currently rendered — gates the image (PNG/SVG) actions.
   *  The spec actions only need text, so they ignore this. */
  chartReady: boolean;
  /** Rasterize/serialize the live view to a downloadable URL, or null if no view
   *  is live (the not-ready case). Rejects if rasterizing/serializing fails — a real
   *  error the caller reports, not a not-ready state (caller owns the Vega view). */
  getImageUrl: (
    format: 'png' | 'svg',
    options: { scale: number; background: string | null },
  ) => Promise<string | null>;
}

export function ChartExport({ chartReady, getImageUrl }: ChartExportProps) {
  const { open, toggle, close, triggerRef, setPopNode } = usePopover({
    id: POP_ID,
    align: 'right',
    initialFocus: INITIAL_FOCUS,
  });
  // Primitive reads only (no fresh objects) so the header doesn't re-render needlessly.
  const name = useSnippetStore((s) => selectActiveSnippet(s)?.name ?? 'chart');
  const shownText = useSnippetStore(selectShownText);
  const datasets = useDatasetStore(useShallow((s) => s.datasets));
  const hasSpec = shownText.trim() !== '';

  // Export options, persisted while the component is mounted (across opens).
  const [scale, setScale] = useState<ScaleChoice>('1');
  const [background, setBackground] = useState<BackgroundChoice>('theme');
  const [inline, setInline] = useState(true);

  // Which saved datasets the shown spec references — drives the inline-data option.
  const refs = useMemo(() => referencedDatasetNames(shownText), [shownText]);

  /** The spec text to export — inlined for portability when chosen and refs exist.
   *  Returns null after surfacing an error (a referenced dataset is missing). */
  const buildSpecText = (): string | null => {
    if (!inline || refs.length === 0) return shownText;
    try {
      return inlineReferencedDatasets(shownText, datasets);
    } catch (e) {
      const missing = e instanceof DatasetNotFoundError ? ` "${e.datasetName}"` : '';
      notify({
        kind: 'error',
        title: 'Couldn’t inline data',
        message: `A referenced dataset${missing} isn’t in your library. Add it, or choose “Keep refs”.`,
      });
      return null;
    }
  };

  const copySpec = async () => {
    close();
    const text = buildSpecText();
    if (text == null) return;
    try {
      await copyText(text);
      notify({
        kind: 'success',
        title: 'Spec copied',
        message: 'The chart’s Vega-Lite spec is on your clipboard as JSON.',
      });
    } catch {
      notify({
        kind: 'error',
        title: 'Couldn’t copy',
        message: 'Your browser blocked clipboard access. Select and copy the spec from the editor.',
      });
    }
  };

  const downloadSpec = () => {
    close();
    const text = buildSpecText();
    if (text == null) return;
    const filename = chartExportFilename(name, 'vl.json');
    downloadJson(filename, text);
    notify({ kind: 'success', title: 'Spec downloaded', message: `Saved ${filename}.` });
  };

  const downloadImage = async (format: 'png' | 'svg') => {
    close();
    let url: string | null;
    try {
      url = await getImageUrl(format, {
        scale: Number(scale),
        background: resolveBackground(background),
      });
    } catch (err) {
      // The view is live but rasterizing/serializing it threw — a real failure
      // (e.g. a tainted canvas or an SVG the browser won't serialize), not a
      // not-ready state. Surface it with its detail rather than burying it.
      notify({
        kind: 'error',
        title: 'Couldn’t export image',
        message: `Astrolabe couldn’t turn the chart into ${format.toUpperCase()}. This can happen with very large charts or content the browser won’t serialize.`,
        detail: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
      });
      return;
    }
    if (!url) {
      notify({
        kind: 'error',
        title: 'Couldn’t export image',
        message: 'The chart isn’t ready yet. Wait for it to finish rendering, then try again.',
      });
      return;
    }
    const filename = chartExportFilename(name, format);
    downloadUrl(filename, url);
    notify({ kind: 'success', title: 'Chart exported', message: `Saved ${filename}.` });
  };

  return (
    <div className={styles.wrap}>
      <Button
        ref={triggerRef}
        variant="ghost"
        aria-expanded={open}
        aria-controls={POP_ID}
        disabled={!hasSpec}
        title={hasSpec ? 'Export this chart' : 'Select a snippet to export'}
        onClick={toggle}
      >
        <Icon name="export" />
        <span>Export</span>
      </Button>
      {open &&
        createPortal(
          <div
            ref={setPopNode}
            id={POP_ID}
            className={styles.pop}
            role="group"
            aria-label="Export chart"
          >
            <h4 className={styles.groupTitle}>Spec</h4>
            {refs.length > 0 && (
              <SettingRow label="Referenced data">
                <SegmentedControl
                  label="Referenced data"
                  options={REFDATA_OPTIONS}
                  value={inline ? 'inline' : 'ref'}
                  onChange={(v) => setInline(v === 'inline')}
                />
              </SettingRow>
            )}
            <button type="button" className={styles.action} onClick={() => void copySpec()}>
              Copy spec
            </button>
            <button type="button" className={styles.action} onClick={downloadSpec}>
              Download JSON <span className={styles.ext}>.vl.json</span>
            </button>

            <h4 className={styles.groupTitle}>Image</h4>
            <SettingRow label="Resolution">
              <SegmentedControl
                label="PNG resolution"
                options={SCALE_OPTIONS}
                value={scale}
                onChange={setScale}
              />
            </SettingRow>
            <SettingRow label="Background">
              <SegmentedControl
                label="Background"
                options={BACKGROUND_OPTIONS}
                value={background}
                onChange={setBackground}
              />
            </SettingRow>
            <button
              type="button"
              className={styles.action}
              disabled={!chartReady}
              onClick={() => void downloadImage('png')}
            >
              Download PNG
            </button>
            <button
              type="button"
              className={styles.action}
              disabled={!chartReady}
              onClick={() => void downloadImage('svg')}
            >
              Download SVG
            </button>
            {!chartReady && <p className={styles.hint}>Render the chart to export an image.</p>}
          </div>,
          document.body,
        )}
    </div>
  );
}
