/**
 * Inspectable drawn tables of a compiled chart (the data inspector — spec §04;
 * docs/architecture/05 → "the data inspector rides the boundary").
 *
 * Portable core: pure analysis of a **compiled Vega spec** (the byproduct
 * `vega-embed` already produces to render the chart — nothing is compiled here).
 * A multi-view Vega-Lite spec (layer/concat/facet/repeat) compiles to several
 * data tables, and the inspector lets the user pick which one to look at — "what
 * data am I actually visualizing?" — to debug in-spec transforms.
 *
 * What counts as inspectable is the set of tables the **marks actually draw**, read
 * from the compiled marks tree's `from.data` (and a facet cell's `from.facet.data`).
 * That is the honest answer to the question and it sidesteps a mapping that cannot
 * be made reliable: Vega-Lite *desugars* some marks into layers (e.g. a `line` with
 * `point: true` becomes two marks over two tables), so a compiled table cannot be
 * traced back to a single authored view. We therefore enumerate by drawn table, not
 * by authored view — one consequence being that a point-overlay shows as two nearly
 * identical tables (the line's and the point's), which is literally what is drawn.
 *
 * For each drawn table we report two ends, mirroring the inspector's Input |
 * Resolved toggle: `resolved` is the table the marks draw (after the view's
 * transforms), and `input` is its most-upstream source (before them), found by
 * following each table's `source` link — the documented, stable Vega dataflow
 * format.
 */

/** A drawn table the user can inspect, with the two ends of its pipeline. */
export interface InspectableView {
  /** The compiled dataset the marks draw — the post-transform "Resolved" rows. */
  resolved: string;
  /** The most-upstream source of `resolved` — the "Input" rows before transforms. */
  input: string;
}

interface VgData {
  name?: unknown;
  source?: unknown;
}

interface VgMark {
  from?: { data?: unknown; facet?: { data?: unknown } };
  marks?: unknown;
}

function isObject(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

/** Vega's compiler-generated table names (`source_0`, `data_2`) — jargon, never shown. */
function isCompilerName(name: string): boolean {
  return /^(source|data)_\d+$/.test(name);
}

/**
 * A user-facing label for an inspectable view: its input source's name when that
 * is user-authored (a named dataset like `sales`), else an ordinal `View N`
 * (1-based, by document order). Compiler-generated names never surface — they are
 * jargon to the user (council: NN/g #2 "speak the users' language";
 * docs/architecture/10). The richer recognition cue — columns + row count — is the
 * UI's `detail` line, built from the rows.
 */
export function inspectViewLabel(input: string, index: number): string {
  return isCompilerName(input) ? `View ${index + 1}` : input;
}

/** Facet layout helper tables (domains/headers), never a table the user draws into. */
function isLayoutHelper(name: string): boolean {
  return name.startsWith('facet_domain');
}

/** The table a mark draws, from `from.data` or a facet cell's `from.facet.data`. */
function drawnTable(mark: VgMark): string | null {
  const from = mark.from;
  if (!isObject(from)) return null;
  if (typeof from.data === 'string') return from.data;
  if (isObject(from.facet) && typeof from.facet.data === 'string') return from.facet.data;
  return null;
}

/**
 * The inspectable drawn tables of a compiled Vega spec, in document order, one per
 * distinct table the marks render. Returns `[]` for a spec with no drawable data
 * (or a malformed input).
 */
export function inspectableViews(vgSpec: unknown): InspectableView[] {
  if (!isObject(vgSpec)) return [];
  const dataList: VgData[] = Array.isArray(vgSpec.data) ? (vgSpec.data as VgData[]) : [];
  const sourceOf = new Map<string, string | undefined>();
  for (const d of dataList) {
    if (isObject(d) && typeof d.name === 'string') {
      sourceOf.set(d.name, typeof d.source === 'string' ? d.source : undefined);
    }
  }

  // The most-upstream ancestor of `name` via the `source` chain (cycle-guarded).
  const rootSourceOf = (name: string): string => {
    const seen = new Set<string>();
    let current = name;
    while (sourceOf.has(current) && !seen.has(current)) {
      const next = sourceOf.get(current);
      if (next === undefined) break;
      seen.add(current);
      current = next;
    }
    return current;
  };

  const views: InspectableView[] = [];
  const seen = new Set<string>();
  const visit = (node: unknown): void => {
    if (Array.isArray(node)) {
      for (const item of node) visit(item);
      return;
    }
    if (!isObject(node)) return;
    const mark = node as VgMark;
    const table = drawnTable(mark);
    if (table !== null && sourceOf.has(table) && !isLayoutHelper(table) && !seen.has(table)) {
      seen.add(table);
      views.push({ resolved: table, input: rootSourceOf(table) });
    }
    if (Array.isArray(mark.marks)) visit(mark.marks);
  };
  visit(vgSpec.marks);
  return views;
}
