/**
 * Fields a spec introduces through its own transforms (docs/architecture/08 →
 * editor augmentation).
 *
 * Portable core: pure spec analysis, no data. The editor's field hints
 * (completion/hover/inlay) suggest the bound dataset's source columns *plus* the
 * fields the spec derives — so a column you just created with a `calculate` is
 * offered too. This collects the statically-named ones: every transform `as`
 * (`calculate`, `timeUnit`, `bin`, `stack`, `fold`, `flatten`, `regression`, …)
 * and the nested `as` of the op-list transforms (`aggregate`, `window`,
 * `joinaggregate`), scoped to the transforms in effect at a cursor — the enclosing
 * view's and its ancestors' (Vega-Lite applies a parent view's transforms upstream
 * of its children; a sibling view's do not apply).
 *
 * Out of scope by design: **data-dependent** derived columns — `pivot`'s
 * one-column-per-value output and `lookup`'s imported fields — which only exist
 * once the pipeline runs over the actual data. Naming those would mean executing
 * transforms, far beyond a static hint.
 */

/** Add an `as` value (a string, or the `[start, end]` / key-value pair array). */
function addAs(names: Set<string>, as: unknown): void {
  if (typeof as === 'string') names.add(as);
  else if (Array.isArray(as)) for (const a of as) if (typeof a === 'string') names.add(a);
}

/** Collect the field name(s) a single transform entry introduces. */
function collectFromTransform(transform: unknown, names: Set<string>): void {
  if (transform === null || typeof transform !== 'object') return;
  const t = transform as Record<string, unknown>;
  addAs(names, t.as);
  // `fold` defaults its output to ['key', 'value'] when `as` is omitted.
  if ('fold' in t && !('as' in t)) {
    names.add('key');
    names.add('value');
  }
  // Op-list transforms nest their `as` inside each operation.
  for (const key of ['aggregate', 'window', 'joinaggregate']) {
    const ops = t[key];
    if (Array.isArray(ops)) {
      for (const op of ops) {
        if (op !== null && typeof op === 'object') addAs(names, (op as Record<string, unknown>).as);
      }
    }
  }
}

/** Add every field name a node's own `transform` array introduces. */
function collectNodeTransforms(node: unknown, names: Set<string>): void {
  if (
    node !== null &&
    typeof node === 'object' &&
    Array.isArray((node as { transform?: unknown }).transform)
  ) {
    for (const t of (node as { transform: unknown[] }).transform) collectFromTransform(t, names);
  }
}

/**
 * The fields derived by transforms *in scope* at a JSON `path` into the spec — the
 * `as` outputs of every transform on the cursor's enclosing view and its ancestors
 * (docs/architecture/08 → editor augmentation). Vega-Lite applies a parent view's
 * transforms upstream of its children, so a field a parent's `calculate` creates is
 * visible to a nested view; sibling views' transforms are not. The walk descends
 * `path` (a jsonc-parser location path) defensively, collecting along the way.
 */
export function derivedFieldNamesAtPath(
  spec: unknown,
  path: ReadonlyArray<string | number>,
): string[] {
  const names = new Set<string>();
  let node: unknown = spec;
  collectNodeTransforms(node, names);
  for (const key of path) {
    if (node === null || typeof node !== 'object') break;
    node = (node as Record<string | number, unknown>)[key];
    collectNodeTransforms(node, names);
  }
  return [...names];
}
