/**
 * Datasets manager — the modal body (spec §05).
 *
 * A two-pane manager rendered inside the shared modal shell (App provides the
 * backdrop, header, close, and focus trap). Left: a "New Dataset" action plus the
 * dataset list, newest-modified first, each row carrying a source/rows/format/size
 * meta line and a usage badge. Right: the selected dataset's detail, the
 * create/edit form, or an empty prompt.
 *
 * State lives in DatasetStore; the bidirectional snippet↔dataset link is derived
 * by scanning SnippetStore (docs/architecture/07 §4), so usage counts and Linked
 * Snippets stay reactive without a stored back-pointer.
 */

import { useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
  cellText,
  datasetReference,
  tabularRows,
  type DataSource,
  type Dataset,
} from '@core/dataset';
import { detectFormat, detectFormatFromUrl, type DataFormat } from '@core/format-detection';
import { datasetUsageCounts, snippetsReferencingDataset } from '@core/relationships';
import { humanizeBytes } from '@core/storage-estimate';
import { closeModal, openModal, resnapshot } from '../modals/ModalCoordinator';
import { fetchRemoteData } from '../infrastructure/remote-data';
import { remoteFetchErrorMessage } from '../services/remote-data-errors';
import { confirm } from '../stores/ConfirmStore';
import { notify } from '../stores/NotificationStore';
import {
  selectCanSave,
  selectSelectedDataset,
  useDatasetStore,
  byModifiedDesc,
} from '../stores/DatasetStore';
import { useSnippetStore } from '../stores/SnippetStore';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { Button } from './Button';
import { Icon } from './Icon';
import styles from './DatasetsModal.module.css';

/** Display label for a format (spec §05 → List item: JSON / CSV / TSV / TopoJSON). */
function formatLabel(format: DataFormat): string {
  return format === 'topojson' ? 'TopoJSON' : format.toUpperCase();
}

const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
  { value: 'inline', label: 'Inline' },
  { value: 'url', label: 'URL' },
];

/** Rows shown in the tabular preview before truncating (spec §05 → Detail Panel). */
const PREVIEW_ROW_LIMIT = 50;

export function DatasetsModal() {
  const datasets = useDatasetStore(useShallow((s) => s.datasets));
  const view = useDatasetStore((s) => s.view);
  const selected = useDatasetStore(selectSelectedDataset);
  const snippets = useSnippetStore(useShallow((s) => s.snippets));

  const select = useDatasetStore((s) => s.select);
  const startCreate = useDatasetStore((s) => s.startCreate);

  const usage = datasetUsageCounts(snippets);
  const ordered = [...datasets].sort(byModifiedDesc);

  const handleNew = () => {
    startCreate();
    resnapshot(); // baseline the discard check to the freshly-opened empty form
  };

  return (
    <div className={styles.manager}>
      <div className={styles.listPane}>
        <Button variant="primary" size="lg" className={styles.newButton} onClick={handleNew}>
          <Icon name="add" /> New Dataset
        </Button>
        <ul className={styles.list}>
          {ordered.length === 0 && (
            <li className={styles.empty}>
              No datasets yet — create one to reuse data across snippets.
            </li>
          )}
          {ordered.map((d) => (
            <DatasetListItem
              key={d.id}
              dataset={d}
              active={d.id === selected?.id}
              usage={usage.get(d.name.toLowerCase()) ?? 0}
              onSelect={() => select(d.id)}
            />
          ))}
        </ul>
      </div>

      <div className={styles.detailPane}>
        {view === 'new' || view === 'edit' ? (
          <DatasetFormView editing={view === 'edit'} />
        ) : selected ? (
          <DatasetDetail dataset={selected} snippets={snippets} />
        ) : (
          <div className={styles.detailEmpty}>Select a dataset or create a new one.</div>
        )}
      </div>
    </div>
  );
}

function DatasetListItem({
  dataset,
  active,
  usage,
  onSelect,
}: {
  dataset: Dataset;
  active: boolean;
  usage: number;
  onSelect: () => void;
}) {
  // Meta line: source ("URL" prefix), row count when known, format label, size.
  // A fetched URL snapshot reads like an inline dataset (rows + size); an unfetched
  // URL reference shows "not fetched" in place of figures it doesn't have yet.
  const unfetched = dataset.source === 'url' && dataset.data == null;
  const parts: string[] = [];
  if (dataset.source === 'url') parts.push('URL');
  if (unfetched) parts.push('not fetched');
  else if (dataset.rowCount !== null) parts.push(`${dataset.rowCount} rows`);
  parts.push(formatLabel(dataset.format));
  if (!unfetched) parts.push(humanizeBytes(dataset.size));

  return (
    <li className={`${styles.item} ${active ? styles.itemActive : ''}`}>
      <button
        type="button"
        className={styles.itemMain}
        aria-current={active || undefined}
        onClick={onSelect}
      >
        <span className={styles.itemName}>{dataset.name}</span>
        <span className={styles.itemMeta}>{parts.join(' · ')}</span>
      </button>
      {usage > 0 && (
        <span className={styles.badge} title={`Used by ${usage} snippet${usage === 1 ? '' : 's'}`}>
          {usage}
        </span>
      )}
    </li>
  );
}

function DatasetDetail({
  dataset,
  snippets,
}: {
  dataset: Dataset;
  snippets: ReadonlyArray<{ id: string; name: string; datasetRefs: string[] }>;
}) {
  const startEdit = useDatasetStore((s) => s.startEdit);
  const remove = useDatasetStore((s) => s.remove);
  const refreshDataset = useDatasetStore((s) => s.refreshDataset);
  const selectSnippet = useSnippetStore((s) => s.selectSnippet);
  const [copied, setCopied] = useState(false);
  const [refreshing, setRefreshing] = useState(false);

  const linked = snippetsReferencingDataset(snippets, dataset.name);

  // Tabular data (CSV/TSV/JSON-array, inline or fetched) previews as a table of the
  // first rows under the profiled columns; non-tabular payloads fall back to text.
  // Memoized on the record so a large CSV isn't re-parsed on unrelated re-renders.
  const previewRows = useMemo(
    () => tabularRows(dataset.data, dataset.format, PREVIEW_ROW_LIMIT),
    [dataset],
  );

  const handleEdit = () => {
    startEdit();
    resnapshot();
  };

  // Re-fetch a URL dataset's source and re-snapshot it. The visible result (updated
  // rows + "Fetched" time) is the confirmation, so success raises no toast; only a
  // failure surfaces one (spec §05 → Actions; docs/architecture/10 → Toast copy).
  const handleRefresh = async () => {
    if (!dataset.url) return;
    setRefreshing(true);
    try {
      const { text } = await fetchRemoteData(dataset.url);
      refreshDataset(dataset.id, { text });
    } catch (err) {
      notify({
        kind: 'error',
        title: "Couldn't refresh dataset",
        message: remoteFetchErrorMessage(err, 'retry'),
      });
    } finally {
      setRefreshing(false);
    }
  };

  const handleCopy = async () => {
    const text = JSON.stringify(datasetReference(dataset.name), null, 2);
    try {
      await navigator.clipboard.writeText(text);
      // Lightweight local feedback; the success-toast wiring is deferred to M6
      // with the other success toasts (spec §05 → Actions).
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      notify({
        kind: 'error',
        title: "Couldn't copy",
        message: 'Your browser blocked clipboard access. Select and copy the reference manually.',
      });
    }
  };

  const handleDelete = async () => {
    const ok = await confirm({
      title: 'Delete dataset',
      message: `Delete "${dataset.name}"? This cannot be undone.`,
      confirmLabel: 'Delete',
      danger: true,
    });
    if (!ok) return;
    const removedName = dataset.name;
    remove(dataset.id);
    // Confirm the deletion (spec §05). The message names which dataset went
    // (council toast-copy rule, docs/architecture/10 → Toast copy).
    notify({
      kind: 'success',
      title: 'Dataset deleted',
      message: `"${removedName}" was permanently removed.`,
    });
  };

  return (
    <div className={styles.detail}>
      <div className={styles.detailHead}>
        <h3 className={styles.detailName}>{dataset.name}</h3>
        <div className={styles.detailActions}>
          <Button onClick={() => void handleCopy()}>{copied ? 'Copied' : 'Copy Reference'}</Button>
          {/* The clipboard write is invisible, so the success is confirmed inline
              ("Copied") rather than by a toast (docs/architecture/10 → Toast copy).
              This polite live region announces it to assistive tech, which the
              button's visual label swap alone would not reliably do. */}
          <span role="status" className="visually-hidden">
            {copied ? 'Reference copied to clipboard' : ''}
          </span>
          {dataset.source === 'url' && (
            <Button onClick={() => void handleRefresh()} disabled={refreshing}>
              {refreshing ? 'Refreshing…' : 'Refresh'}
            </Button>
          )}
          <Button onClick={handleEdit}>Edit</Button>
          {/* Build Chart (spec §05 → §06) — opens the Chart Builder on this dataset.
              Replaces the Datasets modal (one modal at a time, §01C); detail view has
              no transient form state, so no discard prompt. */}
          <Button onClick={() => openModal('chartBuilder', String(dataset.id))}>Build Chart</Button>
          <Button variant="danger-outline" onClick={() => void handleDelete()}>
            Delete
          </Button>
        </div>
      </div>

      {dataset.comment && <p className={styles.comment}>{dataset.comment}</p>}

      {dataset.source === 'url' && (
        <p className={styles.sourceMeta}>
          <a className={styles.sourceLink} href={dataset.url} target="_blank" rel="noreferrer">
            {dataset.url}
          </a>
          <span>
            {dataset.fetchedAt
              ? `Fetched ${new Date(dataset.fetchedAt).toLocaleString()}`
              : 'Not fetched yet'}
          </span>
        </p>
      )}

      <section className={styles.section}>
        <h4 className={styles.sectionTitle}>Overview</h4>
        <dl className={styles.stats}>
          <div>
            <dt>Rows</dt>
            <dd>{dataset.rowCount ?? 'N/A'}</dd>
          </div>
          <div>
            <dt>Columns</dt>
            <dd>{dataset.columnCount ?? 'N/A'}</dd>
          </div>
          <div>
            <dt>Format</dt>
            <dd>{formatLabel(dataset.format)}</dd>
          </div>
          <div>
            <dt>Size</dt>
            <dd>{dataset.data == null ? 'N/A' : humanizeBytes(dataset.size)}</dd>
          </div>
        </dl>

        {dataset.columnTypes.length > 0 && (
          <ul className={styles.columns}>
            {dataset.columnTypes.map((col) => (
              <li key={col.name} className={styles.column}>
                <span className={styles.columnName}>{col.name}</span>
                <span className={styles.columnType}>{col.type}</span>
              </li>
            ))}
          </ul>
        )}

        <div className={styles.timestamps}>
          <span>Created {new Date(dataset.created).toLocaleString()}</span>
          <span>Modified {new Date(dataset.modified).toLocaleString()}</span>
        </div>
      </section>

      <section className={styles.section}>
        <h4 className={styles.sectionTitle}>Preview</h4>
        {previewRows ? (
          <>
            {/* A scrollable region needs a tab stop + name so keyboard-only users can
                reach and scroll it when the preview overflows (WCAG 2.1.1; WAI-ARIA APG
                — a focusable `region` with an accessible name). */}
            <div
              className={styles.previewTableWrap}
              role="region"
              aria-label={`Data preview for ${dataset.name}`}
              tabIndex={0}
            >
              <table className={styles.previewTable}>
                <thead>
                  <tr>
                    {dataset.columns.map((col, ci) => (
                      <th key={ci} scope="col">
                        {col}
                      </th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {previewRows.map((row, ri) => (
                    <tr key={ri}>
                      {dataset.columns.map((col, ci) => (
                        <td key={ci}>{cellText(row[col])}</td>
                      ))}
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            {dataset.rowCount != null && dataset.rowCount > previewRows.length && (
              <p className={styles.muted}>
                Showing the first {previewRows.length} of {dataset.rowCount} rows.
              </p>
            )}
          </>
        ) : (
          <pre className={styles.preview}>{previewText(dataset)}</pre>
        )}
      </section>

      <section className={styles.section}>
        <h4 className={styles.sectionTitle}>Linked Snippets</h4>
        {linked.length === 0 ? (
          <p className={styles.muted}>No snippets reference this dataset yet.</p>
        ) : (
          <ul className={styles.linked}>
            {linked.map((s) => (
              <li key={s.id}>
                <button
                  type="button"
                  className={styles.linkButton}
                  onClick={() => {
                    selectSnippet(s.id);
                    void closeModal();
                  }}
                >
                  {s.name}
                </button>
              </li>
            ))}
          </ul>
        )}
      </section>
    </div>
  );
}

/** A truncated rendering of the data: raw for csv/tsv, pretty JSON otherwise. */
function previewText(dataset: Dataset): string {
  const MAX = 2000;
  // No snapshot yet (an unfetched URL reference) — there is nothing to preview.
  if (dataset.data == null) {
    return dataset.source === 'url' ? 'Not fetched yet — use Refresh to load the data.' : '';
  }
  let text: string;
  if (dataset.format === 'csv' || dataset.format === 'tsv') {
    // CSV/TSV payloads are raw text; fall back to JSON for any non-string value.
    text = typeof dataset.data === 'string' ? dataset.data : JSON.stringify(dataset.data);
  } else {
    try {
      text = JSON.stringify(dataset.data, null, 2);
    } catch {
      text = typeof dataset.data === 'string' ? dataset.data : '[unserializable data]';
    }
  }
  return text.length > MAX ? `${text.slice(0, MAX)}\n…` : text;
}

function DatasetFormView({ editing }: { editing: boolean }) {
  const form = useDatasetStore((s) => s.form);
  const formError = useDatasetStore((s) => s.formError);
  const selected = useDatasetStore(selectSelectedDataset);
  const updateForm = useDatasetStore((s) => s.updateForm);
  const cancelForm = useDatasetStore((s) => s.cancelForm);
  const save = useDatasetStore((s) => s.save);
  const commitUrlSnapshot = useDatasetStore((s) => s.commitUrlSnapshot);
  // Save stays disabled until a name and valid data/URL are present (spec §05).
  const canSave = useDatasetStore(selectCanSave);

  // URL datasets fetch on save (snapshot model): `fetching` drives the button's
  // busy state; `fetchError` holds a failed fetch's message + the inline fallback.
  const [fetching, setFetching] = useState(false);
  const [fetchError, setFetchError] = useState<string | null>(null);

  // Live format/source hint from the current input (spec §05 → Auto-detection).
  const detected =
    form.source === 'url'
      ? { format: detectFormatFromUrl(form.input.trim()), confidence: 'url' as const }
      : detectFormat(form.input);

  const handleSave = async () => {
    // Inline saves are synchronous; only URL datasets touch the network.
    if (form.source !== 'url') {
      if (save()) resnapshot(); // committed — re-baseline so a later close won't prompt
      return;
    }
    const url = form.input.trim();
    // A metadata-only edit (same URL, snapshot already present) needs no re-fetch.
    const metaOnly =
      editing &&
      selected?.source === 'url' &&
      selected.data != null &&
      url === (selected.url ?? '');
    if (metaOnly) {
      if (save()) resnapshot();
      return;
    }
    // Create, changed URL, or inline→URL: fetch once, then snapshot + commit.
    setFetchError(null);
    setFetching(true);
    try {
      const { text } = await fetchRemoteData(url);
      if (commitUrlSnapshot({ text })) resnapshot();
    } catch (err) {
      setFetchError(remoteFetchErrorMessage(err));
    } finally {
      setFetching(false);
    }
  };

  // Recovery from a failed fetch: switch to an inline paste, keeping name + comment.
  const handlePasteInline = () => {
    setFetchError(null);
    updateForm({ source: 'inline', input: '' });
  };

  const handleCancel = () => {
    cancelForm();
    resnapshot();
  };

  return (
    <div className={styles.form}>
      <h3 className={styles.detailName}>{editing ? 'Edit dataset' : 'New dataset'}</h3>

      <label className={styles.field}>
        <span className={styles.label}>Name</span>
        <input
          type="text"
          className={styles.input}
          value={form.name}
          onChange={(e) => updateForm({ name: e.target.value })}
          placeholder="e.g. Sales 2024"
        />
      </label>

      <div className={styles.field}>
        <span className={styles.label}>Source</span>
        <SegmentedControl
          label="Dataset source"
          options={SOURCE_OPTIONS}
          value={form.source}
          onChange={(source) => {
            setFetchError(null);
            updateForm({ source });
          }}
        />
      </div>

      <label className={styles.field}>
        <span className={styles.label}>{form.source === 'url' ? 'URL' : 'Data'}</span>
        {form.source === 'url' ? (
          <input
            type="url"
            className={styles.input}
            value={form.input}
            onChange={(e) => {
              setFetchError(null);
              updateForm({ input: e.target.value });
            }}
            placeholder="https://example.com/data.csv"
          />
        ) : (
          <textarea
            className={styles.textarea}
            value={form.input}
            onChange={(e) => updateForm({ input: e.target.value })}
            placeholder="Paste JSON, CSV, or TSV…"
            rows={10}
            spellCheck={false}
          />
        )}
      </label>

      {form.input.trim() !== '' && (
        <div className={styles.detected}>
          <span className={styles.detectedBadge}>
            {detected.format ? formatLabel(detected.format) : 'Unrecognized'}
          </span>
          {form.source !== 'url' && 'confidence' in detected && (
            <span className={styles.detectedHint}>{detected.confidence} confidence</span>
          )}
        </div>
      )}

      <label className={styles.field}>
        <span className={styles.label}>Comment (optional)</span>
        <input
          type="text"
          className={styles.input}
          value={form.comment}
          onChange={(e) => updateForm({ comment: e.target.value })}
          placeholder="Notes about this dataset"
        />
      </label>

      {(formError || fetchError) && (
        <p className={styles.formError} role="alert">
          {formError ?? fetchError}
        </p>
      )}
      {/* A failed fetch is recoverable by pasting the data inline (the user's choice
          when CORS or offline blocks the URL) — offered as a direct one-click path. */}
      {fetchError && (
        <Button className={styles.fallback} onClick={handlePasteInline}>
          Paste data inline instead
        </Button>
      )}

      <div className={styles.formActions}>
        <Button onClick={handleCancel} disabled={fetching}>
          Cancel
        </Button>
        <Button variant="primary" disabled={!canSave || fetching} onClick={() => void handleSave()}>
          {fetching ? 'Fetching…' : editing ? 'Save changes' : 'Create dataset'}
        </Button>
      </div>
    </div>
  );
}
