/**
 * The cursor-aware CodeLens skeleton shared by the editor's structural surfaces
 * (docs/architecture/08 → editor augmentation). A "cursor lens" follows the view the
 * cursor sits in — recomputing only when the *enclosing* thing changes, not on every
 * keystroke — and is installed **per editor** because its lens commands need the
 * editor handle to apply the edit. The composition CodeLens (`spec-transform-actions`),
 * the data-transform scaffold (`spec-transform-scaffold`), and the parameter scaffold
 * (`spec-param-scaffold`) are all this one shape; only three things vary between them:
 *   - `resolve(text, offset)` — the site the cursor is in (or null when in none);
 *   - `keyOf(site)` — a string that changes exactly when the lenses should refresh
 *     (so typing *within* the resolved site doesn't churn them);
 *   - `lensesOf(site, model, lens)` — the lenses for that site, built with the shared
 *     `lens` factory and closing over the caller's editor commands.
 *
 * The draft gate (scaffolding only edits the active draft; the published view is a
 * read-only reference) and the position guard live here too, since every consumer
 * shares them. App-layer glue — imports `monaco` and reads `useSnippetStore`.
 */

import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { useSnippetStore } from '../stores/SnippetStore';

/** Builds a zero-width CodeLens at the start of `line` invoking command `id` with `args`. */
export type LensFactory = (
  line: number,
  title: string,
  id: string | null,
  args: unknown[],
) => monaco.languages.CodeLens;

/** What varies between one cursor-lens consumer and another. */
export interface CursorLensSpec<Site> {
  /** The site the cursor is in, or null when it is in none. */
  resolve(text: string, offset: number): Site | null;
  /** A key that changes exactly when the lenses should refresh. */
  keyOf(site: Site): string;
  /** The lenses for `site`, built with `lens` (which closes over the caller's commands). */
  lensesOf(
    site: Site,
    model: monaco.editor.ITextModel,
    lens: LensFactory,
  ): monaco.languages.CodeLens[];
}

const NO_LENSES: monaco.languages.CodeLensList = { lenses: [], dispose() {} };

/** True when the active draft is being edited (the only place scaffolding acts). */
function onDraft(): boolean {
  return useSnippetStore.getState().editorView === 'draft';
}

/**
 * Install a cursor-aware CodeLens provider on `editor`, disposed with it. The provider
 * refreshes when `keyOf(resolve(...))` changes under the cursor; between refreshes
 * Monaco reuses the last lenses. Returns a disposable that tears down the cursor
 * subscription, the refresh emitter, and the provider registration together.
 */
export function installCursorLens<Site>(
  editor: monaco.editor.IStandaloneCodeEditor,
  spec: CursorLensSpec<Site>,
): monaco.IDisposable {
  const lens: LensFactory = (line, title, id, args) => ({
    range: new monaco.Range(line, 1, line, 1),
    command: { id: id ?? '', title, arguments: args },
  });

  /** Resolve the site under the cursor, gated to the draft. */
  const siteUnderCursor = (model: monaco.editor.ITextModel | null): Site | null => {
    const pos = editor.getPosition();
    if (!model || !pos || !onDraft()) return null;
    return spec.resolve(model.getValue(), model.getOffsetAt(pos));
  };

  const onDidChange = new monaco.Emitter<monaco.languages.CodeLensProvider>();
  const provider: monaco.languages.CodeLensProvider = {
    onDidChange: onDidChange.event,
    provideCodeLenses(model) {
      const site = siteUnderCursor(model);
      if (!site) return NO_LENSES;
      return { lenses: spec.lensesOf(site, model, lens), dispose() {} };
    },
  };
  const registration = monaco.languages.registerCodeLensProvider('json', provider);

  let lastKey = '';
  const cursorSub = editor.onDidChangeCursorPosition(() => {
    const site = siteUnderCursor(editor.getModel());
    const key = site ? spec.keyOf(site) : '';
    if (key !== lastKey) {
      lastKey = key;
      onDidChange.fire(provider);
    }
  });

  return {
    dispose() {
      cursorSub.dispose();
      onDidChange.dispose();
      registration.dispose();
    },
  };
}
