/**
 * Spec ↔ config editor actions (docs/chart-theming-scope.md §4.3) — the Monaco
 * command-palette / context-menu pair over the portable core operations:
 *
 * - **Merge chart theme into spec** bakes the *currently selected* chart theme
 *   (the preview's Chart theme control) into the draft's `config` block, so the
 *   styling travels when the spec is published outside Astrolabe. The spec's
 *   existing `config` keys win — rendering is unchanged.
 * - **Extract config from spec** removes the draft's `config` block and copies
 *   it to the clipboard — for cleaning baked-in styling out of a pasted spec.
 *   The clipboard write happens *before* the edit, so a clipboard failure
 *   never destroys the only copy.
 * - **Extract config to new theme** removes the block and saves it as a custom
 *   chart theme instead, selecting it — the one-step "this pasted spec's
 *   styling should be a reusable theme" path.
 *
 * Both replace the document via `executeEdits`, so ⌘Z restores the previous
 * text. Both no-op (with a toast naming the reason) on invalid JSON; Monaco's
 * `!editorReadonly` precondition hides them on the published view.
 *
 * Surfacing (council: NN/g #6 recognition-over-recall, #7 accelerators; Carbon
 * menu-buttons "use an overflow menu when additional options are available and
 * there is a space constraint"): the visible home is the editor toolbar's
 * **Config menu** (SpecEditor), which calls `runMergeChartTheme` /
 * `runExtractConfig` directly; the context-menu/palette registrations here are
 * the expert accelerators on the same code paths.
 */

import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { formatJson } from '@core/json-format';
import { extractConfigFromSpec, isJsonObject, mergeConfigIntoSpec } from '@core/spec-config';
import {
  CHART_THEME_OPTIONS,
  chartConfigForSelection,
  customThemeSelection,
} from '@core/vega-themes';
import { copyText } from '../infrastructure/file-transfer';
import { useAppStore } from '../stores/AppStore';
import { useCustomThemeStore } from '../stores/CustomThemeStore';
import { notify } from '../stores/NotificationStore';
import { selectActiveSnippet, useSnippetStore } from '../stores/SnippetStore';

/**
 * Parse spec JSON, or toast (and return null) when it isn't a JSON object. Takes
 * the text (not the model) so it serves both whole-document and selection-scoped
 * callers — the config actions pass `model.getValue()`, the transform actions a
 * selection.
 */
export function parseSpecObject(text: string): Record<string, unknown> | null {
  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch {
    notify({
      kind: 'error',
      title: 'Spec is not valid JSON',
      message: 'Fix the JSON syntax first, then try again.',
    });
    return null;
  }
  if (!isJsonObject(parsed)) {
    notify({
      kind: 'error',
      title: 'Spec is not a JSON object',
      message: 'This action needs a top-level { … } Vega-Lite spec.',
    });
    return null;
  }
  return parsed;
}

/** Replace the whole document as one undoable edit, in the app's JSON style. */
function replaceDocument(
  editor: monaco.editor.IStandaloneCodeEditor,
  model: monaco.editor.ITextModel,
  source: string,
  next: Record<string, unknown>,
): void {
  const raw = JSON.stringify(next);
  const text = formatJson(raw, { indent: model.getOptions().tabSize }) ?? raw;
  // Undo stops on both sides keep the replacement its own undo step — without
  // the leading stop it can coalesce with the user's preceding typing, and ⌘Z
  // would revert that too (Monaco's built-in actions bracket the same way).
  editor.pushUndoStop();
  editor.executeEdits(source, [{ range: model.getFullModelRange(), text }]);
  editor.pushUndoStop();
}

/** Bake the currently selected chart theme into the draft's `config` block. */
export function runMergeChartTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
  const model = editor.getModel();
  if (!model) return;
  const spec = parseSpecObject(model.getValue());
  if (!spec) return;

  const { chartTheme, uiTheme } = useAppStore.getState();
  // A Config is plain JSON data; the cast bridges its closed vega-lite type
  // to the JsonObject the portable merge operates on.
  const themeConfig = chartConfigForSelection(chartTheme, uiTheme) as Record<string, unknown>;
  if (Object.keys(themeConfig).length === 0) {
    notify({
      kind: 'info',
      title: 'Nothing to merge',
      message: 'The Stock Vega-Lite chart theme injects no config.',
    });
    return;
  }

  const themeLabel = CHART_THEME_OPTIONS.find((o) => o.value === chartTheme)?.label ?? chartTheme;
  replaceDocument(editor, model, 'merge-chart-theme', mergeConfigIntoSpec(spec, themeConfig));
  notify({
    kind: 'success',
    title: 'Chart theme merged',
    message: `The ${themeLabel} theme now travels in the spec’s config; existing keys were kept. Undo with ⌘/Ctrl+Z.`,
  });
}

/**
 * Shared prologue of the two extract actions: the model, the spec parsed off it,
 * and the config split out — or null (after toasting) when any step has nothing
 * to work with.
 */
function extractableConfig(editor: monaco.editor.IStandaloneCodeEditor): {
  model: monaco.editor.ITextModel;
  rest: Record<string, unknown>;
  config: Record<string, unknown>;
} | null {
  const model = editor.getModel();
  if (!model) return null;
  const spec = parseSpecObject(model.getValue());
  if (!spec) return null;

  const { spec: rest, config } = extractConfigFromSpec(spec);
  if (config === null) {
    notify({
      kind: 'info',
      title: 'No config to extract',
      message: 'This spec has no config block.',
    });
    return null;
  }
  return { model, rest, config };
}

/** Remove the draft's `config` block, copying it to the clipboard first. */
export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEditor): Promise<void> {
  const extracted = extractableConfig(editor);
  if (!extracted) return;
  const { model, rest, config } = extracted;

  // Copy before removing — if the clipboard write fails, the spec keeps its
  // config and nothing is lost.
  try {
    await copyText(JSON.stringify(config, null, 2));
  } catch {
    notify({
      kind: 'error',
      title: 'Could not copy the config',
      message: 'Clipboard access failed, so the spec was left unchanged.',
    });
    return;
  }

  replaceDocument(editor, model, 'extract-config', rest);
  notify({
    kind: 'success',
    title: 'Config extracted',
    message: 'The config block was removed and copied to the clipboard. Undo with ⌘/Ctrl+Z.',
  });
}

/**
 * Turn the draft's `config` block into a saved custom theme: create the theme
 * (named after the snippet, auto-suffixed if taken), remove the block from the
 * spec, and select the new theme as the active chart theme — so the styling
 * keeps applying to this chart, now from the injected side of the boundary.
 * (Not pixel-identical when another theme was active: that theme's
 * non-overlapping keys stop applying once the new theme replaces it.)
 */
export function runExtractConfigToTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
  const extracted = extractableConfig(editor);
  if (!extracted) return;
  const { model, rest, config } = extracted;

  const snippet = selectActiveSnippet(useSnippetStore.getState());
  const theme = useCustomThemeStore
    .getState()
    .createTheme(snippet ? `${snippet.name} theme` : 'Extracted theme', config);
  useAppStore.getState().setChartTheme(customThemeSelection(theme.id));

  replaceDocument(editor, model, 'extract-config-to-theme', rest);
  notify({
    kind: 'success',
    title: 'Theme created',
    message:
      `The config block became the custom theme “${theme.name}”, now selected as the ` +
      'chart theme. Rename or refine it via Edit themes…; undo the spec edit with ⌘/Ctrl+Z.',
  });
}

/**
 * Register the config actions on the editor (context menu + F1 palette — the expert
 * accelerators; the toolbar Config menu is the discoverable home). Returns a
 * disposable that detaches them (dispose on editor unmount, like the
 * format-on-paste hook).
 */
export function installSpecConfigActions(
  editor: monaco.editor.IStandaloneCodeEditor,
): monaco.IDisposable {
  const merge = editor.addAction({
    id: 'astrolabe.merge-chart-theme',
    label: 'Merge Chart Theme into Spec',
    contextMenuGroupId: 'astrolabe',
    contextMenuOrder: 1,
    precondition: '!editorReadonly',
    run: () => runMergeChartTheme(editor),
  });

  const extract = editor.addAction({
    id: 'astrolabe.extract-config',
    label: 'Extract Config from Spec',
    contextMenuGroupId: 'astrolabe',
    contextMenuOrder: 2,
    precondition: '!editorReadonly',
    run: () => runExtractConfig(editor),
  });

  const extractToTheme = editor.addAction({
    id: 'astrolabe.extract-config-to-theme',
    label: 'Extract Config to New Theme',
    contextMenuGroupId: 'astrolabe',
    contextMenuOrder: 3,
    precondition: '!editorReadonly',
    run: () => runExtractConfigToTheme(editor),
  });

  return {
    dispose() {
      merge.dispose();
      extract.dispose();
      extractToTheme.dispose();
    },
  };
}
