/**
 * Snippet → dataset reference extraction and rename propagation
 * (spec §09F → Cross-entity relationships; docs/architecture/07 §3.1, §6).
 *
 * Portable core: no browser APIs, no React, no store access. A Vega-Lite spec
 * references a library dataset through *named* data — `{ "data": { "name": "X" } }`
 * — which can appear at the top level, per-layer, inside `spec`/`facet`/concat
 * children, or in a lookup transform's `from.data`. Rather than enumerate the
 * grammar, we walk the spec recursively and collect every library reference we
 * find — the single source of truth for "what does this spec reference", which the
 * renderer's resolution must agree with.
 *
 * What counts as a *library reference* is decided by `libraryRefName`
 * (core/spec-data): named data (a string `name` with no `values`/`url`/generator)
 * whose name the spec does not define for itself via a top-level `datasets` object.
 * A `name` riding on inline `values` or a `url` is a Vega-Lite label, not a
 * dependency, so it is left alone — keeping Astrolabe a strict extension of
 * Vega-Lite. The renderer resolves through the same predicate, so the two cannot
 * disagree.
 *
 * The walk recurses into every key EXCEPT two, which hold user data payloads
 * rather than nested specs: a `data` object (classified at the parent site; its
 * `values`/`format` are payload, never a nested ref) and a top-level `datasets`
 * map (the spec's own inline data). Pruning those is what keeps a data *row* that
 * happens to carry a field literally named `data: { name: "x" }` from being misread
 * as a library reference. The renderer's resolution prunes the same two keys.
 *
 * A spec may be stored as an **object** or as **JSON text** (see spec §09A); we
 * normalize once at the boundary (unparseable text → no refs / unchanged spec) so
 * the recursive walk never has to care.
 */

import { libraryRefName, selfDefinedNames } from './spec-data';

type Json = unknown;

/** Parse a string spec; an unparseable draft simply has no resolvable refs. */
function safeParse(s: string): Json {
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}

/**
 * Collects every **library** dataset name referenced anywhere in the spec
 * (`libraryRefName`), excluding names the spec defines for itself via a top-level
 * `datasets` object. Accepts an object or JSON text.
 */
export function extractDatasetRefs(spec: Json): string[] {
  const root = typeof spec === 'string' ? safeParse(spec) : spec;
  const selfDefined = selfDefinedNames(root);
  const names = new Set<string>();

  const walk = (node: Json): void => {
    if (Array.isArray(node)) {
      for (const item of node) walk(item);
      return;
    }
    if (node && typeof node === 'object') {
      const obj = node as Record<string, Json>;
      const refName = libraryRefName(obj.data, selfDefined);
      if (refName !== null) names.add(refName);
      // Recurse into every key except the two that hold data payloads (`data` —
      // classified above; `datasets` — the spec's own inline data). Pruning them
      // keeps the walk out of user data rows, where a field named `data` would
      // otherwise be misread as a reference. Kept in step with rendering.ts.
      for (const key of Object.keys(obj)) {
        if (key === 'data' || key === 'datasets') continue;
        walk(obj[key]);
      }
    }
  };

  walk(root);
  return [...names];
}

/** The list stored on `snippet.datasetRefs` — sorted + de-duped for stable diffs. */
export function recomputeDatasetRefs(spec: Json): string[] {
  return [...new Set(extractDatasetRefs(spec))].sort();
}

/**
 * Returns a copy of `spec` with every library reference to `oldName` (per
 * `libraryRefName`) renamed to `newName`, recursing through arrays and objects.
 * Native Vega-Lite data that merely carries the name as a label (inline `values`,
 * a `url`, a generator) and a self-defined top-level `datasets` key are left
 * untouched. The input's stored shape is preserved: a string spec is parsed,
 * rewritten, and re-serialized as pretty JSON text; an object spec returns an
 * object. The generic `<T>` reflects this shape preservation; the input is never
 * mutated.
 */
export function renameDatasetInSpec<T>(spec: T, oldName: string, newName: string): T {
  const isString = typeof spec === 'string';
  const root = isString ? safeParse(spec) : spec;
  // Unparseable text → return it unchanged (nothing resolvable to rewrite).
  if (isString && root === null) return spec;

  const selfDefined = selfDefinedNames(root);

  const rewrite = (node: Json): Json => {
    if (Array.isArray(node)) return node.map(rewrite);
    if (node && typeof node === 'object') {
      const out: Record<string, Json> = {};
      for (const [k, v] of Object.entries(node as Record<string, Json>)) {
        // A `data` block is a reference site, not a container: rename it when it is
        // a library reference to `oldName`, and never recurse into its payload. A
        // `datasets` map is the spec's own inline data: leave it whole. Pruning
        // both (mirroring extractDatasetRefs) keeps rename out of user data rows,
        // where a field named `data` would otherwise be rewritten as a reference.
        if (k === 'data') {
          out[k] =
            libraryRefName(v, selfDefined) === oldName
              ? { ...(v as Record<string, Json>), name: newName }
              : v;
        } else if (k === 'datasets') {
          out[k] = v;
        } else {
          out[k] = rewrite(v);
        }
      }
      return out;
    }
    return node;
  };

  const rewritten = rewrite(root);
  return (isString ? JSON.stringify(rewritten, null, 2) : rewritten) as T;
}
