import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap, completionStatus } from '@codemirror/autocomplete';
import {
  defaultKeymap,
  history,
  historyField,
  historyKeymap,
  indentWithTab,
  redo,
  redoDepth,
  undo,
  undoDepth,
} from '@codemirror/commands';
import {
  bracketMatching,
  HighlightStyle,
  foldGutter,
  foldKeymap,
  indentOnInput,
  syntaxHighlighting,
} from '@codemirror/language';
import { highlightSelectionMatches, search, searchKeymap } from '@codemirror/search';
import {
  Annotation,
  Compartment,
  EditorState,
  Transaction,
  type Extension,
} from '@codemirror/state';
import {
  crosshairCursor,
  drawSelection,
  dropCursor,
  EditorView,
  highlightActiveLine,
  highlightActiveLineGutter,
  keymap,
  lineNumbers,
  rectangularSelection,
} from '@codemirror/view';
import { tags } from '@lezer/highlight';
import { latexCompletionSource } from './autocomplete';
import {
  clearLatexSearchHighlights,
  createLatexProjectFeatureExtensions,
  createLatexReviewExtension,
  setLatexReviewDecorations,
  setLatexSearchHighlights,
  type LatexNavigationTarget,
  type LatexProjectCompletionData,
  type LatexReviewDecoration,
} from './project-features';
import { bibtexLanguage } from './bibtex-language';
import { latexLanguage } from './latex-language';
import { createListingsExtension } from './listings';
import { loadProjectLanguageExtension, type ProjectEditorLanguageId } from './project-language';
import {
  createLatexToolbarController,
  type LatexToolbarController,
} from './toolbar';

const learnitiveSourceHighlightStyle = HighlightStyle.define([
  { tag: tags.keyword, color: 'var(--learnitive-syntax-keyword)', fontWeight: '600' },
  { tag: tags.tagName, color: 'var(--learnitive-syntax-command)' },
  { tag: tags.escape, color: 'var(--learnitive-syntax-command)' },
  { tag: tags.typeName, color: 'var(--learnitive-syntax-type)', fontWeight: '600' },
  { tag: tags.propertyName, color: 'var(--learnitive-syntax-property)' },
  { tag: tags.variableName, color: 'var(--learnitive-syntax-name)' },
  { tag: tags.string, color: 'var(--learnitive-syntax-string)' },
  { tag: tags.number, color: 'var(--learnitive-syntax-number)' },
  { tag: tags.comment, color: 'var(--learnitive-syntax-comment)', fontStyle: 'italic' },
  { tag: tags.operator, color: 'var(--learnitive-syntax-operator)' },
  { tag: tags.bracket, color: 'var(--learnitive-syntax-punctuation)' },
  { tag: tags.punctuation, color: 'var(--learnitive-syntax-punctuation)' },
  { tag: tags.invalid, color: 'var(--destructive-text)', textDecoration: 'underline wavy' },
]);

const canonicalDocumentUpdate = Annotation.define<boolean>();
const SOFT_WRAP_STATE = new WeakMap<EditorView, { compartment: Compartment; enabled: boolean }>();

export type LatexSourceTextEdit = { from: number; to: number; text: string };

export type LatexCodeMirrorPersistedState = {
  selection: { anchor: number; head: number };
  scrollTop: number;
  scrollLeft: number;
  history?: unknown;
};

export type LatexCodeMirrorLanguage = 'latex' | 'bibtex';

export type LatexCodeMirrorConfig = {
  doc: string;
  language?: LatexCodeMirrorLanguage;
  parent: HTMLElement;
  ariaLabel: string;
  softWrap?: boolean;
  showLineNumbers?: boolean;
  fontSize?: number;
  spellcheck?: boolean;
  getProjectCompletionData?: () => LatexProjectCompletionData | Promise<LatexProjectCompletionData>;
  onNavigate?: (target: LatexNavigationTarget) => void | Promise<void>;
  onPasteFile?: (file: File) => void | Promise<void>;
  onChanges?: (edits: readonly LatexSourceTextEdit[]) => void;
  onSave: () => void;
  onUndo: () => void;
  onRedo: () => void;
  onCursorChange?: (location: { offset: number; line: number; column: number }) => void;
  /**
   * Lightweight editor-transaction signal used by local agents such as AI
   * Suggest. This deliberately exposes no provider behavior: consumers decide
   * whether a transaction is worth processing and can cancel/debounce there.
   */
  onActivity?: (activity: {
    kind: 'text' | 'cursor' | 'selection' | 'undo-redo' | 'autocomplete-open' | 'autocomplete-close';
    selectionFrom: number;
    selectionTo: number;
  }) => void;
  onHistoryChange?: (state: { canUndo: boolean; canRedo: boolean }) => void;
  extensions?: readonly Extension[];
  nativeHistory?: boolean;
  restoredState?: LatexCodeMirrorPersistedState;
};

export type LatexCodeMirrorSession = {
  view: EditorView;
  toolbar: LatexToolbarController;
  setSoftWrap(enabled: boolean): void;
  setLineNumbers(enabled: boolean): void;
  setFontSize(size: number): void;
  setSpellcheck(enabled: boolean): void;
  setSearchHighlights(query: string, caseSensitive?: boolean): void;
  clearSearchHighlights(): void;
  applyCanonicalText(content: string): LatexSourceTextEdit | undefined;
  captureState(): LatexCodeMirrorPersistedState;
  canUndo(): boolean;
  canRedo(): boolean;
  undo(): boolean;
  redo(): boolean;
  destroy(): void;
};

export function computeMinimalSourceEdit(
  current: string,
  next: string,
): LatexSourceTextEdit | undefined {
  if (current === next) return undefined;
  let prefix = 0;
  const maxPrefix = Math.min(current.length, next.length);
  while (prefix < maxPrefix && current.charCodeAt(prefix) === next.charCodeAt(prefix)) prefix += 1;

  let currentSuffix = current.length;
  let nextSuffix = next.length;
  while (
    currentSuffix > prefix &&
    nextSuffix > prefix &&
    current.charCodeAt(currentSuffix - 1) === next.charCodeAt(nextSuffix - 1)
  ) {
    currentSuffix -= 1;
    nextSuffix -= 1;
  }
  return { from: prefix, to: currentSuffix, text: next.slice(prefix, nextSuffix) };
}

function collectDocumentEdits(update: {
  startState: EditorState;
  changes: { iterChanges: (f: (fromA: number, toA: number, fromB: number, toB: number, inserted: { toString(): string }) => void) => void };
}): LatexSourceTextEdit[] {
  const edits: LatexSourceTextEdit[] = [];
  update.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => {
    edits.push({ from: fromA, to: toA, text: inserted.toString() });
  });
  return edits;
}

export function setLatexCodeMirrorSoftWrap(view: EditorView, enabled: boolean): void {
  const state = SOFT_WRAP_STATE.get(view);
  if (!state || state.enabled === enabled) return;
  state.enabled = enabled;
  view.dispatch({ effects: state.compartment.reconfigure(enabled ? EditorView.lineWrapping : []) });
}

export function getLatexCodeMirrorSoftWrap(view: EditorView): boolean {
  return SOFT_WRAP_STATE.get(view)?.enabled ?? false;
}

export function applyCanonicalLatexCodeMirrorText(
  view: EditorView,
  content: string,
): LatexSourceTextEdit | undefined {
  const edit = computeMinimalSourceEdit(view.state.doc.toString(), content);
  if (!edit) return undefined;
  view.dispatch({
    changes: { from: edit.from, to: edit.to, insert: edit.text },
    annotations: [
      canonicalDocumentUpdate.of(true),
      Transaction.addToHistory.of(false),
    ],
  });
  return edit;
}

function clampPersistedSelection(
  persisted: LatexCodeMirrorPersistedState | undefined,
  docLength: number,
): { anchor: number; head: number } | undefined {
  if (!persisted) return undefined;
  return {
    anchor: Math.max(0, Math.min(persisted.selection.anchor, docLength)),
    head: Math.max(0, Math.min(persisted.selection.head, docLength)),
  };
}

export function createLatexCodeMirror(config: LatexCodeMirrorConfig): LatexCodeMirrorSession {
  const wrapCompartment = new Compartment();
  const lineNumbersCompartment = new Compartment();
  const fontSizeCompartment = new Compartment();
  const spellcheckCompartment = new Compartment();
  const toolbar = createLatexToolbarController();
  const softWrap = config.softWrap ?? true;
  const language = config.language ?? 'latex';
  let showLineNumbers = config.showLineNumbers ?? true;
  let fontSize = Math.max(10, Math.min(24, config.fontSize ?? 14));
  let spellcheck = config.spellcheck ?? true;
  const projectFeatures = createLatexProjectFeatureExtensions({
    getCompletionData: config.getProjectCompletionData,
    onNavigate: config.onNavigate,
    onPasteFile: config.onPasteFile,
  });
  const nativeHistory = config.nativeHistory ?? true;

  const emitHistoryState = (state: EditorState) => {
    if (!config.onHistoryChange) return;
    config.onHistoryChange({
      canUndo: nativeHistory ? undoDepth(state) > 0 : false,
      canRedo: nativeHistory ? redoDepth(state) > 0 : false,
    });
  };

  const updateListener = EditorView.updateListener.of((update) => {
    const isCanonicalUpdate = update.transactions.some((transaction) =>
      transaction.annotation(canonicalDocumentUpdate),
    );
    if (update.docChanged && !isCanonicalUpdate) {
      const edits = collectDocumentEdits(update);
      if (edits.length > 0) config.onChanges?.(edits);
    }
    if (!isCanonicalUpdate && config.onActivity) {
      const previousCompletion = completionStatus(update.startState);
      const currentCompletion = completionStatus(update.state);
      if (previousCompletion !== currentCompletion && (currentCompletion || previousCompletion)) {
        const selection = update.state.selection.main;
        config.onActivity({
          kind: currentCompletion ? 'autocomplete-open' : 'autocomplete-close',
          selectionFrom: selection.from,
          selectionTo: selection.to,
        });
      }
    }
    if (!isCanonicalUpdate && config.onActivity && (update.docChanged || update.selectionSet)) {
      const selection = update.state.selection.main;
      const undoRedo = update.transactions.some((transaction) =>
        transaction.isUserEvent('undo') || transaction.isUserEvent('redo'),
      );
      config.onActivity({
        kind: undoRedo
          ? 'undo-redo'
          : update.docChanged
            ? 'text'
            : selection.from === selection.to
              ? 'cursor'
              : 'selection',
        selectionFrom: selection.from,
        selectionTo: selection.to,
      });
    }
    if ((update.selectionSet || update.docChanged) && config.onCursorChange) {
      const offset = update.state.selection.main.head;
      const line = update.state.doc.lineAt(offset);
      config.onCursorChange({
        offset,
        line: line.number,
        column: offset - line.from + 1,
      });
    }
    if (update.docChanged) emitHistoryState(update.state);
  });

  const fallbackHistoryKeymap = nativeHistory
    ? []
    : [
        {
          key: 'Mod-z',
          preventDefault: true,
          run: () => {
            config.onUndo();
            return true;
          },
        },
        {
          key: 'Shift-Mod-z',
          preventDefault: true,
          run: () => {
            config.onRedo();
            return true;
          },
        },
        {
          key: 'Mod-y',
          preventDefault: true,
          run: () => {
            config.onRedo();
            return true;
          },
        },
      ];

  const commandKeymap = keymap.of([
    {
      key: 'Mod-s',
      preventDefault: true,
      run: () => {
        config.onSave();
        return true;
      },
    },
    ...fallbackHistoryKeymap,
    ...(nativeHistory ? historyKeymap : []),
    indentWithTab,
    ...closeBracketsKeymap,
    ...defaultKeymap,
    ...searchKeymap,
    ...foldKeymap,
    ...completionKeymap,
  ]);

  const extensions: Extension[] = [
    lineNumbersCompartment.of(showLineNumbers ? [lineNumbers(), highlightActiveLineGutter()] : []),
    foldGutter(),
    drawSelection(),
    dropCursor(),
    EditorState.allowMultipleSelections.of(true),
    EditorState.tabSize.of(2),
    indentOnInput(),
    bracketMatching(),
    closeBrackets(),
    rectangularSelection(),
    crosshairCursor(),
    highlightActiveLine(),
    highlightSelectionMatches(),
    search(),
    language === 'bibtex' ? bibtexLanguage : latexLanguage,
    // StreamLanguage only provides syntax tags. A highlighter extension is
    // required for CodeMirror to turn those tags into visible token spans.
    // This was previously missing, leaving .tex/.bib source visually plain.
    syntaxHighlighting(learnitiveSourceHighlightStyle),
    autocompletion({
      override: [
        latexCompletionSource,
        ...(projectFeatures.completionSource ? [projectFeatures.completionSource] : []),
      ],
      icons: false,
      activateOnTyping: true,
    }),
    createListingsExtension('latex'),
    wrapCompartment.of(softWrap ? EditorView.lineWrapping : []),
    toolbar.extension,
    ...(nativeHistory ? [history()] : []),
    ...projectFeatures.extensions,
    ...(config.extensions ?? []),
    commandKeymap,
    spellcheckCompartment.of(EditorView.contentAttributes.of({
      'aria-label': config.ariaLabel,
      spellcheck: spellcheck ? 'true' : 'false',
      autocapitalize: 'off',
    })),
    fontSizeCompartment.of(EditorView.theme({
      '.cm-content, .cm-gutterElement': { fontSize: `${fontSize}px` },
    })),
    EditorView.theme({
      '&': { height: '100%', backgroundColor: 'transparent' },
      '.cm-scroller': {
        overflow: 'auto',
        fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
        lineHeight: '1.5',
      },
      '.cm-content': { minHeight: '100%', padding: '16px 0' },
      '.cm-line': { padding: '0 8px' },
      '.cm-gutters': {
        backgroundColor: 'transparent',
        borderRight: '1px solid var(--border)',
      },
      '&.cm-focused': { outline: 'none' },
    }),
    updateListener,
  ];

  const persistedSelection = clampPersistedSelection(
    config.restoredState,
    config.doc.length,
  );
  let state: EditorState;
  if (nativeHistory && config.restoredState?.history !== undefined) {
    try {
      state = EditorState.fromJSON(
        {
          doc: config.doc,
          ...(persistedSelection
            ? {
                selection: {
                  ranges: [persistedSelection],
                  main: 0,
                },
              }
            : {}),
          history: config.restoredState.history,
        },
        { extensions },
        { history: historyField },
      );
    } catch {
      state = EditorState.create({
        doc: config.doc,
        extensions,
        ...(persistedSelection ? { selection: persistedSelection } : {}),
      });
    }
  } else {
    state = EditorState.create({
      doc: config.doc,
      extensions,
      ...(persistedSelection ? { selection: persistedSelection } : {}),
    });
  }

  const view = new EditorView({ state, parent: config.parent });
  SOFT_WRAP_STATE.set(view, { compartment: wrapCompartment, enabled: softWrap });
  if (config.restoredState) {
    view.scrollDOM.scrollTop = Math.max(0, config.restoredState.scrollTop);
    view.scrollDOM.scrollLeft = Math.max(0, config.restoredState.scrollLeft);
    view.requestMeasure();
  }
  emitHistoryState(view.state);

  const captureState = (): LatexCodeMirrorPersistedState => {
    const selection = view.state.selection.main;
    const persisted: LatexCodeMirrorPersistedState = {
      selection: { anchor: selection.anchor, head: selection.head },
      scrollTop: view.scrollDOM.scrollTop,
      scrollLeft: view.scrollDOM.scrollLeft,
    };
    if (nativeHistory) {
      persisted.history = view.state.toJSON({ history: historyField }).history;
    }
    return persisted;
  };

  return {
    view,
    toolbar,
    setSoftWrap(enabled) {
      setLatexCodeMirrorSoftWrap(view, enabled);
    },
    setLineNumbers(enabled) {
      if (showLineNumbers === enabled) return;
      showLineNumbers = enabled;
      view.dispatch({
        effects: lineNumbersCompartment.reconfigure(enabled ? [lineNumbers(), highlightActiveLineGutter()] : []),
      });
    },
    setFontSize(size) {
      const next = Math.max(10, Math.min(24, size));
      if (fontSize === next) return;
      fontSize = next;
      view.dispatch({
        effects: fontSizeCompartment.reconfigure(EditorView.theme({
          '.cm-content, .cm-gutterElement': { fontSize: `${next}px` },
        })),
      });
    },
    setSpellcheck(enabled) {
      if (spellcheck === enabled) return;
      spellcheck = enabled;
      view.dispatch({
        effects: spellcheckCompartment.reconfigure(EditorView.contentAttributes.of({
          'aria-label': config.ariaLabel,
          spellcheck: enabled ? 'true' : 'false',
          autocapitalize: 'off',
        })),
      });
    },
    setSearchHighlights(query, caseSensitive = false) {
      view.dispatch({ effects: setLatexSearchHighlights.of({ query, caseSensitive }) });
    },
    clearSearchHighlights() {
      view.dispatch({ effects: clearLatexSearchHighlights.of(null) });
    },
    applyCanonicalText(content) {
      return applyCanonicalLatexCodeMirrorText(view, content);
    },
    captureState,
    canUndo() {
      return nativeHistory ? undoDepth(view.state) > 0 : false;
    },
    canRedo() {
      return nativeHistory ? redoDepth(view.state) > 0 : false;
    },
    undo() {
      if (nativeHistory) return undo(view);
      config.onUndo();
      return true;
    },
    redo() {
      if (nativeHistory) return redo(view);
      config.onRedo();
      return true;
    },
    destroy() {
      SOFT_WRAP_STATE.delete(view);
      view.destroy();
    },
  };
}

export function getLatexCodeMirrorViewportCenterLocation(view: EditorView): {
  offset: number;
  line: number;
  column: number;
} {
  const rect = view.scrollDOM.getBoundingClientRect();
  const fallback = view.state.selection.main.head;
  const offset = view.posAtCoords({
    x: rect.left + Math.max(24, rect.width / 2),
    y: rect.top + Math.max(1, rect.height / 2),
  }) ?? fallback;
  const line = view.state.doc.lineAt(offset);
  return { offset, line: line.number, column: offset - line.from + 1 };
}

export function getLatexCodeMirrorCursor(view: EditorView): number {
  return view.state.selection.main.head;
}

export function getLatexCodeMirrorSelection(view: EditorView): { from: number; to: number; text: string } {
  const selection = view.state.selection.main;
  return {
    from: selection.from,
    to: selection.to,
    text: view.state.sliceDoc(selection.from, selection.to),
  };
}

export function getLatexCodeMirrorCursorRect(view: EditorView): DOMRect | undefined {
  const offset = view.state.selection.main.head;
  const coords = view.coordsAtPos(offset);
  if (!coords) return undefined;
  return new DOMRect(
    coords.left,
    coords.top,
    Math.max(1, coords.right - coords.left),
    Math.max(1, coords.bottom - coords.top),
  );
}

export function getLatexCodeMirrorLocation(view: EditorView): { offset: number; line: number; column: number } {
  const offset = view.state.selection.main.head;
  const line = view.state.doc.lineAt(offset);
  return { offset, line: line.number, column: offset - line.from + 1 };
}

function positionForLocation(view: EditorView, lineNumber: number, column = 1): number {
  const safeLineNumber = Math.max(1, Math.min(lineNumber, view.state.doc.lines));
  const line = view.state.doc.line(safeLineNumber);
  const safeColumnOffset = Math.max(0, Math.min(column - 1, line.length));
  return line.from + safeColumnOffset;
}

export function revealLatexCodeMirrorLocation(
  view: EditorView,
  lineNumber: number,
  column = 1,
): void {
  const position = positionForLocation(view, lineNumber, column);
  view.dispatch({ effects: EditorView.scrollIntoView(position, { y: 'center' }) });
}

export function selectLatexCodeMirrorOffset(
  view: EditorView,
  position: number,
  options: { focus?: boolean } = {},
): void {
  const safePosition = Math.max(0, Math.min(position, view.state.doc.length));
  view.dispatch({ selection: { anchor: safePosition }, scrollIntoView: true });
  if (options.focus ?? true) view.focus();
}

export function selectLatexCodeMirrorLocation(
  view: EditorView,
  lineNumber: number,
  column = 1,
  options: { focus?: boolean } = {},
): void {
  selectLatexCodeMirrorOffset(view, positionForLocation(view, lineNumber, column), options);
}

export type ProjectCodeMirrorConfig = {
  doc: string;
  parent: HTMLElement;
  ariaLabel: string;
  softWrap?: boolean;
  showLineNumbers?: boolean;
  fontSize?: number;
  spellcheck?: boolean;
  onChange: (text: string) => void;
  onChanges?: (edits: readonly LatexSourceTextEdit[]) => void;
  onSave: () => void;
  onCompile?: () => void;
  onCursorChange?: (location: { offset: number; line: number; column: number }) => void;
  onHistoryChange?: (state: { canUndo: boolean; canRedo: boolean }) => void;
  onOpenReviewThread?: (threadId: string) => void;
  language?: ProjectEditorLanguageId;
};

export type ProjectCodeMirrorSession = {
  view: EditorView;
  applyExternalText(text: string): void;
  setSoftWrap(enabled: boolean): void;
  setLineNumbers(enabled: boolean): void;
  setFontSize(size: number): void;
  setSpellcheck(enabled: boolean): void;
  setReviewDecorations(decorations: readonly LatexReviewDecoration[]): void;
  canUndo(): boolean;
  canRedo(): boolean;
  undo(): boolean;
  redo(): boolean;
  focus(): void;
  destroy(): void;
};

/**
 * Lightweight CodeMirror surface for scientific/code/data text files. It
 * deliberately omits LaTeX completion, MathLive and project-LaTeX plugins.
 * Language parsers are selected from the file path and lazy-loaded so code
 * highlighting does not inflate the application's initial bundle.
 */
export function createProjectCodeMirror(
  config: ProjectCodeMirrorConfig,
): ProjectCodeMirrorSession {
  const externalUpdate = Annotation.define<boolean>();
  const languageCompartment = new Compartment();
  const wrapCompartment = new Compartment();
  const lineNumbersCompartment = new Compartment();
  const fontSizeCompartment = new Compartment();
  const spellcheckCompartment = new Compartment();
  const softWrap = config.softWrap ?? true;
  let showLineNumbers = config.showLineNumbers ?? true;
  let fontSize = Math.max(10, Math.min(24, config.fontSize ?? 14));
  let spellcheck = config.spellcheck ?? false;

  const emitHistoryState = (state: EditorState) => {
    config.onHistoryChange?.({
      canUndo: undoDepth(state) > 0,
      canRedo: redoDepth(state) > 0,
    });
  };

  const updateListener = EditorView.updateListener.of((update) => {
    const external = update.transactions.some((transaction) =>
      transaction.annotation(externalUpdate),
    );
    if (update.docChanged && !external) {
      config.onChanges?.(collectDocumentEdits(update));
      config.onChange(update.state.doc.toString());
    }
    if ((update.selectionSet || update.docChanged) && config.onCursorChange) {
      const offset = update.state.selection.main.head;
      const line = update.state.doc.lineAt(offset);
      config.onCursorChange({
        offset,
        line: line.number,
        column: offset - line.from + 1,
      });
    }
    if (update.docChanged) emitHistoryState(update.state);
  });

  const extensions: Extension[] = [
    lineNumbersCompartment.of(showLineNumbers ? [lineNumbers(), highlightActiveLineGutter()] : []),
    drawSelection(),
    dropCursor(),
    EditorState.tabSize.of(2),
    bracketMatching(),
    closeBrackets(),
    rectangularSelection(),
    highlightActiveLine(),
    highlightSelectionMatches(),
    search(),
    history(),
    keymap.of([
      {
        key: 'Mod-s',
        preventDefault: true,
        run: () => {
          config.onSave();
          return true;
        },
      },
      ...(config.onCompile ? [{
        key: 'Mod-Enter',
        preventDefault: true,
        run: () => {
          config.onCompile?.();
          return true;
        },
      }] : []),
      indentWithTab,
      ...closeBracketsKeymap,
      ...defaultKeymap,
      ...historyKeymap,
      ...searchKeymap,
    ]),
    syntaxHighlighting(learnitiveSourceHighlightStyle),
    languageCompartment.of([]),
    wrapCompartment.of(softWrap ? EditorView.lineWrapping : []),
    spellcheckCompartment.of(EditorView.contentAttributes.of({
      'aria-label': config.ariaLabel,
      spellcheck: spellcheck ? 'true' : 'false',
      autocapitalize: 'off',
    })),
    fontSizeCompartment.of(EditorView.theme({
      '.cm-content, .cm-gutterElement': { fontSize: `${fontSize}px` },
    })),
    ...(config.onOpenReviewThread ? [createLatexReviewExtension(config.onOpenReviewThread)] : []),
    EditorView.theme({
      '&': { height: '100%', backgroundColor: 'transparent' },
      '.cm-scroller': {
        overflow: 'auto',
        fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
        lineHeight: '1.5',
      },
      '.cm-content': { minHeight: '100%', padding: '12px 0' },
      '.cm-line': { padding: '0 8px' },
      '.cm-gutters': {
        backgroundColor: 'transparent',
        borderRight: '1px solid var(--border)',
      },
      '&.cm-focused': { outline: 'none' },
    }),
    updateListener,
  ];
  const state = EditorState.create({ doc: config.doc, extensions });
  const view = new EditorView({ state, parent: config.parent });
  SOFT_WRAP_STATE.set(view, { compartment: wrapCompartment, enabled: softWrap });
  emitHistoryState(view.state);
  config.onCursorChange?.({ offset: 0, line: 1, column: 1 });

  let alive = true;
  if (config.language && config.language !== 'plain') {
    void loadProjectLanguageExtension(config.language).then((extension) => {
      if (!alive) return;
      view.dispatch({ effects: languageCompartment.reconfigure(extension) });
    }).catch(() => {
      // Plain text is the safe fallback when a lazy language chunk cannot load.
    });
  }
  return {
    view,
    applyExternalText(text) {
      if (view.state.doc.toString() === text) return;
      view.dispatch({
        changes: { from: 0, to: view.state.doc.length, insert: text },
        annotations: [externalUpdate.of(true), Transaction.addToHistory.of(false)],
      });
    },
    setSoftWrap(enabled) {
      setLatexCodeMirrorSoftWrap(view, enabled);
    },
    setLineNumbers(enabled) {
      if (showLineNumbers === enabled) return;
      showLineNumbers = enabled;
      view.dispatch({
        effects: lineNumbersCompartment.reconfigure(enabled ? [lineNumbers(), highlightActiveLineGutter()] : []),
      });
    },
    setFontSize(size) {
      const next = Math.max(10, Math.min(24, size));
      if (fontSize === next) return;
      fontSize = next;
      view.dispatch({
        effects: fontSizeCompartment.reconfigure(EditorView.theme({
          '.cm-content, .cm-gutterElement': { fontSize: `${next}px` },
        })),
      });
    },
    setSpellcheck(enabled) {
      if (spellcheck === enabled) return;
      spellcheck = enabled;
      view.dispatch({
        effects: spellcheckCompartment.reconfigure(EditorView.contentAttributes.of({
          'aria-label': config.ariaLabel,
          spellcheck: enabled ? 'true' : 'false',
          autocapitalize: 'off',
        })),
      });
    },
    setReviewDecorations(decorations) {
      if (!config.onOpenReviewThread) return;
      view.dispatch({ effects: setLatexReviewDecorations.of(decorations) });
    },
    canUndo() { return undoDepth(view.state) > 0; },
    canRedo() { return redoDepth(view.state) > 0; },
    undo() { return undo(view); },
    redo() { return redo(view); },
    focus() { view.focus(); },
    destroy() {
      alive = false;
      SOFT_WRAP_STATE.delete(view);
      view.destroy();
    },
  };
}