/**
 * Spec Editor — the center pane (spec §03).
 *
 * A Monaco JSON editor bound to the active snippet, running uncontrolled (raw
 * `monaco-editor`, per docs/architecture/08): created once, keystrokes push into
 * the store's `draftText` buffer. The buffer is reloaded only when the store
 * signals a programmatic load (`bufferEpoch`) or the view toggles — never
 * keystroke-by-keystroke, which would fight the cursor.
 *
 * The pane header carries the Draft/Published toggle plus Publish and Revert
 * (spec §03D). The published view is read-only — it shows the last published
 * spec for reference; all editing happens on the draft. Render problems surface
 * inline near the editor (spec §03E), mirroring the preview via PreviewStore.
 */

import { useEffect, useRef } from 'react';
// `edcore.main` is the full standalone editor — every feature contribution
// (folding, suggest widget, word operations like Cmd+Backspace, find, bracket
// colorization, multi-cursor, …) — but WITHOUT the `monaco-editor` barrel's
// basic-languages (sql, abap, solidity, …) we never use. We then add only the
// JSON language service. Full editor UX, JSON-only weight.
import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import '../infrastructure/monaco-env'; // side-effect: wire workers before create
import { configureVegaLiteJson } from '../infrastructure/monaco-schema';
import { openModal } from '../modals/ModalCoordinator';
import { useAppStore } from '../stores/AppStore';
import { confirm } from '../stores/ConfirmStore';
import { hasInlineData } from '../stores/ExtractStore';
import { publishActiveSnippet } from '../services/snippet-actions';
import { notify } from '../stores/NotificationStore';
import { usePreviewStore } from '../stores/PreviewStore';
import { selectActiveSnippet, selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import {
  NumberControl,
  RangeControl,
  ResetButton,
  SettingFooter,
  SettingRow,
  SettingsPopover,
} from './SettingsPopover';
import type { EditorView } from '../stores/SnippetStore';
import styles from './SpecEditor.module.css';

/** The two editor views as a single-select set (spec §03D). */
const VIEW_OPTIONS: ReadonlyArray<SegmentedOption<EditorView>> = [
  { value: 'draft', label: 'Draft' },
  { value: 'published', label: 'Published' },
];

/** Editor syntax theme: Auto follows the app theme; overrides force one (spec §07). */
const EDITOR_THEME_OPTIONS: ReadonlyArray<SegmentedOption<string>> = [
  { value: 'auto', label: 'Auto' },
  { value: 'light', label: 'Light' },
  { value: 'dark', label: 'Dark' },
];

const ONOFF_OPTIONS: ReadonlyArray<SegmentedOption<'on' | 'off'>> = [
  { value: 'on', label: 'On' },
  { value: 'off', label: 'Off' },
];

/**
 * Editor settings cluster (spec §07 → Editor), disclosed from the editor toolbar
 * and applied live. The id matches the Cmd/Ctrl+, shortcut target (App.tsx).
 */
function EditorSettings() {
  const editor = useUserSettingsStore((s) => s.saved.editor);
  const setEditor = useUserSettingsStore((s) => s.setEditor);
  const resetEditor = useUserSettingsStore((s) => s.resetEditor);

  return (
    <SettingsPopover id="editor-settings" label="Editor settings" title="Editor">
      <SettingRow label="Font size" htmlFor="set-fontsize">
        <RangeControl
          id="set-fontsize"
          min={10}
          max={18}
          value={editor.fontSize}
          suffix="px"
          onChange={(fontSize) => setEditor({ fontSize })}
        />
      </SettingRow>
      <SettingRow label="Theme">
        <SegmentedControl
          label="Editor theme"
          options={EDITOR_THEME_OPTIONS}
          value={editor.theme}
          onChange={(theme) => setEditor({ theme })}
        />
      </SettingRow>
      <SettingRow label="Minimap">
        <SegmentedControl
          label="Minimap"
          options={ONOFF_OPTIONS}
          value={editor.minimap ? 'on' : 'off'}
          onChange={(v) => setEditor({ minimap: v === 'on' })}
        />
      </SettingRow>
      <SettingRow label="Word wrap">
        <SegmentedControl
          label="Word wrap"
          options={ONOFF_OPTIONS}
          value={editor.wordWrap}
          onChange={(wordWrap) => setEditor({ wordWrap })}
        />
      </SettingRow>
      <SettingRow label="Line numbers">
        <SegmentedControl
          label="Line numbers"
          options={ONOFF_OPTIONS}
          value={editor.lineNumbers}
          onChange={(lineNumbers) => setEditor({ lineNumbers })}
        />
      </SettingRow>
      <SettingRow label="Tab size" htmlFor="set-tabsize">
        <NumberControl
          id="set-tabsize"
          min={1}
          max={8}
          value={editor.tabSize}
          onChange={(tabSize) => setEditor({ tabSize })}
        />
      </SettingRow>
      <SettingFooter>
        <ResetButton onClick={resetEditor}>Reset editor defaults</ResetButton>
      </SettingFooter>
    </SettingsPopover>
  );
}

// Register the bundled Vega-Lite schema once: resolves `$schema` locally (no
// network warning) and powers validation, autocomplete, and hover docs.
configureVegaLiteJson();

function EditorToolbar() {
  const activeId = useSnippetStore((s) => s.activeSnippetId);
  const editorView = useSnippetStore((s) => s.editorView);
  const setEditorView = useSnippetStore((s) => s.setEditorView);
  // Are there draft changes to revert? Use the live buffer in the draft view so
  // the control responds before the auto-save debounce commits (spec §03D).
  const dirty = useSnippetStore((s) => {
    const active = selectActiveSnippet(s);
    if (!active) return false;
    const draft = s.editorView === 'draft' ? s.draftText : active.draftSpec;
    return draft !== active.spec;
  });
  // Offer Extract only when the live draft carries top-level inline data to lift
  // out (spec §03F → hidden when the spec has no inline data).
  const canExtract = useSnippetStore(
    (s) => s.activeSnippetId !== null && hasInlineData(s.draftText),
  );

  // Publish + its success toast live in one place (services/snippet-actions) so
  // the button and the Cmd/Ctrl+S shortcut (EventRouter) behave identically.
  const handlePublish = publishActiveSnippet;

  const handleRevert = async () => {
    const ok = await confirm({
      title: 'Revert draft',
      message:
        'Discard all draft changes and restore the last published version? This cannot be undone.',
      confirmLabel: 'Revert',
      danger: true,
    });
    if (ok) {
      useSnippetStore.getState().revert();
      notify({
        kind: 'success',
        title: 'Draft reverted',
        message: 'The editor was restored to the last published version.',
      });
    }
  };

  return (
    <div className={styles.toolbar}>
      <SegmentedControl
        label="Editor view"
        options={VIEW_OPTIONS}
        value={editorView}
        onChange={setEditorView}
      />

      <span className={styles.spacer} />

      {canExtract && (
        <button
          type="button"
          className={styles.action}
          onClick={() => openModal('extract')}
          title="Extract inline data into a reusable dataset"
        >
          Extract to Dataset
        </button>
      )}
      <button
        type="button"
        className={styles.action}
        onClick={() => void handleRevert()}
        disabled={activeId === null || !dirty}
      >
        Revert
      </button>
      <button
        type="button"
        className={`${styles.action} ${styles.publish}`}
        onClick={handlePublish}
        disabled={activeId === null}
        title="Publish (⌘/Ctrl+S)"
        aria-keyshortcuts="Meta+S Control+S"
      >
        Publish
      </button>
      <EditorSettings />
    </div>
  );
}

export function SpecEditor() {
  const hostRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
  const activeId = useSnippetStore((s) => s.activeSnippetId);
  const editorView = useSnippetStore((s) => s.editorView);
  const bufferEpoch = useSnippetStore((s) => s.bufferEpoch);
  const uiTheme = useAppStore((s) => s.uiTheme);
  const error = usePreviewStore((s) => s.error);
  // Editor preferences (spec §07 → Editor); applied live below as they change.
  const editorPrefs = useUserSettingsStore((s) => s.saved.editor);

  // Create the editor once, on mount.
  useEffect(() => {
    if (!hostRef.current) return;
    // Seed from the user's current editor settings so the first paint matches.
    const ed = useUserSettingsStore.getState().saved.editor;
    const editor = monaco.editor.create(hostRef.current, {
      value: selectShownText(useSnippetStore.getState()),
      language: 'json',
      automaticLayout: true,
      minimap: { enabled: ed.minimap },
      // The editor is a Plex Mono surface per the design language (doc §3.1).
      // Monaco needs an explicit family string — it can't read the CSS token.
      fontFamily: "'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace",
      fontSize: ed.fontSize,
      tabSize: ed.tabSize,
      wordWrap: ed.wordWrap,
      lineNumbers: ed.lineNumbers,
      folding: true,
      showFoldingControls: 'always', // keep fold arrows visible, not only on hover
      scrollBeyondLastLine: false,
      // Vega-Lite enum values ("bar", "quantitative", …) live inside JSON
      // strings, where Monaco disables auto-suggest by default — turn it on so
      // those keywords are hinted as you type, not just on Ctrl+Space.
      quickSuggestions: { other: true, comments: false, strings: true },
      suggestOnTriggerCharacters: true,
    });
    editorRef.current = editor;

    // Keystrokes feed the draft buffer — but only on the editable draft view.
    // (Programmatic setValue while showing the read-only published spec must not
    // overwrite the draft.)
    const sub = editor.onDidChangeModelContent(() => {
      if (useSnippetStore.getState().editorView === 'draft') {
        useSnippetStore.getState().updateDraft(editor.getValue());
      }
    });

    // Cmd/Ctrl+S is owned globally by the EventRouter (docs/architecture/04 →
    // "bind listeners in exactly one place"), which publishes before the
    // interactive-context gate so it works while the editor has focus. Monaco
    // binds no default for Cmd+S, so the keystroke bubbles to that one handler.

    return () => {
      sub.dispose();
      editor.dispose();
      editorRef.current = null;
    };
  }, []);

  // Reload the buffer on a programmatic load (select/create/delete/revert →
  // bufferEpoch) or a view toggle. Not on keystrokes: typing changes neither dep.
  useEffect(() => {
    const editor = editorRef.current;
    if (!editor) return;
    const state = useSnippetStore.getState();
    const text = selectShownText(state);
    if (editor.getValue() !== text) editor.setValue(text);
    editor.updateOptions({
      readOnly: state.activeSnippetId === null || state.editorView === 'published',
    });
  }, [bufferEpoch, editorView, activeId]);

  // Apply editor preferences live as they change (spec §07 Apply → takes effect
  // immediately). tabSize is a model option, so it's set on the model.
  useEffect(() => {
    const editor = editorRef.current;
    if (!editor) return;
    editor.updateOptions({
      fontSize: editorPrefs.fontSize,
      wordWrap: editorPrefs.wordWrap,
      lineNumbers: editorPrefs.lineNumbers,
      minimap: { enabled: editorPrefs.minimap },
    });
    editor.getModel()?.updateOptions({ tabSize: editorPrefs.tabSize });
  }, [editorPrefs]);

  // Editor theme: Auto follows the app UI theme; an explicit override forces one
  // (spec §07 → Editor theme, provisional).
  useEffect(() => {
    const pref = editorPrefs.theme;
    const effective = pref === 'auto' ? uiTheme : pref;
    monaco.editor.setTheme(effective === 'dark' ? 'vs-dark' : 'vs');
  }, [uiTheme, editorPrefs.theme]);

  return (
    <div className={styles.editorPane}>
      <EditorToolbar />
      <div className={styles.editorWrap}>
        {activeId === null && <div className={styles.placeholder}>Select or create a snippet</div>}
        <div className={styles.editor} ref={hostRef} />
      </div>
      {/* The single live region for render/parse errors: assertive, since the
          user just caused it. The preview shows the same text visually but is
          not a live region, so the message is announced once (doc §10.1). */}
      {error !== null && (
        <pre className={styles.error} role="alert">
          {error}
        </pre>
      )}
    </div>
  );
}
