// /tools/verfahrensablauf URL + scenario-localStorage state contract
// (t-paliad-308 / m/paliad#137). Splits the page's persisted state into
// two namespaces:
//
//   URL params (filter chips — the timeline kind the user is looking
//   at; paste-able, shareable, refresh-resistant):
//     proceeding, side, target, trigger_date
//
//   localStorage `paliad.verfahrensablauf.scenario.*` (per-user
//   scenario inputs — the noisy parts that don't belong in a URL):
//     event_choices, court_id, ccr, inf_amend, rev_amend, rev_cci,
//     show_hidden
//
// Hydration order: URL wins. On page load, URL fills the filter chips;
// localStorage fills the rest. Filter-chip changes write to URL only.
// Scenario changes write to localStorage only. A shared link from a
// colleague reproduces the timeline kind (proceeding + side + target +
// trigger_date) but never leaks the recipient's court / flag /
// event_choices state in.
//
// All helpers in this module are pure: they take a search string (or a
// StorageLike) and return values, no DOM. The wiring in
// ../verfahrensablauf.ts mounts them onto window.location +
// window.localStorage at runtime.

import type { EventChoice, ChoiceKind } from "./event-card-choices";

// ----- URL params (filter chips) ----------------------------------

export type Side = "claimant" | "defendant" | null;

export const APPEAL_TARGETS = [
  "endentscheidung",
  "kostenentscheidung",
  "anordnung",
  "schadensbemessung",
  "bucheinsicht",
] as const;
export type AppealTarget = (typeof APPEAL_TARGETS)[number] | "";

export const URL_KEYS = {
  proceeding: "proceeding",
  side: "side",
  target: "target",
  triggerDate: "trigger_date",
} as const;

// parseProceedingFromSearch extracts the proceeding code. Returns ""
// if absent. No validation against the proceeding registry — that's
// the caller's job (an unknown code from a stale link should leave
// the first-tile auto-select fallback running).
export function parseProceedingFromSearch(search: string): string {
  const v = new URLSearchParams(search).get(URL_KEYS.proceeding);
  return v ?? "";
}

export function parseSideFromSearch(search: string): Side {
  const raw = new URLSearchParams(search).get(URL_KEYS.side);
  return raw === "claimant" || raw === "defendant" ? raw : null;
}

export function parseAppealTargetFromSearch(search: string): AppealTarget {
  const raw = new URLSearchParams(search).get(URL_KEYS.target) || "";
  if ((APPEAL_TARGETS as readonly string[]).includes(raw)) {
    return raw as AppealTarget;
  }
  return "";
}

// parseTriggerDateFromSearch validates the ISO-date shape so a
// malformed link can't poison the date input. Accepts "YYYY-MM-DD"
// only. Round-tripped against Date to reject 2026-02-30 etc.
export function parseTriggerDateFromSearch(search: string): string {
  const raw = new URLSearchParams(search).get(URL_KEYS.triggerDate) || "";
  if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return "";
  const d = new Date(raw + "T00:00:00Z");
  if (Number.isNaN(d.getTime())) return "";
  if (d.toISOString().slice(0, 10) !== raw) return "";
  return raw;
}

// applyFiltersToSearch produces the canonical query string for the
// four URL-owned params. Other params (e.g. ?view=, ?project=) are
// preserved verbatim. Empty values are deleted, never written as
// empty string, so the URL stays clean on the default.
export function applyFiltersToSearch(
  search: string,
  filters: { proceeding?: string; side?: Side; target?: AppealTarget; triggerDate?: string },
): string {
  const params = new URLSearchParams(search);
  if ("proceeding" in filters) {
    if (filters.proceeding && filters.proceeding !== "") {
      params.set(URL_KEYS.proceeding, filters.proceeding);
    } else {
      params.delete(URL_KEYS.proceeding);
    }
  }
  if ("side" in filters) {
    if (filters.side === "claimant" || filters.side === "defendant") {
      params.set(URL_KEYS.side, filters.side);
    } else {
      params.delete(URL_KEYS.side);
    }
  }
  if ("target" in filters) {
    if (filters.target && filters.target !== "") {
      params.set(URL_KEYS.target, filters.target);
    } else {
      params.delete(URL_KEYS.target);
    }
  }
  if ("triggerDate" in filters) {
    if (filters.triggerDate && /^\d{4}-\d{2}-\d{2}$/.test(filters.triggerDate)) {
      params.set(URL_KEYS.triggerDate, filters.triggerDate);
    } else {
      params.delete(URL_KEYS.triggerDate);
    }
  }
  const s = params.toString();
  return s ? `?${s}` : "";
}

// ----- localStorage (scenario state) ------------------------------

export const SCENARIO_PREFIX = "paliad.verfahrensablauf.scenario";
export const SCENARIO_KEYS = {
  eventChoices: `${SCENARIO_PREFIX}.event_choices`,
  courtId: `${SCENARIO_PREFIX}.court_id`,
  ccr: `${SCENARIO_PREFIX}.ccr`,
  infAmend: `${SCENARIO_PREFIX}.inf_amend`,
  revAmend: `${SCENARIO_PREFIX}.rev_amend`,
  revCci: `${SCENARIO_PREFIX}.rev_cci`,
  showHidden: `${SCENARIO_PREFIX}.show_hidden`,
} as const;

// StorageLike is the tiny subset of the Web Storage API the scenario
// helpers actually use. Lets the tests pass a Map-backed fake without
// pulling in a full localStorage polyfill.
export interface StorageLike {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

// readEventChoices is forgiving: malformed tuples or unknown
// choice_kinds are dropped silently. Same shape as the legacy URL
// codec (comma-separated `submission_code:kind=value`).
export function readEventChoices(storage: StorageLike): EventChoice[] {
  const raw = storage.getItem(SCENARIO_KEYS.eventChoices);
  if (!raw) return [];
  const out: EventChoice[] = [];
  for (const tuple of raw.split(",")) {
    const m = tuple.match(/^([^:]+):([^=]+)=(.+)$/);
    if (!m) continue;
    const kind = m[2] as ChoiceKind;
    if (kind !== "appellant" && kind !== "include_ccr" && kind !== "skip") continue;
    out.push({ submission_code: m[1], choice_kind: kind, choice_value: m[3] });
  }
  return out;
}

export function writeEventChoices(storage: StorageLike, choices: EventChoice[]): void {
  if (choices.length === 0) {
    storage.removeItem(SCENARIO_KEYS.eventChoices);
    return;
  }
  const enc = choices
    .map((c) => `${c.submission_code}:${c.choice_kind}=${c.choice_value}`)
    .join(",");
  storage.setItem(SCENARIO_KEYS.eventChoices, enc);
}

// readCourtId / writeCourtId — empty string == no court picked. The
// "" value is stored as a removed key, not an empty string entry, so
// reading it back yields null rather than "".
export function readCourtId(storage: StorageLike): string {
  return storage.getItem(SCENARIO_KEYS.courtId) ?? "";
}

export function writeCourtId(storage: StorageLike, courtId: string): void {
  if (courtId === "") {
    storage.removeItem(SCENARIO_KEYS.courtId);
    return;
  }
  storage.setItem(SCENARIO_KEYS.courtId, courtId);
}

// Boolean flags — "1" / "0" string encoding, removeItem on default
// (false for flags, also false for show_hidden) so the storage stays
// uncluttered on a fresh page.
export function readBoolFlag(storage: StorageLike, key: string): boolean {
  return storage.getItem(key) === "1";
}

export function writeBoolFlag(storage: StorageLike, key: string, on: boolean): void {
  if (on) storage.setItem(key, "1");
  else storage.removeItem(key);
}

// Read all scenario state in one call — convenience for the page's
// load-time hydration. Caller decides whether to apply each field
// (e.g. court_id is proceeding-specific; the page may discard the
// stored value if the active proceeding doesn't expose a court row).
export interface ScenarioState {
  eventChoices: EventChoice[];
  courtId: string;
  ccr: boolean;
  infAmend: boolean;
  revAmend: boolean;
  revCci: boolean;
  showHidden: boolean;
}

export function readScenario(storage: StorageLike): ScenarioState {
  return {
    eventChoices: readEventChoices(storage),
    courtId: readCourtId(storage),
    ccr: readBoolFlag(storage, SCENARIO_KEYS.ccr),
    infAmend: readBoolFlag(storage, SCENARIO_KEYS.infAmend),
    revAmend: readBoolFlag(storage, SCENARIO_KEYS.revAmend),
    revCci: readBoolFlag(storage, SCENARIO_KEYS.revCci),
    showHidden: readBoolFlag(storage, SCENARIO_KEYS.showHidden),
  };
}

// ----- URL → localStorage hydration order -------------------------

// The page's load-time contract: read URL filters, then read
// scenario state from localStorage. URL wins on conflict — but the
// only field that can conflict is none of them today (URL owns
// proceeding/side/target/trigger_date; localStorage owns the rest).
// The order matters for one edge case: if a future field migrates
// from URL → localStorage with overlap, the URL value MUST be honored.

export interface HydratedState extends ScenarioState {
  proceeding: string;
  side: Side;
  target: AppealTarget;
  triggerDate: string;
}

export function hydrate(search: string, storage: StorageLike): HydratedState {
  const scenario = readScenario(storage);
  return {
    proceeding: parseProceedingFromSearch(search),
    side: parseSideFromSearch(search),
    target: parseAppealTargetFromSearch(search),
    triggerDate: parseTriggerDateFromSearch(search),
    ...scenario,
  };
}

// makeMemoryStorage — tiny StorageLike for tests / SSR fallback.
// Not used by the runtime page (which mounts real localStorage), but
// kept here so test files have one well-known import.
export function makeMemoryStorage(): StorageLike {
  const store = new Map<string, string>();
  return {
    getItem: (k) => (store.has(k) ? store.get(k)! : null),
    setItem: (k, v) => { store.set(k, v); },
    removeItem: (k) => { store.delete(k); },
  };
}
