/**
 * URL hash ↔ view-state routing (spec §01E, docs/architecture/04 → "Routing").
 *
 * The hash is the single serialized source of the current view: selected
 * snippet, open Datasets manager (list / a dataset / the new-dataset form), or
 * the Chart Builder. It is read on load to restore state, written as the user
 * navigates, and Back/Forward step between prior states.
 *
 * Two directions, one guard:
 *
 * - **state → hash** (`syncUrlFromState`): a derive-from-stores writer. Subscribed
 *   to every store that can change the view, it computes the current `ViewState`
 *   and `pushView`s it when it differs from the URL. Deriving from state (rather
 *   than reacting to individual navigation calls) is what lets the *in-modal*
 *   dataset sub-views (`#datasets/new`, `#datasets/dataset-<id>`) reach the hash:
 *   those are `DatasetStore` view changes, not modal-open events.
 * - **hash → state** (`applyView`): restore-on-load and Back/Forward. Drives the
 *   stores to match a parsed view, validating ids and cleaning a stale hash.
 *
 * The `applying` flag suppresses the state→hash writer while `applyView` is
 * driving the stores, so a restore/Back never echoes back into a navigation.
 *
 * `applyView` writes `activeModal` with the bare `setActiveModal` primitive
 * rather than the coordinator's `openModal`/`closeModal`: it is reflecting the
 * URL *into* the stores, so it must not re-sync the URL or run the unsaved-change
 * discard prompt (docs/architecture/04 §1.3).
 *
 * The coordinator's original `syncModalToUrl` / `clearModalFromUrl` seam is kept
 * (now thin wrappers over `syncUrlFromState`) so its call sites are unchanged.
 */

import { useAppStore } from '../stores/AppStore';
import { useChartBuilderStore } from '../stores/ChartBuilderStore';
import { useDatasetStore } from '../stores/DatasetStore';
import { useSnippetStore } from '../stores/SnippetStore';
import {
  currentHash,
  pushView,
  readView,
  replaceView,
  serializeHash,
  type ViewState,
} from '../infrastructure/url-hash';
import { getModalConfig } from './modal-registry';
import type { ModalName } from './types';

let applying = false;
let started = false;

/**
 * Derive the current view from store state — the URL's single source of truth.
 * Only URL-navigable modals own the hash; `extract` / `about` / `donate` are not
 * navigable (spec §01E lists no hash for them), so while one is open the URL
 * keeps showing the underlying snippet view.
 */
export function deriveViewState(): ViewState {
  const modal = useAppStore.getState().activeModal;
  if (modal && getModalConfig(modal)?.isUrlNavigable) {
    if (modal === 'chartBuilder') {
      const datasetId = useChartBuilderStore.getState().datasetId;
      return datasetId !== null ? { kind: 'dataset-build', datasetId } : { kind: 'build' };
    }
    if (modal === 'datasets') {
      const ds = useDatasetStore.getState();
      if (ds.view === 'new') return { kind: 'dataset-new' };
      if (ds.selectedId !== null) return { kind: 'dataset', datasetId: ds.selectedId };
      return { kind: 'datasets' };
    }
  }
  const snippetId = useSnippetStore.getState().activeSnippetId;
  return snippetId ? { kind: 'snippet', snippetId } : { kind: 'snippets' };
}

/** True when `view` already matches the current URL hash. */
function isCurrent(view: ViewState): boolean {
  return serializeHash(view) === currentHash();
}

/**
 * state → hash. Computes the derived view and pushes it when it differs from the
 * URL. Suppressed while `applyView` is running (the `applying` guard) so a
 * Back/Forward or load-restore never echoes a redundant navigation.
 */
function syncUrlFromState(): void {
  if (applying) return;
  const view = deriveViewState();
  if (!isCurrent(view)) pushView(view);
}

/**
 * hash → state. Drives the stores to reflect `view`; falls back and cleans the
 * URL (`replaceView`, no history entry) when a referenced id no longer exists
 * (deleted record, stale shared link).
 */
export function applyView(view: ViewState): void {
  applying = true;
  try {
    const app = useAppStore.getState();
    switch (view.kind) {
      case 'snippets':
        app.setActiveModal(null);
        return;
      case 'snippet': {
        const exists = useSnippetStore.getState().snippets.some((s) => s.id === view.snippetId);
        if (!exists) {
          replaceView({ kind: 'snippets' });
          return;
        }
        app.setActiveModal(null);
        useSnippetStore.getState().selectSnippet(view.snippetId);
        return;
      }
      case 'datasets':
        useDatasetStore.getState().select(null);
        app.setActiveModal('datasets');
        return;
      case 'dataset':
      case 'dataset-build': {
        const ds = useDatasetStore.getState();
        if (!ds.datasets.some((d) => d.id === view.datasetId)) {
          ds.select(null);
          app.setActiveModal('datasets');
          replaceView({ kind: 'datasets' });
          return;
        }
        ds.select(view.datasetId);
        if (view.kind === 'dataset-build') {
          useChartBuilderStore.getState().init(view.datasetId);
          app.setActiveModal('chartBuilder');
        } else {
          app.setActiveModal('datasets');
        }
        return;
      }
      case 'dataset-new':
        app.setActiveModal('datasets');
        useDatasetStore.getState().startCreate();
        return;
      case 'build':
        // The no-target builder door. init(null) picks the freshest dataset, so
        // with any datasets present the derived view immediately self-corrects to
        // `dataset-build`; only an empty dataset library stays on `#build`.
        useChartBuilderStore.getState().init(null);
        app.setActiveModal('chartBuilder');
        return;
    }
  } finally {
    applying = false;
  }
}

/**
 * Start routing. Restores the view from the hash (must run *after* the stores
 * are hydrated so ids resolve), then keeps hash ↔ state in sync via a
 * `hashchange` listener (Back/Forward) and store subscriptions (navigation).
 * Idempotent.
 */
export function startRouting(): void {
  if (started) return;
  started = true;

  applyView(readView());

  // Reflect the view the stores actually settled on (e.g. the snippet hydrate
  // auto-selected) back into the URL with `replaceView` — no history entry. This
  // replaces the blank load entry rather than letting the first store tick
  // `pushView` it, which would leave a dead Back step at startup.
  const initial = deriveViewState();
  if (!isCurrent(initial)) replaceView(initial);

  window.addEventListener('hashchange', () => {
    if (applying) return;
    applyView(readView());
  });

  // Any store that can change the view writes the hash. A change that leaves the
  // derived view unchanged (e.g. a draft keystroke) is a cheap no-op in
  // `syncUrlFromState`'s equality check.
  useAppStore.subscribe(syncUrlFromState);
  useSnippetStore.subscribe(syncUrlFromState);
  useDatasetStore.subscribe(syncUrlFromState);
  useChartBuilderStore.subscribe(syncUrlFromState);
}

// --- Coordinator seam (unchanged call sites) -------------------------------
// The coordinator calls these on modal open/close; both now just re-derive the
// hash from state. Kept as named exports so ModalCoordinator needs no edit.

/** Reflect an open navigable modal (and optional sub-target) in the URL hash. */
export function syncModalToUrl(_name: ModalName, _arg?: string): void {
  syncUrlFromState();
}

/** Return the hash to the underlying workspace when a navigable modal closes. */
export function clearModalFromUrl(_name: ModalName): void {
  syncUrlFromState();
}
