/**
 * Data inspector — input vs. resolved rows, per drawn table (spec §04).
 *
 * Shows the chart's data with a toggle between two ends of a table's pipeline:
 * **Input** (the parsed source rows, before the view's transforms) and
 * **Resolved** (the rows the marks draw, after filters / calculated fields /
 * aggregation). Seeing input → output is how you answer "why is my chart
 * empty/wrong" — look at what the transforms did to the data.
 *
 * A composed spec (layer/concat/facet/repeat) draws several tables, so a **view
 * picker** (`SelectControl`) lets the user choose which one to inspect — "what data
 * am I actually visualizing?". The picker is hidden for the common single-table
 * case (council: NN/g #8 — no one-option control; docs/architecture/10). Labels
 * never show Vega's compiler names (`source_0`/`data_2`), only a user-authored
 * dataset name or an ordinal "View N" plus a columns·rows recognition cue
 * (`@core/inspect-views`; NN/g #2/#6).
 *
 * Tables come from the live Vega view via `RenderHandle.inspectData()`, passed in as
 * `getData` so this component never touches the view (the embedding boundary, arch
 * 05). Read lazily — only while expanded — so a collapsed inspector costs nothing.
 *
 * `DataInspectorPanel` is the reusable shape (an APG disclosure, mirroring the
 * builder's source-rows preview); `DataInspector` binds it to the persisted
 * preview-pane open state for the live-preview pane.
 */

import { useMemo, useState } from 'react';
import type { InspectableTable, InspectedData } from '../services/chart-renderer';
import { useAppStore } from '../stores/AppStore';
import { DataTable } from './DataTable';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { SelectControl, type SelectControlOption } from './SelectControl';
import styles from './DataInspector.module.css';

/** Rows shown before truncating — matches the builder's source-preview cap (spec §06). */
const ROW_LIMIT = 50;
/** Columns named in a table's picker `detail` before eliding the rest. */
const DETAIL_COLUMNS = 4;

type DataView = 'input' | 'resolved';

/** The two stages, in input → output order (the natural reading direction). */
const VIEW_OPTIONS: ReadonlyArray<SegmentedOption<DataView>> = [
  // `title` doubles as the accessible name (WCAG 2.5.3 label-in-name; leads with
  // the visible label).
  { value: 'input', label: 'Input', title: 'Input — the source rows before the view’s transforms' },
  {
    value: 'resolved',
    label: 'Resolved',
    title: 'Resolved — the rows the marks draw, after the view’s transforms',
  },
];

/** A recognition cue for the view picker: row count + the first few columns. */
function tableDetail(table: InspectableTable): string {
  const rows = table.resolved;
  const count = `${rows.length.toLocaleString()} ${rows.length === 1 ? 'row' : 'rows'}`;
  const columns = rows[0] ? Object.keys(rows[0]) : [];
  if (columns.length === 0) return count;
  const shown = columns.slice(0, DETAIL_COLUMNS).join(', ');
  const more = columns.length > DETAIL_COLUMNS ? `, +${columns.length - DETAIL_COLUMNS}` : '';
  return `${count} · ${shown}${more}`;
}

interface DataInspectorPanelProps {
  /** Whether the panel is expanded. */
  open: boolean;
  /** Toggle the expanded state. */
  onToggle: (open: boolean) => void;
  /**
   * Reads the input + resolved rows from the live view, or null when there is no
   * chart to inspect. Must be stable across renders (the read is memoized on
   * `renderEpoch`).
   */
  getData: () => InspectedData | null;
  /**
   * Refresh trigger: bumps whenever the data to show may have changed, so the open
   * table re-reads. The live-preview pane bumps it on each settled render *and* on
   * an interactive selection that changes the inspected rows (live mode, M5); the
   * builder bumps it on render only.
   */
  renderEpoch: number;
  /**
   * Explicit panel height (px) — the live-preview pane sets this from its
   * resizable divider so the table fills the allotted space. Omitted (the builder)
   * leaves the table at its default capped height.
   */
  heightPx?: number;
  /** id of the panel root, for a splitter's `aria-controls` to point at. */
  id?: string;
}

/**
 * The reusable disclosure: a toggle bar (APG disclosure — `aria-expanded`,
 * conditional content, matching the builder's source-rows preview) over an
 * Input | Resolved view switch and the chosen table. Expanded states, each named
 * (council: GOV.UK / NN/g — say what happened and the next step):
 * - no live chart → guidance to render one;
 * - the chosen view's table is empty → say which side and why (the resolved side's
 *   transforms produced nothing; the input side's source is empty);
 * - rows → the grid.
 */
export function DataInspectorPanel({
  open,
  onToggle,
  getData,
  renderEpoch,
  heightPx,
  id,
}: DataInspectorPanelProps) {
  const [view, setView] = useState<DataView>('resolved');
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // Read tables only while open; re-read when a render settles. `renderEpoch` is an
  // intentional refresh trigger — not read in the body (getData is stable and always
  // reads the latest view), so exhaustive-deps sees it as unnecessary.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const data = useMemo(() => (open ? getData() : null), [open, renderEpoch, getData]);
  const tables = data?.tables ?? null;
  // The chosen table, falling back to the first when the selection is stale (the
  // spec changed under it) or unset — so a re-render never lands on a missing table.
  const table = tables?.find((t) => t.id === selectedId) ?? tables?.[0] ?? null;
  const rows = table ? table[view] : null;

  const pickerId = `${id ?? 'preview'}-view-picker`;

  return (
    <div
      id={id}
      className={`${styles.inspector} ${heightPx !== undefined ? styles.fill : ''}`}
      style={heightPx !== undefined ? { height: heightPx } : undefined}
    >
      <div className={styles.bar}>
        <button
          type="button"
          className={styles.toggle}
          aria-expanded={open}
          onClick={() => onToggle(!open)}
        >
          <span className={styles.caret} aria-hidden="true">
            {open ? '▾' : '▸'}
          </span>
          Data
        </button>
        {open && (
          <>
            {tables && tables.length > 1 && table && (
              <SelectControl
                id={pickerId}
                label="Inspected view"
                value={table.id}
                onSelect={setSelectedId}
                options={tables.map<SelectControlOption<string>>((t) => ({
                  value: t.id,
                  label: t.label,
                  detail: tableDetail(t),
                }))}
              />
            )}
            <SegmentedControl
              label="Pipeline stage"
              options={VIEW_OPTIONS}
              value={view}
              onChange={setView}
            />
            {rows !== null && rows.length > 0 && (
              <span className={styles.meta}>
                {rows.length.toLocaleString()} {rows.length === 1 ? 'row' : 'rows'}
              </span>
            )}
          </>
        )}
      </div>

      {open &&
        (data === null ? (
          <p className={styles.stateNote}>Render a chart to inspect its data.</p>
        ) : table === null || rows === null ? (
          <p className={styles.stateNote}>This chart has no inspectable data.</p>
        ) : rows.length === 0 ? (
          <p className={styles.stateNote}>
            {view === 'resolved'
              ? 'No rows — the view’s filters or transforms left nothing to draw.'
              : 'The source data has no rows.'}
          </p>
        ) : (
          <DataTable
            columns={Object.keys(rows[0])}
            rows={rows.slice(0, ROW_LIMIT)}
            total={rows.length}
            ariaLabel="Data rows"
            fill={heightPx !== undefined}
          />
        ))}
    </div>
  );
}

interface DataInspectorProps {
  /** Reads the input + resolved rows from the live preview's view (stable). */
  getData: () => InspectedData | null;
  /** Bumps on each settled render so the open table refreshes. */
  renderEpoch: number;
  /** Panel height (px) from the preview pane's resizable divider (only when open). */
  heightPx?: number;
  /** id of the panel root, for the divider's `aria-controls`. */
  id?: string;
}

/** The live-preview data inspector: the panel bound to the persisted open state. */
export function DataInspector({ getData, renderEpoch, heightPx, id }: DataInspectorProps) {
  const open = useAppStore((s) => s.dataInspectorOpen);
  const setOpen = useAppStore((s) => s.setDataInspectorOpen);
  return (
    <DataInspectorPanel
      open={open}
      onToggle={setOpen}
      getData={getData}
      renderEpoch={renderEpoch}
      heightPx={heightPx}
      id={id}
    />
  );
}
