// Fristenrechner overhaul Mode B — "Geführt" / wizard (design §3.2).
//
// 3-5 question row stack that lands the user on one procedural_event
// (the trigger), then transitions to the shared §4 result view.
//
//   R1  Was ist passiert?           (event_kind)               always asked
//   R2  Vor welchem Gericht?        (jurisdiction)             skip if R1 narrows
//   R3  In welchem Verfahren?       (proceeding_type)          auto-skip when 1 option
//   R4  Welches Schriftstück?       (procedural_event — land)  always asked
//   R5  Welche Seite vertreten Sie? (party)                    only when follow-ups differ
//
// Row badges per §11.Q3: R1+R2 = "Filter", R3+R4+R5 = "Qualifier".
// R5 has NO "Beide" option per §11.Q8 (Mode B is the file-mode where
// perspective is a qualifier).
// Pre-fill + collapse rows from project (project.proceeding_type →
// R3 + R2 derived; project.our_side → R5). Preserve compatible
// downstream picks on back-navigation (§11.Q10).

import { escAttr, escHtml } from "./views/verfahrensablauf-core";
import { getLang, t, tDyn } from "./i18n";
import { mountResultView } from "./fristenrechner-result";

// Wire shapes — duplicates the parts of fristenrechner-mode-a.ts we
// need; kept local so the wizard doesn't depend on Mode A.

interface EventSearchHit {
  id: string;
  code: string;
  name_de: string;
  name_en: string;
  event_kind?: string;
  proceeding_type: {
    id: number;
    code: string;
    name_de: string;
    name_en: string;
    jurisdiction?: string;
  };
  follow_up_count: number;
}

interface EventSearchResponse {
  events: EventSearchHit[];
  total: number;
}

interface ProceedingChip {
  code: string;
  name: string;
  nameEN: string;
  group: string;
}

interface ProjectSummary {
  id: string;
  proceeding_type_id?: number | null;
  our_side?: string | null;
}

type Forum = "UPC" | "DE" | "EPA" | "DPMA";
type EventKindRow = "filing" | "hearing" | "decision" | "order" | "missed";
type WizardParty = "claimant" | "defendant";

// WIZARD_HOST_ID is the DOM id the wizard renders into. Mounted by
// fristenrechner-result.mountModeShell which creates the host element
// under the overhaul root.
const WIZARD_HOST_ID = "fristen-overhaul-mode-host";

// FORUMS + EVENT_KINDS — closed sets. Keep parallel to Mode A's lists
// so re-grouping happens in one place.
const FORUMS: Forum[] = ["UPC", "DE", "EPA", "DPMA"];
const EVENT_KINDS: EventKindRow[] = ["filing", "hearing", "decision", "order", "missed"];

// Single wizard state. Module-local; one wizard at a time.
interface WizardState {
  // Picks. "" = not answered. R5 only set when the question is asked.
  r1: EventKindRow | "";
  r2: Forum | "";
  r3: string;        // proceeding_types.code
  r4: string;        // procedural_events.code
  r5: WizardParty | "";

  // Pre-fill provenance — when a pick came from the project context,
  // the row renders with an "aus Akte" tag so the user notices.
  r2FromProject: boolean;
  r3FromProject: boolean;
  r5FromProject: boolean;

  // Implicit fills — R2 auto-derived from R1 when R1 narrows to one
  // forum (e.g. "missed" → no narrowing, "filing" → cross-forum, but
  // if downstream R3 lookup returns a single forum we can mark R2 as
  // implicit).
  r2Implicit: boolean;
  r3Implicit: boolean;
}

const state: WizardState = {
  r1: "", r2: "", r3: "", r4: "", r5: "",
  r2FromProject: false, r3FromProject: false, r5FromProject: false,
  r2Implicit: false, r3Implicit: false,
};

// Loaded from the project (if any).
let projectSummary: ProjectSummary | null = null;

// Proceeding chip cache key: jurisdiction × event_kind.
let lastProcCacheKey = "";
let cachedProcChips: ProceedingChip[] = [];

// Event chip cache: keyed on R3 code + R1 event_kind.
let lastEventCacheKey = "";
let cachedEventChips: EventSearchHit[] = [];

// Public API ---------------------------------------------------------

export async function mountWizard(): Promise<void> {
  const host = document.getElementById(WIZARD_HOST_ID);
  if (!host) return;

  // Hydrate from URL state (mode=wizard&forum=UPC&pt=upc.inf.cfi&…).
  const params = new URLSearchParams(window.location.search);
  state.r1 = (params.get("kind") as EventKindRow) || "";
  state.r2 = (params.get("forum") as Forum) || "";
  state.r3 = params.get("pt") || "";
  state.r4 = params.get("event") || "";
  state.r5 = (params.get("party") as WizardParty) || "";

  // Project prefills.
  const projectId = params.get("project");
  if (projectId) {
    projectSummary = await fetchProject(projectId);
    await applyProjectPrefills();
  } else {
    projectSummary = null;
  }

  renderShell();
  void renderRows();
}

// applyProjectPrefills derives R2 + R3 + R5 from the project when they
// haven't been set explicitly. Project picks take precedence over
// unspecified state, but a user-supplied URL pick wins over the
// project default.
async function applyProjectPrefills(): Promise<void> {
  if (!projectSummary) return;
  // Map our_side → R5.
  if (!state.r5) {
    const side = projectSummary.our_side;
    if (side === "claimant" || side === "applicant" || side === "appellant") {
      state.r5 = "claimant";
      state.r5FromProject = true;
    } else if (side === "defendant" || side === "respondent") {
      state.r5 = "defendant";
      state.r5FromProject = true;
    }
  }
  // Map proceeding_type_id → R3 + infer R2 jurisdiction.
  if (projectSummary.proceeding_type_id && !state.r3) {
    const pt = await fetchProceedingByID(projectSummary.proceeding_type_id);
    if (pt) {
      state.r3 = pt.code;
      state.r3FromProject = true;
      if (pt.group && !state.r2) {
        state.r2 = pt.group as Forum;
        state.r2FromProject = true;
      }
    }
  }
}

// Render -------------------------------------------------------------

function renderShell(): void {
  const host = document.getElementById(WIZARD_HOST_ID);
  if (!host) return;
  host.innerHTML = `
    <div class="fristen-wizard-root">
      <header class="fristen-wizard-header">
        <h2 class="fristen-wizard-title">${escHtml(t("deadlines.overhaul.wizard.heading"))}</h2>
        <p class="fristen-wizard-hint">${escHtml(t("deadlines.overhaul.wizard.hint"))}</p>
      </header>
      <div class="fristen-wizard-rows" id="fristen-wizard-rows" aria-live="polite"></div>
    </div>
  `;
}

async function renderRows(): Promise<void> {
  const host = document.getElementById("fristen-wizard-rows");
  if (!host) return;

  // Resolve dynamic row prerequisites BEFORE building markup so chip
  // sets are populated.
  if (state.r1 && state.r2) {
    await ensureProceedingChips(state.r2, state.r1);
    // Auto-skip R3 when the narrowed pool has exactly one option.
    if (!state.r3 && cachedProcChips.length === 1) {
      state.r3 = cachedProcChips[0].code;
      state.r3Implicit = true;
    }
  }
  if (state.r1 && state.r3) {
    await ensureEventChips(state.r3, state.r1);
  }

  const rows: string[] = [];
  rows.push(rowR1());
  if (shouldShowR2()) rows.push(rowR2());
  if (shouldShowR3()) rows.push(rowR3());
  if (shouldShowR4()) rows.push(rowR4());
  if (state.r4 && shouldShowR5Sync()) rows.push(rowR5Loading());

  host.innerHTML = rows.join("");
  wireRowEvents();

  // R5 conditional check — fires after R4 picked. Inspects /follow-ups
  // to see whether they actually differ by party. If yes, show R5. If
  // no, or R5 already set, transition straight to result view.
  if (state.r4) {
    void maybeAdvanceFromR4();
  }
}

// Should-show predicates --------------------------------------------

function shouldShowR2(): boolean {
  // Skip R2 only when R1 narrows to a single forum — which today
  // never happens for the closed event_kind set (every kind exists in
  // multiple jurisdictions). Always show R2 until we have empirical
  // evidence otherwise.
  return state.r1 !== "" && state.r1 !== "missed";
}

function shouldShowR3(): boolean {
  if (state.r1 === "" || state.r2 === "") return false;
  if (state.r3 && state.r3Implicit) return true; // visible compact
  return true;
}

function shouldShowR4(): boolean {
  return state.r3 !== "" && state.r1 !== "";
}

// shouldShowR5Sync renders the placeholder row immediately; the actual
// asked-or-not decision happens after the async follow-ups probe in
// maybeAdvanceFromR4.
function shouldShowR5Sync(): boolean {
  return state.r4 !== "";
}

// Row builders ------------------------------------------------------

function rowR1(): string {
  const chips = EVENT_KINDS.map((k) => {
    const label = t(`deadlines.overhaul.kind.${k}` as never);
    const icon = eventKindIcon(k);
    return chipHtml("r1", k, label, state.r1 === k, icon);
  }).join("");
  return rowShell({
    n: 1,
    badge: "filter",
    label: t("deadlines.overhaul.wizard.r1.label"),
    active: !state.r1,
    answeredText: state.r1 ? t(`deadlines.overhaul.kind.${state.r1}` as never) : "",
    body: `<div class="fristen-wizard-chips">${chips}</div>`,
  });
}

function rowR2(): string {
  const chips = FORUMS.map((f) => chipHtml("r2", f, f, state.r2 === f)).join("");
  return rowShell({
    n: 2,
    badge: "filter",
    label: t("deadlines.overhaul.wizard.r2.label"),
    active: !state.r2,
    fromProject: state.r2FromProject,
    answeredText: state.r2 || "",
    body: `<div class="fristen-wizard-chips">${chips}</div>`,
  });
}

function rowR3(): string {
  if (cachedProcChips.length === 0) {
    return rowShell({
      n: 3, badge: "qualifier",
      label: t("deadlines.overhaul.wizard.r3.label"),
      active: true,
      body: `<div class="fristen-wizard-empty">${escHtml(t("deadlines.overhaul.wizard.r3.empty"))}</div>`,
    });
  }
  const lang = getLang();
  const chips = cachedProcChips.map((p) => {
    const label = lang === "en" ? p.nameEN || p.name : p.name;
    return chipHtml("r3", p.code, label, state.r3 === p.code, undefined, p.code);
  }).join("");
  let answered = "";
  if (state.r3) {
    const hit = cachedProcChips.find((p) => p.code === state.r3);
    if (hit) answered = lang === "en" ? hit.nameEN || hit.name : hit.name;
  }
  return rowShell({
    n: 3,
    badge: "qualifier",
    label: t("deadlines.overhaul.wizard.r3.label"),
    active: !state.r3,
    fromProject: state.r3FromProject,
    implicit: state.r3Implicit,
    answeredText: answered,
    body: `<div class="fristen-wizard-chips">${chips}</div>`,
  });
}

function rowR4(): string {
  if (cachedEventChips.length === 0) {
    return rowShell({
      n: 4, badge: "qualifier",
      label: t("deadlines.overhaul.wizard.r4.label"),
      active: true,
      body: `<div class="fristen-wizard-empty">${escHtml(t("deadlines.overhaul.wizard.r4.empty"))}</div>`,
    });
  }
  const lang = getLang();
  const chips = cachedEventChips.map((e) => {
    const label = lang === "en" ? e.name_en || e.name_de : e.name_de;
    return chipHtml("r4", e.code, label, state.r4 === e.code, eventKindIcon(e.event_kind as EventKindRow));
  }).join("");
  let answered = "";
  if (state.r4) {
    const hit = cachedEventChips.find((e) => e.code === state.r4);
    if (hit) answered = lang === "en" ? hit.name_en || hit.name_de : hit.name_de;
  }
  return rowShell({
    n: 4,
    badge: "qualifier",
    label: t("deadlines.overhaul.wizard.r4.label"),
    active: !state.r4,
    answeredText: answered,
    body: `<div class="fristen-wizard-chips">${chips}</div>`,
  });
}

function rowR5Loading(): string {
  // Placeholder while we probe whether R5 is needed. The async
  // follow-ups probe replaces this with rowR5 chips or skips
  // straight to the result view.
  return rowShell({
    n: 5, badge: "qualifier",
    label: t("deadlines.overhaul.wizard.r5.label"),
    active: !state.r5,
    fromProject: state.r5FromProject,
    answeredText: state.r5 ? t(`deadlines.party.${state.r5}` as never) : "",
    body: `<div class="fristen-wizard-probe">${escHtml(t("deadlines.overhaul.wizard.r5.probing"))}</div>`,
  });
}

function rowR5Chips(): string {
  const chips = (["claimant", "defendant"] as const).map((p) =>
    chipHtml("r5", p, t(`deadlines.party.${p}` as never), state.r5 === p)).join("");
  return rowShell({
    n: 5, badge: "qualifier",
    label: t("deadlines.overhaul.wizard.r5.label"),
    active: !state.r5,
    fromProject: state.r5FromProject,
    answeredText: state.r5 ? t(`deadlines.party.${state.r5}` as never) : "",
    body: `<div class="fristen-wizard-chips">${chips}</div>`,
  });
}

interface RowShellOpts {
  n: number;
  badge: "filter" | "qualifier";
  label: string;
  active: boolean;
  body: string;
  answeredText?: string;
  fromProject?: boolean;
  implicit?: boolean;
}

function rowShell(o: RowShellOpts): string {
  const cls = `fristen-wizard-row fristen-wizard-row--${o.badge}` +
    (o.active ? " is-active" : " is-answered") +
    (o.fromProject ? " is-from-project" : "") +
    (o.implicit ? " is-implicit" : "");
  const badgeText = o.badge === "filter"
    ? t("deadlines.overhaul.wizard.badge.filter")
    : t("deadlines.overhaul.wizard.badge.qualifier");
  const annotations: string[] = [];
  if (o.fromProject) annotations.push(`<span class="fristen-wizard-row-anno">${escHtml(t("deadlines.overhaul.wizard.anno.from_project"))}</span>`);
  if (o.implicit) annotations.push(`<span class="fristen-wizard-row-anno">${escHtml(t("deadlines.overhaul.wizard.anno.implicit"))}</span>`);
  const answered = o.answeredText
    ? `<span class="fristen-wizard-row-answer">${escHtml(o.answeredText)}</span>`
    : "";
  const edit = !o.active
    ? `<button type="button" class="fristen-wizard-row-edit" data-row="${o.n}">${escHtml(t("deadlines.overhaul.wizard.edit"))}</button>`
    : "";
  return `
    <section class="${cls}" data-row="${o.n}">
      <header class="fristen-wizard-row-header">
        <span class="fristen-wizard-row-n">${o.n}</span>
        <span class="fristen-wizard-row-badge fristen-wizard-row-badge--${o.badge}">${escHtml(badgeText)}</span>
        <span class="fristen-wizard-row-label">${escHtml(o.label)}</span>
        ${annotations.join("")}
        ${answered}
        ${edit}
      </header>
      ${o.active ? `<div class="fristen-wizard-row-body">${o.body}</div>` : ""}
    </section>
  `;
}

// Event wiring ------------------------------------------------------

function wireRowEvents(): void {
  document.querySelectorAll<HTMLButtonElement>(".fristen-wizard-row .fristen-mode-a-chip").forEach((btn) => {
    btn.addEventListener("click", () => {
      const axis = btn.dataset.axis || "";
      const value = btn.dataset.value || "";
      handleChip(axis, value);
    });
  });
  document.querySelectorAll<HTMLButtonElement>(".fristen-wizard-row-edit").forEach((btn) => {
    btn.addEventListener("click", () => {
      const n = parseInt(btn.dataset.row || "0", 10);
      handleEdit(n);
    });
  });
}

function handleChip(axis: string, value: string): void {
  switch (axis) {
    case "r1": {
      if (state.r1 === value) return;
      state.r1 = value as EventKindRow;
      // R1 change resets R3/R4 (event-kind narrows the pools).
      state.r3 = "";
      state.r3Implicit = false;
      state.r4 = "";
      state.r5 = state.r5FromProject ? state.r5 : "";
      cachedEventChips = [];
      lastEventCacheKey = "";
      cachedProcChips = [];
      lastProcCacheKey = "";
      break;
    }
    case "r2": {
      if (state.r2 === value) return;
      state.r2 = value as Forum;
      state.r2FromProject = false;
      state.r2Implicit = false;
      // R2 change may invalidate R3 → reset.
      state.r3 = "";
      state.r3FromProject = false;
      state.r3Implicit = false;
      state.r4 = "";
      cachedProcChips = [];
      lastProcCacheKey = "";
      cachedEventChips = [];
      lastEventCacheKey = "";
      break;
    }
    case "r3": {
      if (state.r3 === value) return;
      state.r3 = value;
      state.r3FromProject = false;
      state.r3Implicit = false;
      state.r4 = "";
      cachedEventChips = [];
      lastEventCacheKey = "";
      break;
    }
    case "r4": {
      if (state.r4 === value) return;
      state.r4 = value;
      break;
    }
    case "r5": {
      if (state.r5 === value) return;
      state.r5 = value as WizardParty;
      state.r5FromProject = false;
      break;
    }
  }
  syncUrl();
  void renderRows();
}

function handleEdit(n: number): void {
  switch (n) {
    case 1:
      state.r1 = ""; state.r2 = ""; state.r3 = ""; state.r4 = ""; state.r5 = state.r5FromProject ? state.r5 : "";
      cachedProcChips = []; lastProcCacheKey = "";
      cachedEventChips = []; lastEventCacheKey = "";
      break;
    case 2:
      state.r2 = ""; state.r2FromProject = false; state.r2Implicit = false;
      state.r3 = ""; state.r3FromProject = false; state.r3Implicit = false;
      state.r4 = "";
      cachedProcChips = []; lastProcCacheKey = "";
      cachedEventChips = []; lastEventCacheKey = "";
      break;
    case 3:
      state.r3 = ""; state.r3FromProject = false; state.r3Implicit = false;
      state.r4 = "";
      cachedEventChips = []; lastEventCacheKey = "";
      break;
    case 4:
      state.r4 = "";
      state.r5 = state.r5FromProject ? state.r5 : "";
      break;
    case 5:
      state.r5 = ""; state.r5FromProject = false;
      break;
  }
  syncUrl();
  void renderRows();
}

// maybeAdvanceFromR4 fetches /follow-ups for the picked event to
// decide whether R5 is needed. If R5 is already set OR the
// follow-ups don't differ by party, transition straight to the
// result view. Else swap the R5 loading row for the chip picker.
async function maybeAdvanceFromR4(): Promise<void> {
  if (!state.r4) return;
  if (state.r5) {
    // R5 already answered (project prefill or explicit pick) → go.
    void launchResult();
    return;
  }
  // Probe follow-ups.
  const url = new URL("/api/tools/fristenrechner/follow-ups", window.location.origin);
  url.searchParams.set("event", state.r4);
  try {
    const resp = await fetch(url.toString(), { headers: { Accept: "application/json" } });
    if (!resp.ok) {
      // Soft-fail → swap to R5 chips so the user can decide manually.
      swapR5(rowR5Chips());
      return;
    }
    const data = (await resp.json()) as { follow_ups: Array<{ primary_party?: string }> };
    const differs = followUpsDifferByParty(data.follow_ups);
    if (!differs) {
      void launchResult();
      return;
    }
    swapR5(rowR5Chips());
  } catch {
    swapR5(rowR5Chips());
  }
}

function swapR5(html: string): void {
  const host = document.getElementById("fristen-wizard-rows");
  if (!host) return;
  const r5 = host.querySelector('.fristen-wizard-row[data-row="5"]');
  if (!r5) {
    host.insertAdjacentHTML("beforeend", html);
  } else {
    r5.outerHTML = html;
  }
  wireRowEvents();
}

function launchResult(): void {
  // Hand off to the §4 result view. The URL already carries the
  // picks via syncUrl(); add event= so the boot path treats this
  // as a deep-link.
  const url = new URL(window.location.href);
  url.searchParams.set("overhaul", "1");
  url.searchParams.set("event", state.r4);
  if (state.r5) url.searchParams.set("party", state.r5);
  else url.searchParams.delete("party");
  history.pushState(null, "", url.pathname + url.search + url.hash);
  void mountResultView({ eventRef: state.r4, party: state.r5 || undefined });
}

export function followUpsDifferByParty(rows: Array<{ primary_party?: string }>): boolean {
  let hasClaimant = false, hasDefendant = false;
  for (const r of rows) {
    if (r.primary_party === "claimant") hasClaimant = true;
    else if (r.primary_party === "defendant") hasDefendant = true;
    if (hasClaimant && hasDefendant) return true;
  }
  return false;
}

// Fetches -----------------------------------------------------------

async function fetchProject(id: string): Promise<ProjectSummary | null> {
  try {
    const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`, { headers: { Accept: "application/json" } });
    if (!resp.ok) return null;
    return (await resp.json()) as ProjectSummary;
  } catch {
    return null;
  }
}

async function fetchProceedingByID(id: number): Promise<ProceedingChip | null> {
  // The proceeding-types endpoint returns codes, names, jurisdictions
  // but doesn't carry the id (the wire shape FristenrechnerType is
  // code-keyed). Walk the unfiltered list and pick by sort-order
  // proximity / sort-fallback: we need the row whose id matches; since
  // the wire doesn't expose id, fetch the projects detail to get the
  // code directly. Cheap workaround: rely on /api/projects/{id}'s
  // proceeding_type_id being matched against the proceeding-types list
  // by jurisdiction round-trip is not possible without id. Instead
  // expose the proceeding-types-by-id mapping via a follow-up endpoint
  // later. For now hit the unfiltered list and assume the project's
  // pick is in the active set.
  //
  // Pragmatic fallback: query the full list and return the only entry
  // whose pseudo-id-via-sort-order matches. The lookup is unreliable
  // until the wire shape includes id; for the project-prefill case the
  // user can always re-pick R3 / R2 if the prefill misfires.
  try {
    const resp = await fetch(`/api/tools/proceeding-types`, { headers: { Accept: "application/json" } });
    if (!resp.ok) return null;
    const list = (await resp.json()) as ProceedingChip[] | null;
    if (!list || list.length === 0) return null;
    // Without id in the wire we cannot match by id. Skip the prefill
    // silently — R3 stays unanswered and the user picks manually.
    // (S5/follow-up can extend the wire shape to include id.)
    void id;
    return null;
  } catch {
    return null;
  }
}

async function ensureProceedingChips(forum: Forum, kind: EventKindRow): Promise<void> {
  const key = `${forum}\x00${kind}`;
  if (lastProcCacheKey === key) return;
  lastProcCacheKey = key;
  const url = new URL("/api/tools/proceeding-types", window.location.origin);
  url.searchParams.set("kind", "proceeding");
  url.searchParams.set("jurisdiction", forum);
  if (kind !== "missed") url.searchParams.set("event_kind", kind);
  try {
    const resp = await fetch(url.toString(), { headers: { Accept: "application/json" } });
    if (!resp.ok) {
      cachedProcChips = [];
      return;
    }
    const data = (await resp.json()) as ProceedingChip[] | null;
    cachedProcChips = data || [];
  } catch {
    cachedProcChips = [];
  }
}

async function ensureEventChips(procCode: string, kind: EventKindRow): Promise<void> {
  const key = `${procCode}\x00${kind}`;
  if (lastEventCacheKey === key) return;
  lastEventCacheKey = key;
  const url = new URL("/api/tools/fristenrechner/search", window.location.origin);
  url.searchParams.set("kind", "events");
  url.searchParams.set("proc", procCode);
  if (kind !== "missed") url.searchParams.set("event_kind", kind);
  url.searchParams.set("limit", "100");
  try {
    const resp = await fetch(url.toString(), { headers: { Accept: "application/json" } });
    if (!resp.ok) {
      cachedEventChips = [];
      return;
    }
    const data = (await resp.json()) as EventSearchResponse;
    cachedEventChips = data.events || [];
  } catch {
    cachedEventChips = [];
  }
}

// Helpers -----------------------------------------------------------

function chipHtml(axis: string, value: string, label: string, active: boolean, icon?: string, title?: string): string {
  const cls = `fristen-mode-a-chip${active ? " is-active" : ""}`;
  const tt = title ? ` title="${escAttr(title)}"` : "";
  const i = icon ? `<span class="fristen-mode-a-chip-icon" aria-hidden="true">${icon}</span>` : "";
  return `<button type="button" class="${cls}" data-axis="${escAttr(axis)}" data-value="${escAttr(value)}"${tt}>${i}<span class="fristen-mode-a-chip-label">${escHtml(label)}</span></button>`;
}

function eventKindIcon(kind?: EventKindRow): string {
  switch (kind) {
    case "filing":   return "&#128229;";
    case "hearing":  return "&#127963;&#65039;";
    case "decision": return "&#9878;&#65039;";
    case "order":    return "&#128220;";
    case "missed":   return "&#9202;";
    default:         return "&#128197;";
  }
}

function syncUrl(): void {
  const url = new URL(window.location.href);
  url.searchParams.set("overhaul", "1");
  url.searchParams.set("mode", "wizard");
  setOrClear(url, "kind", state.r1);
  setOrClear(url, "forum", state.r2);
  setOrClear(url, "pt", state.r3);
  // event=… is set only on launchResult; the wizard URL carries the
  // R4 candidate via r4= so back/forward navigates within the wizard.
  setOrClear(url, "r4", state.r4);
  setOrClear(url, "party", state.r5);
  history.replaceState(null, "", url.pathname + url.search + url.hash);
}

function setOrClear(url: URL, key: string, val: string): void {
  if (val) url.searchParams.set(key, val);
  else url.searchParams.delete(key);
}
