/**
 * Cursor → enclosing-view mapping (docs/architecture/08 → editor augmentation).
 *
 * Portable core: no browser APIs, no React, no Monaco — just `jsonc-parser`'s
 * error-tolerant scanner (so this keeps working while the draft is briefly
 * unparseable mid-edit). Given the spec text and a cursor offset, it finds the
 * range of the *view* the cursor sits in, so a structural transform can act on
 * "the element I'm focused on" rather than the whole document.
 *
 * A **view** is a spec object you can meaningfully wrap in a composition: an
 * element of a `layer`/`hconcat`/`vconcat`/`concat` array, or the `spec` child of
 * a facet/repeat. Encoding blocks, `data`, a single `field` — the other objects
 * the cursor might land in — are not views and are skipped; the walk climbs to
 * the nearest enclosing one. When the cursor is in a top-level unit spec (no
 * nesting), there is no inner view and the result is null — the caller falls back
 * to the whole document, which is the right target for a flat spec.
 *
 * Only the byte range is returned; turning it into a Monaco range is the editor
 * integration's job (app/services/spec-transform-actions).
 */

import { findNodeAtOffset, getLocation, parseTree, type Node } from 'jsonc-parser';
import { ARRAY_COMPOSITIONS } from './spec-transforms';

/** A byte range into the spec text. */
interface NodeRange {
  offset: number;
  length: number;
}

/** The property key a node sits under, when it is a property's value. */
function propertyKey(node: Node | undefined): string | undefined {
  if (node?.type === 'property') {
    const key = node.children?.[0];
    return typeof key?.value === 'string' ? key.value : undefined;
  }
  return undefined;
}

/**
 * Is this object node a view — an element of a composition array, or a facet /
 * repeat `spec` child? (The object's parent is the array/property that frames it.)
 */
function isViewObject(node: Node): boolean {
  const parent = node.parent;
  if (!parent) return false; // the root object: not an *inner* view
  if (parent.type === 'property') return propertyKey(parent) === 'spec';
  if (parent.type === 'array') {
    const key = propertyKey(parent.parent);
    return key !== undefined && ARRAY_COMPOSITIONS.includes(key);
  }
  return false;
}

/**
 * The range of the nested view enclosing `offset`, or null when the cursor is not
 * inside one (a flat unit spec, or whitespace between top-level keys) — the
 * caller then targets the whole document.
 */
export function findViewRange(text: string, offset: number): NodeRange | null {
  const tree = parseTree(text);
  if (!tree) return null;
  let node: Node | undefined = findNodeAtOffset(tree, offset);
  while (node) {
    if (node.type === 'object' && isViewObject(node)) {
      return { offset: node.offset, length: node.length };
    }
    node = node.parent;
  }
  return null;
}

/**
 * The JSON path (object keys and array indices, root → cursor) of the node at
 * `offset`. Drives the view-scoped data context: the path is walked to find the
 * nearest enclosing view's data binding and ancestor transforms
 * (`core/spec-data`, `core/spec-fields`). Error-tolerant, so it works mid-edit.
 */
export function pathAtOffset(text: string, offset: number): (string | number)[] {
  return [...getLocation(text, offset).path];
}

/**
 * The property key whose *value* the cursor sits in, or null when the cursor is
 * on a key, at the top level, or otherwise not in a value. For an array element
 * the key is the array's property (so a `groupby: ["a", "b|"]` element reports
 * `groupby`). Drives the field-position field hints — "am I completing a
 * `field`?". Error-tolerant, so it works mid-edit.
 */
export function valueKeyAtOffset(text: string, offset: number): string | null {
  const { path, isAtPropertyKey } = getLocation(text, offset);
  if (isAtPropertyKey || path.length === 0) return null;
  const last = path[path.length - 1];
  // An array element reports the array's own key (e.g. groupby[i] → "groupby").
  const key = typeof last === 'number' ? path[path.length - 2] : last;
  return typeof key === 'string' ? key : null;
}

/** The string literal the cursor is inside, or null when it is not on a string. */
export function stringValueAtOffset(text: string, offset: number): string | null {
  const tree = parseTree(text);
  if (!tree) return null;
  const node = findNodeAtOffset(tree, offset);
  return node?.type === 'string' && typeof node.value === 'string' ? node.value : null;
}
