/**
 * Parameter scaffolding for the editor (docs/architecture/08 → editor augmentation) —
 * the "quick parameter" affordance, sibling of `spec-transform-scaffold`. It offers
 * Vega-Lite parameters as ready-to-fill snippets: **variable** widgets (slider,
 * dropdown, radio, checkbox — bound via `bind`) and **selection** parameters (point,
 * interval — via `select`), each seeded from the data in scope (a slider's bounds from
 * the numeric field's extent, a point selection's field from a categorical column).
 * The facts the schema can't supply — *where each family legally attaches* and *what a
 * seeded skeleton looks like* — are pure core (`core/spec-params`); this is the Monaco
 * glue.
 *
 * **Two surfaces, mirroring `spec-transform-scaffold`:**
 *   - the **CodeLens** (`installSpecParamScaffoldCodeLens`, per editor) is the
 *     discoverable home. Because the two families attach to different homes — variable
 *     parameters are document-global (only legal in the root `params[]`), selections
 *     belong to the unit whose marks they read — the *kind* is chosen at the lens, and
 *     the lens is cursor-scoped by family: at the top level (or a single-view spec) the
 *     variable widgets show; inside a unit the selections show; a single-view spec, where
 *     the root *is* the unit, shows both on the one line. Clicking creates `params: []`
 *     with the entry, or appends to an existing array.
 *   - the **completion** (`configureSpecParamScaffold`, global-once) is the
 *     type-to-filter accelerator, offering both families in the root array and
 *     selections only in a nested unit's.
 *
 * Both are gated to the active draft. This is the home the schema leaves bare: the
 * schema completes the `params` key and its enum values, but never a seeded slider with
 * your column's real range or a `select` wired to the view.
 */

import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { appendEntryEdit, createArrayPropertyEdit } from '@core/spec-snippet';
import {
  PARAMS,
  type ParamFamily,
  type ParamHost,
  type ParamSite,
  paramPlacementAt,
  paramSiteAt,
} from '@core/spec-params';
import { boundParamFieldsAt } from './active-dataset';
import { installCursorLens } from './editor-cursor-lens';
import { insertSnippetAt, scaffoldSlotAt, scaffoldSuggestion } from './editor-snippet';
import { useSnippetStore } from '../stores/SnippetStore';

/** The kinds surfaced as CodeLens buttons — the common few per family; the completion has the rest. */
const COMMON_VARIABLE_IDS = ['slider', 'dropdown'] as const;
const COMMON_SELECTION_IDS = ['point', 'interval'] as const;

let registered = false;

/** Register the `params[]` scaffold completion provider once. */
export function configureSpecParamScaffold(): void {
  if (registered) return;
  registered = true;

  monaco.languages.registerCompletionItemProvider('json', {
    provideCompletionItems(model, position) {
      // Scaffolding only edits the draft; the published view is read-only.
      if (useSnippetStore.getState().editorView !== 'draft') return { suggestions: [] };

      const text = model.getValue();
      const offset = model.getOffsetAt(position);
      const placement = paramPlacementAt(text, offset);
      if (placement.kind !== 'slot') return { suggestions: [] };

      const slot = scaffoldSlotAt(model, position, text, offset);
      const fields = boundParamFieldsAt(text, offset);
      // The root array takes both families; a nested unit's takes selections only.
      const kinds = placement.atRoot ? PARAMS : PARAMS.filter((p) => p.family === 'selection');

      const suggestions = kinds.map((p, i) =>
        scaffoldSuggestion(slot, i, { label: p.label, detail: p.detail, body: p.param(fields) }),
      );
      return { suggestions };
    },
  });
}

/**
 * Add a parameter of `id`/`family` to its home — the root (variable) or the unit
 * (selection) at `anchorOffset`. Re-resolves the site from live text (the lens args may
 * be a version stale), then appends to an existing `params[]` or creates one with this
 * entry as its first element. Both go through Monaco's snippet engine so the entry's tab
 * stops are Tab-through.
 */
function runAddParam(
  editor: monaco.editor.IStandaloneCodeEditor,
  id: string,
  family: ParamFamily,
  anchorOffset: number,
): void {
  const model = editor.getModel();
  if (!model) return;
  const text = model.getValue();
  const site = paramSiteAt(text, anchorOffset + 1);
  const host = family === 'variable' ? site?.root : site?.unit;
  const kind = PARAMS.find((p) => p.id === id);
  if (!host || !kind) return;

  const entry = kind.param(boundParamFieldsAt(text, anchorOffset + 1));

  // Append to the existing `params[]`, or create one with this entry as its first
  // element — placement, indentation, and commas are the core edits' tested math.
  const edit = host.array
    ? appendEntryEdit(text, host.array, entry)
    : createArrayPropertyEdit(
        text,
        host.view.offset,
        model.getOptions().tabSize || 2,
        'params',
        entry,
      );
  insertSnippetAt(editor, edit.offset, edit.snippet);
}

/**
 * Install the cursor-aware parameter-scaffold CodeLens (per editor — its command needs
 * this editor's handle to apply the edit). Variable widgets show at the root unless the
 * cursor is inside a nested unit; selections show on the unit the cursor is in. Returns
 * a disposable; dispose on unmount.
 */
export function installSpecParamScaffoldCodeLens(
  editor: monaco.editor.IStandaloneCodeEditor,
): monaco.IDisposable {
  const addParamCmd = editor.addCommand(
    0,
    (_a, id: string, family: ParamFamily, anchorOffset: number) =>
      runAddParam(editor, id, family, anchorOffset),
  );

  return installCursorLens<ParamSite>(editor, {
    resolve: paramSiteAt,
    // Refresh when either home's presence or param count changes, not on every keystroke.
    keyOf: (site) =>
      JSON.stringify([
        site.root.view.offset,
        site.root.array?.count ?? -1,
        site.unit?.view.offset ?? -1,
        site.unit?.array?.count ?? -1,
      ]),
    lensesOf: (site, model, lens) => {
      // Anchor on the params array's line when it exists, else the object's opening line.
      const lineFor = (host: ParamHost) =>
        model.getPositionAt(host.array ? host.array.offset : host.view.offset).lineNumber;

      const lenses: monaco.languages.CodeLens[] = [];
      // Variable widgets are root-only; hide them when the cursor is in a nested unit,
      // where only selections are legal.
      const inNestedUnit = !!site.unit && site.unit.view.offset !== site.root.view.offset;
      if (!inNestedUnit) {
        const line = lineFor(site.root);
        for (const id of COMMON_VARIABLE_IDS) {
          lenses.push(
            lens(line, `$(add) ${id}`, addParamCmd, [id, 'variable', site.root.view.offset]),
          );
        }
      }
      if (site.unit) {
        const line = lineFor(site.unit);
        for (const id of COMMON_SELECTION_IDS) {
          lenses.push(
            lens(line, `$(add) ${id}`, addParamCmd, [id, 'selection', site.unit.view.offset]),
          );
        }
      }
      return lenses;
    },
  });
}
