/**
 * code-editor — CodeMirror 6 runtime bundled for <code-ui>.
 *
 * Single file for the whole integration: CM core re-exports, the AdiaUI
 * base theme + highlight style, the per-language lazy-load map, and the
 * load-timeout helper. `code.js` does `import('./code-editor.js')` once
 * per element mount.
 *
 * Styling contract: `EditorView.theme()` can't emit CSS custom properties
 * (per CodeMirror docs), so the base theme below is structural-only.
 * Every color, padding, border, and syntax-token style lives in
 * `code.css` under `@scope (code-ui)`. See SPEC-CODE-EDITOR-001 §6.
 *
 * In Vite dev, the bare-specifier imports resolve through node_modules.
 * In production, `scripts/build-site.mjs` runs esbuild with
 * `splitting: true` on this file; each `languages[…]()` dynamic import
 * becomes its own chunk, lazy-loaded on first use.
 */

import {
  EditorState, EditorSelection, Compartment, StateEffect,
} from '@codemirror/state';
import {
  EditorView,
  lineNumbers, highlightActiveLine, highlightActiveLineGutter,
  placeholder, drawSelection, keymap,
} from '@codemirror/view';
import {
  defaultKeymap, history, historyKeymap, indentWithTab,
} from '@codemirror/commands';
import {
  HighlightStyle, syntaxHighlighting, LanguageSupport, defaultHighlightStyle,
} from '@codemirror/language';
import { linter, lintGutter } from '@codemirror/lint';
import { tags as t } from '@lezer/highlight';

// ── Base theme (structural only — colors live in code.css) ───────────

export const adiaBaseTheme = EditorView.theme({
  '&':           { fontFamily: 'inherit', fontSize: 'inherit', color: 'inherit', backgroundColor: 'transparent' },
  '.cm-content': { padding: '0', caretColor: 'inherit' },
  '.cm-focused': { outline: 'none' },
});

// ── Syntax highlight (class-based so CSS owns the colors) ────────────

export const adiaHighlightStyle = HighlightStyle.define([
  { tag: [t.comment, t.lineComment, t.blockComment, t.docComment],                 class: 'tok-comment' },
  { tag: [t.keyword, t.controlKeyword, t.modifier, t.operatorKeyword],             class: 'tok-keyword' },
  { tag: [t.string, t.character, t.regexp, t.escape, t.special(t.string)],         class: 'tok-string' },
  { tag: [t.number, t.integer, t.float],                                           class: 'tok-number' },
  { tag: [t.bool, t.null, t.atom],                                                 class: 'tok-boolean' },
  { tag: [t.operator, t.logicOperator, t.arithmeticOperator, t.compareOperator,
          t.updateOperator, t.definitionOperator],                                 class: 'tok-operator' },
  { tag: [t.punctuation, t.bracket, t.paren, t.brace, t.squareBracket,
          t.angleBracket, t.separator],                                            class: 'tok-punctuation' },
  { tag: [t.function(t.variableName), t.function(t.propertyName), t.macroName],    class: 'tok-function' },
  { tag: [t.variableName, t.local(t.variableName), t.self],                        class: 'tok-variable' },
  { tag: [t.typeName, t.className, t.namespace],                                   class: 'tok-type' },
  { tag: [t.propertyName, t.labelName, t.definition(t.variableName)],              class: 'tok-property' },
  { tag: [t.tagName, t.heading, t.contentSeparator],                               class: 'tok-tag' },
  { tag: [t.attributeName, t.attributeValue],                                      class: 'tok-attribute' },
  { tag: [t.url, t.link],                                                          class: 'tok-url' },
  { tag: [t.invalid, t.deleted],                                                   class: 'tok-invalid' },
]);

// ── Per-language lazy loaders ────────────────────────────────────────

export const languages = {
  json:       () => import('@codemirror/lang-json').then((m) => ({ extension: m.json() })),
  html:       () => import('@codemirror/lang-html').then((m) => ({ extension: m.html() })),
  javascript: () => import('@codemirror/lang-javascript').then((m) => ({ extension: m.javascript({ jsx: false, typescript: false }) })),
  css:        () => import('@codemirror/lang-css').then((m) => ({ extension: m.css() })),
  markdown:   () => import('@codemirror/lang-markdown').then((m) => ({ extension: m.markdown() })),
  yaml:       () => import('@codemirror/lang-yaml').then((m) => ({ extension: m.yaml() })),
};

// ── Load helper ──────────────────────────────────────────────────────

/** 10s ceiling on any single dynamic import. Per SPEC-CODE-EDITOR-001 §7.4. */
export const LOAD_TIMEOUT_MS = 10_000;

export function importWithTimeout(loader, label) {
  return Promise.race([
    loader(),
    new Promise((_, reject) =>
      setTimeout(
        () => reject(new Error(`code-editor: ${label} load timed out after ${LOAD_TIMEOUT_MS}ms`)),
        LOAD_TIMEOUT_MS,
      ),
    ),
  ]);
}

// ── Language-specific linters ────────────────────────────────────────
//
// Kept here (not in code.js) so code.js doesn't depend on the lint
// primitives directly. `code.js` picks the right linter by language
// name when editable mode is on. Phase 3 of SPEC-CODE-EDITOR-001 —
// JSON only for now; extend per-language as need arises.

/** JSON linter. Parses the full document; emits one error Diagnostic
 *  at the reported position on `SyntaxError`, else clean. The error
 *  position is parsed from the engine's message (V8 / Node ≥20 /
 *  Firefox / JavaScriptCore all shape these slightly differently) and
 *  falls back to the last character so a marker is always visible. */
export const jsonLinter = linter((view) => {
  const text = view.state.doc.toString();
  if (text.trim() === '') return [];
  try {
    JSON.parse(text);
    return [];
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    const { from, to } = extractJsonErrorPos(msg, text);
    return [{
      from, to,
      severity: 'error',
      message: msg.replace(/^SyntaxError:\s*/, ''),
      source: 'json',
    }];
  }
});

function extractJsonErrorPos(msg, text) {
  const docLen = text.length;
  // "at position N" — V8 / Chromium / pre-Node-20
  const posMatch = msg.match(/at position (\d+)/);
  if (posMatch) {
    const from = Math.min(docLen, Number(posMatch[1]));
    return { from, to: Math.min(docLen, from + 1) };
  }
  // "line N column M" — Firefox / Node ≥20
  const lineColMatch = msg.match(/line (\d+) column (\d+)/);
  if (lineColMatch) {
    const line = Number(lineColMatch[1]);
    const col = Number(lineColMatch[2]);
    let lineStart = 0;
    let currentLine = 1;
    for (let i = 0; i < text.length && currentLine < line; i++) {
      if (text[i] === '\n') { currentLine++; lineStart = i + 1; }
    }
    const from = Math.min(docLen, lineStart + Math.max(0, col - 1));
    return { from, to: Math.min(docLen, from + 1) };
  }
  // Fallback: point at the last character so the marker is visible.
  const end = Math.max(0, docLen);
  return { from: Math.max(0, end - 1), to: end };
}

// ── Re-exports (so code.js gets everything from one import) ─────────

export {
  EditorState, EditorSelection, Compartment, StateEffect,
  EditorView,
  lineNumbers, highlightActiveLine, highlightActiveLineGutter,
  placeholder, drawSelection, keymap,
  defaultKeymap, history, historyKeymap, indentWithTab,
  HighlightStyle, syntaxHighlighting, LanguageSupport, defaultHighlightStyle,
  linter, lintGutter,
};
