/**
 * Dataset-aware editor hints (docs/architecture/08 → editor augmentation) — the
 * three things the Vega-Lite JSON schema *can't* know, because they depend on the
 * user's data and this spec:
 *
 *   - **Completion** — in a `field` / `groupby` value, the bound dataset's real
 *     column names (plus the spec's derived fields). The schema only knows `field`
 *     takes a string; it can't list your columns. Enum values (`type`, `mark`, …)
 *     are left to the schema — we add only what it lacks, no second source.
 *   - **Hover** — a column's inferred type + cardinality/range (from the stored
 *     profile); over a `calculate`/`filter`/`expr` string, a live validity check
 *     via core/expr-validate. Monaco merges these with the schema's own hovers.
 *   - **Inlay hints** — a faint `·<data-type>` beside each `field` (the column's
 *     raw type: number/string/date/boolean), annotation without touching the text.
 *     Deliberately the *data* type, not the encoding `type` — they share the line,
 *     so a `: quantitative` here would read as annotating the adjacent `"type"`,
 *     which the hint never describes.
 *
 * All three read the active draft's bound dataset (services/active-dataset) and
 * the cursor's JSON context (core/spec-cursor) at provide-time via `getState()` —
 * outside React. They register **once, globally for JSON** (like the schema and
 * formatter), not per editor. Suggestion-only: over- or under-listing is
 * harmless, which is why there is deliberately no "unknown field" diagnostic
 * (that would false-positive on every data-dependent derived column).
 */

import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { validateExpression } from '@core/expr-validate';
import { stringValueAtOffset, valueKeyAtOffset } from '@core/spec-cursor';
import { useSnippetStore } from '../stores/SnippetStore';
import {
  availableFieldsAt,
  dataInfoAt,
  fieldTypeAt,
  type DataInfo,
  type FieldHint,
} from './active-dataset';

/** Property values that reference a data field (where column names belong). */
const FIELD_KEYS = new Set(['field', 'groupby']);
/** Property values that hold a Vega expression (where the validity check fires). */
const EXPR_KEYS = new Set(['calculate', 'filter', 'expr']);

/** Markdown hover for a field hint: type + stats for source, a note for derived. */
function fieldHoverContents(hint: FieldHint, info: DataInfo): { value: string }[] {
  if (hint.derived || hint.type === null) {
    return [{ value: `**${hint.name}** · _derived by a transform_` }];
  }
  // The column's raw data type (number/string/date/boolean) — same vocabulary as
  // the inlay hint, not the Vega-Lite encoding `type` the user declares.
  const lines = [`**${hint.name}** · \`${hint.type}\``];
  const stat = info.columnStats.find((s) => s.name === hint.name);
  if (stat) {
    if (stat.numericExtent)
      lines.push(`Range ${stat.numericExtent.min} – ${stat.numericExtent.max}`);
    lines.push(`${stat.distinct}${stat.distinctCapped ? '+' : ''} distinct`);
  }
  lines.push(info.name ? `_from dataset “${info.name}”_` : '_from the spec’s inline data_');
  return lines.map((value) => ({ value }));
}

let registered = false;

/** Register the dataset-aware completion / hover / inlay providers once. */
export function configureSpecDatasetHints(): void {
  if (registered) return;
  registered = true;

  monaco.languages.registerCompletionItemProvider('json', {
    triggerCharacters: ['"'],
    provideCompletionItems(model, position) {
      // Field suggestions only edit the draft; nothing to offer on the published view.
      if (useSnippetStore.getState().editorView !== 'draft') return { suggestions: [] };
      const text = model.getValue();
      const offset = model.getOffsetAt(position);
      const key = valueKeyAtOffset(text, offset);
      if (key === null || !FIELD_KEYS.has(key)) return { suggestions: [] };
      const fields = availableFieldsAt(text, offset);
      if (fields.length === 0) return { suggestions: [] };

      const word = model.getWordUntilPosition(position);
      const range = new monaco.Range(
        position.lineNumber,
        word.startColumn,
        position.lineNumber,
        word.endColumn,
      );
      return {
        suggestions: fields.map((f) => ({
          label: f.name,
          kind: f.derived
            ? monaco.languages.CompletionItemKind.Variable
            : monaco.languages.CompletionItemKind.Field,
          detail: f.derived || f.type === null ? 'derived field' : f.type,
          insertText: f.name,
          range,
        })),
      };
    },
  });

  // All three providers annotate the draft only: they derive their field set from
  // the draft buffer (dataInfoAt), so gating to the draft view keeps the hints
  // consistent with the text they are computed from. The published view is a
  // read-only reference, where field hints are marginal.
  monaco.languages.registerHoverProvider('json', {
    provideHover(model, position) {
      if (useSnippetStore.getState().editorView !== 'draft') return null;
      const text = model.getValue();
      const offset = model.getOffsetAt(position);

      // Over an expression value: a live validity check.
      const key = valueKeyAtOffset(text, offset);
      if (key !== null && EXPR_KEYS.has(key)) {
        const expr = stringValueAtOffset(text, offset);
        if (expr !== null) {
          const result = validateExpression(expr);
          return {
            contents: [
              {
                value: result.valid
                  ? '✓ Valid Vega expression'
                  : `✗ ${result.error ?? 'Invalid expression'}`,
              },
            ],
          };
        }
      }

      // Over a field name: its type + stats, resolved at this view's data binding.
      const word = model.getWordAtPosition(position);
      if (word) {
        const info = dataInfoAt(text, offset);
        const hint = info.fields.find((f) => f.name === word.word);
        if (hint) {
          return {
            range: new monaco.Range(
              position.lineNumber,
              word.startColumn,
              position.lineNumber,
              word.endColumn,
            ),
            contents: fieldHoverContents(hint, info),
          };
        }
      }
      return null;
    },
  });

  monaco.languages.registerInlayHintsProvider('json', {
    provideInlayHints(model, range) {
      if (useSnippetStore.getState().editorView !== 'draft') return { hints: [], dispose() {} };
      const text = model.getValue();
      const hints: monaco.languages.InlayHint[] = [];
      for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
        // First `field` per line; the compact format keeps each encoding channel
        // (and its lone field) on its own line, so one match per line suffices.
        const match = /"field"\s*:\s*"([^"]+)"/.exec(model.getLineContent(line));
        if (!match) continue;
        // Resolve the type at this field's own data binding — views in a
        // composition can bind different datasets. The offset points inside the
        // field's value so the path resolves to its enclosing view.
        const valueCol = match.index + match[0].length - match[1].length;
        const offset = model.getOffsetAt({ lineNumber: line, column: valueCol });
        const type = fieldTypeAt(text, offset, match[1]);
        if (!type) continue; // unknown or derived (no type to annotate)
        hints.push({
          position: { lineNumber: line, column: match.index + match[0].length + 1 },
          // The column's raw data type (number/string/date/boolean), not the
          // Vega-Lite encoding `type`: a `: quantitative` here would read as an
          // annotation of the adjacent `"type"`, which the hint never describes.
          // The leading `·` marks it as a data fact about the field, not JSON syntax.
          label: `·${type}`,
          kind: monaco.languages.InlayHintKind.Type,
          paddingLeft: true,
        });
      }
      return { hints, dispose() {} };
    },
  });
}
