/**
 * Extract-embedded-data → Dataset state (spec §03F).
 *
 * Backs the Extract modal: the reverse of a named reference. It captures the
 * embedded data of the **view the cursor sits in**, takes a dataset name, and on
 * confirm saves the data as a new library dataset and rewrites the spec so the
 * embedded data is replaced by a by-name reference.
 *
 * Two shapes of embedded data, captured as a `target` (spec §03F; multi-view scope
 * doc M3):
 *   - **inline** — a view's `data.values`. Confirm rewrites that view's `data`
 *     block at its anchor path to `{ name }`.
 *   - **self-defined** — a `{ name: X }` reference to the spec's own top-level
 *     `datasets.X`. Confirm removes the `datasets` entry (and the map when it
 *     empties); the reference resolves to the new library dataset, renamed when the
 *     name changes (`core/spec-refs` → `promoteSelfDefinedDataset`).
 *
 * The store is the editor-free side of the flow: it never reads the cursor or the
 * Monaco model — `services/extract-action` resolves the focused binding and seeds
 * it via `begin`, so this stays a plain, testable store.
 */

import { create } from 'zustand';
import { createDataset } from '@core/dataset';
import { formatSpec } from '@core/json-format';
import { isNameTaken } from '@core/naming';
import { setDataBindingAtPath } from '@core/spec-data';
import { type InlinePayload, specHasExtractableData } from '@core/spec-inline-data';
import { promoteSelfDefinedDataset } from '@core/spec-refs';
import { useDatasetStore } from './DatasetStore';
import { notify } from './NotificationStore';
import { useSnippetStore } from './SnippetStore';

/** The path of the view whose `data` block Extract rewrites (`[]` = root). */
type AnchorPath = ReadonlyArray<string | number>;

/** What confirm rewrites — an inline `data` block, or a self-defined `datasets` entry. */
export type ExtractTarget =
  | { kind: 'inline'; anchorPath: AnchorPath }
  | { kind: 'self-defined'; datasetName: string };

/** True when the active snippet's draft has data Extract can lift, in any view. */
export function hasExtractableData(draftText: string): boolean {
  try {
    return specHasExtractableData(JSON.parse(draftText));
  } catch {
    return false;
  }
}

export interface ExtractState {
  /** Proposed dataset name (required, unique). */
  name: string;
  /** The focused view's embedded data captured at open, for the read-only preview. */
  source: InlinePayload | null;
  /** Where `confirm` writes the by-name reference. */
  target: ExtractTarget | null;
  /** Inline validation message, or null. */
  error: string | null;

  /** Seed the form with the focused view's captured data (extract-action). */
  begin: (captured: { source: InlinePayload; target: ExtractTarget; name?: string }) => void;
  setName: (name: string) => void;
  /**
   * Validate, create the dataset, and rewrite the spec to reference it by name.
   * Returns whether it committed; on failure `error` is set. `now` injectable for
   * tests.
   */
  confirm: (now?: Date) => boolean;
  reset: () => void;
}

const INITIAL = {
  name: '',
  source: null as InlinePayload | null,
  target: null as ExtractTarget | null,
  error: null as string | null,
};

export const useExtractStore = create<ExtractState>((set, get) => ({
  ...INITIAL,

  begin: ({ source, target, name = '' }) => set({ name, source, target, error: null }),

  setName: (name) => set({ name, error: null }),

  confirm: (now) => {
    const name = get().name.trim();
    if (name === '') {
      set({ error: 'Enter a dataset name.' });
      return false;
    }
    if (isNameTaken(name, useDatasetStore.getState().datasets)) {
      set({ error: `A dataset named "${name}" already exists. Choose a different name.` });
      return false;
    }
    const { source, target } = get();
    if (!source || !target) {
      set({ error: 'No data to extract.' });
      return false;
    }

    // Resolve the rewrite before any side effect: parse the live draft and apply the
    // target's rewrite to a fresh copy. Both can fail (the draft is no longer valid
    // JSON, or its shape changed under the modal); bail with a message and create
    // nothing rather than leave a half-done extraction.
    const draftText = useSnippetStore.getState().draftText;
    let spec: unknown;
    try {
      spec = JSON.parse(draftText);
    } catch {
      set({ error: 'The spec is no longer valid JSON. Close and reopen Extract.' });
      return false;
    }

    let rewritten: unknown;
    if (target.kind === 'inline') {
      if (!setDataBindingAtPath(spec, target.anchorPath, { name })) {
        set({ error: 'Could not locate the data to replace. Close and reopen Extract.' });
        return false;
      }
      rewritten = spec;
    } else {
      const next = promoteSelfDefinedDataset(spec, target.datasetName, name);
      if (!next) {
        set({ error: 'Could not locate the data to replace. Close and reopen Extract.' });
        return false;
      }
      rewritten = next;
    }

    // JSON/TopoJSON store the parsed value; CSV/TSV keep raw text — but the captured
    // `values` is already the right runtime shape for each, so store it directly.
    const dataset = createDataset({
      name,
      data: source.values,
      format: source.format,
      source: 'inline',
      now,
    });
    useDatasetStore.getState().add(dataset);

    // Re-serialize in the app's house style (json-format), preserving the spec and
    // only swapping the captured data for the reference.
    useSnippetStore.getState().replaceActiveDraft(formatSpec(rewritten), now);

    // Success confirmation (spec §03F). Title states the action; the message adds
    // the consequence — the spec was rewritten to reference the new dataset by name
    // (council toast-copy rule, docs/architecture/10 → Toast copy).
    notify({
      kind: 'success',
      title: 'Dataset created',
      message: `The spec now references "${name}" instead of its embedded data.`,
    });
    set(INITIAL);
    return true;
  },

  reset: () => set(INITIAL),
}));
