/**
 * Extract-inline-data → Dataset state (spec §03F).
 *
 * Backs the Extract modal: the reverse of a named reference. It captures the
 * active snippet draft's inline data, takes a dataset name, and on confirm saves
 * the data as a new dataset and rewrites the draft so the inline data is replaced
 * by a by-name reference (`{ "data": { "name": … } }`).
 *
 * Scope (M3): the **top-level** `data` block of the draft spec — the common case
 * for a single-view chart. Inline data nested inside layers/concats is left for a
 * later pass; `hasInlineData` reflects exactly what `confirm` can lift, so the
 * editor only offers the action when this store can act on it.
 */

import { create } from 'zustand';
import type { DataFormat } from '@core/format-detection';
import { createDataset } from '@core/dataset';
import { isNameTaken } from '@core/naming';
import { useDatasetStore } from './DatasetStore';
import { notify } from './NotificationStore';
import { useSnippetStore } from './SnippetStore';

/** The inline `data` block of a parsed spec, if it carries `values`. */
interface InlineData {
  values: unknown;
  format: DataFormat;
}

/**
 * Read the top-level inline data from a draft spec's text, or null when there is
 * none (no snippet, unparseable, or no `data.values`). The format comes from an
 * explicit `data.format.type` when present (raw CSV/TSV strings), else JSON.
 */
function readInlineData(draftText: string): InlineData | null {
  let parsed: unknown;
  try {
    parsed = JSON.parse(draftText);
  } catch {
    return null;
  }
  if (!parsed || typeof parsed !== 'object') return null;
  const data = (parsed as Record<string, unknown>).data;
  if (!data || typeof data !== 'object') return null;
  const values = (data as Record<string, unknown>).values;
  if (values === undefined) return null;
  const declared = (data as Record<string, unknown>).format;
  const type =
    declared && typeof declared === 'object'
      ? (declared as Record<string, unknown>).type
      : undefined;
  const format: DataFormat =
    type === 'csv' || type === 'tsv' || type === 'topojson' ? type : 'json';
  return { values, format };
}

/** True when the active snippet's draft has top-level inline data to extract. */
export function hasInlineData(draftText: string): boolean {
  return readInlineData(draftText) !== null;
}

export interface ExtractState {
  /** Proposed dataset name (required, unique). */
  name: string;
  /** The inline data captured at open, for the read-only preview. */
  source: InlineData | null;
  /** Inline validation message, or null. */
  error: string | null;

  /** Capture the active snippet's inline data and reset the form. */
  init: () => void;
  setName: (name: string) => void;
  /**
   * Validate, create the dataset, and rewrite the active draft 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 InlineData | null, error: null as string | null };

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

  init: () => {
    const draft = useSnippetStore.getState().draftText;
    set({ name: '', source: readInlineData(draft), 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 = get().source;
    if (!source) {
      set({ error: 'No inline data to extract.' });
      return false;
    }

    // JSON/TopoJSON store the parsed value; CSV/TSV keep raw text — but inline
    // `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);

    // Rewrite the top-level data block to a by-name reference, preserving the rest
    // of the spec and its pretty-printed text shape.
    const draftText = useSnippetStore.getState().draftText;
    const spec = JSON.parse(draftText) as Record<string, unknown>;
    spec.data = { name };
    useSnippetStore.getState().replaceActiveDraft(JSON.stringify(spec, null, 2), 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 inline data.`,
    });
    set(INITIAL);
    return true;
  },

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