// /tools/verfahrensablauf client (t-paliad-179 Slice 1)
//
// Abstract-browse surface: pick a proceeding, pick a trigger date,
// see the typical timeline. No Akte, no save-to-project, no anchor
// override editing, no Pathway B cascade. Variant chips + lane view
// (Slice 3) and compare (Slice 4) layer on top of this in later
// slices. Court picker + view toggle + calc fetch + renderers all
// come from ./views/verfahrensablauf-core, which fristenrechner.ts
// shares.

import { initI18n, t, tDyn, getLang, onLangChange } from "./i18n";
import { initSidebar } from "./sidebar";
import {
  type DeadlineResponse,
  type Side,
  calculateDeadlines,
  escHtml,
  formatDate,
  populateCourtPicker,
  renderColumnsBody,
  renderTimelineBody,
  wireDateEditClicks,
} from "./views/verfahrensablauf-core";

let selectedType = "";
let lastResponse: DeadlineResponse | null = null;

// Perspective state (t-paliad-250 / m/paliad#81). URL-driven so the
// view is shareable and survives reload:
//   ?side=claimant|defendant      → swaps which column owns the user's
//                                    side (proactive vs reactive label).
//                                    Default null = claimant-on-the-left.
//   ?appellant=claimant|defendant → collapses party=both rows into the
//                                    appellant's column (no mirror).
//                                    Only meaningful for role-swap
//                                    proceedings (Appeal etc.). Default
//                                    null = legacy mirror behaviour.
let currentSide: Side = null;
let currentAppellant: Side = null;

// Proceedings where one party initiates and "both" rows are role-swap
// (i.e. either party files depending on who acted at the lower
// instance). For these proceedings the appellant selector is meaningful
// — when set, "both" rows collapse to a single row in the appellant's
// column. For first-instance proceedings (Inf, Rev, …) the selector is
// hidden because there's no appellant axis.
//
// Today: every upc.apl.* family member plus dpma.appeal.* and
// de.inf.olg / de.inf.bgh / de.null.bgh (DE Berufung / Revision).
// Conservative — false negatives just hide a control; false positives
// would show an irrelevant control.
const APPELLANT_AXIS_PROCEEDINGS = new Set([
  "upc.apl.merits",
  "upc.apl.cost",
  "upc.apl.order",
  "de.inf.olg",
  "de.inf.bgh",
  "de.null.bgh",
  "dpma.appeal.bpatg",
  "dpma.appeal.bgh",
  "epa.opp.boa",
]);

function hasAppellantAxis(proceedingType: string): boolean {
  return APPELLANT_AXIS_PROCEEDINGS.has(proceedingType);
}

function readSideFromURL(): Side {
  const raw = new URLSearchParams(window.location.search).get("side");
  return raw === "claimant" || raw === "defendant" ? raw : null;
}

function readAppellantFromURL(): Side {
  const raw = new URLSearchParams(window.location.search).get("appellant");
  return raw === "claimant" || raw === "defendant" ? raw : null;
}

function writeSideToURL(s: Side) {
  const url = new URL(window.location.href);
  if (s === null) url.searchParams.delete("side");
  else url.searchParams.set("side", s);
  window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
}

function writeAppellantToURL(a: Side) {
  const url = new URL(window.location.href);
  if (a === null) url.searchParams.delete("appellant");
  else url.searchParams.set("appellant", a);
  window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
}

// Per-rule anchor overrides set by the click-to-edit affordance on
// timeline / column date cells. Posted as `anchorOverrides` to the
// /api/tools/fristenrechner calc so downstream rules re-anchor off the
// user's chosen date. Cleared whenever the trigger changes (proceeding,
// trigger date, flag toggle) so a fresh calc starts unanchored — same
// semantic as /tools/fristenrechner.
const anchorOverrides = new Map<string, string>();
function clearAnchorOverrides() { anchorOverrides.clear(); }

type ProcedureView = "timeline" | "columns";
let procedureView: ProcedureView = "columns";

// Notes toggle — when off (default), per-rule descriptive notes render
// as a compact ⓘ icon next to the meta line (hover for full text). When
// on, the full notes block expands under each card. Choice persists in
// localStorage so a reload or recalc keeps the user's preference.
const NOTES_PREF_KEY = "paliad.fristen.notes-show";
function readNotesPref(): boolean {
  try { return localStorage.getItem(NOTES_PREF_KEY) === "1"; } catch { return false; }
}
function writeNotesPref(on: boolean): void {
  try { localStorage.setItem(NOTES_PREF_KEY, on ? "1" : "0"); } catch { /* no-op */ }
}
let showNotes = readNotesPref();

// Jurisdiction display prefix for the proceeding-summary chip + the
// trigger-event placeholder. Same forum slugs the .proceeding-group
// `data-forum` attribute carries in verfahrensablauf.tsx /
// fristenrechner.tsx (upc / de / epa / dpma). Disambiguates the
// 4 redundancies in the corpus (UPC Verletzungsverfahren vs DE
// Verletzungsklage etc.) once the picker collapses.
const FORUM_LABEL: Record<string, string> = {
  upc: "UPC",
  de: "DE",
  epa: "EPA",
  dpma: "DPMA",
};

function jurisdictionFor(btn: HTMLButtonElement): string {
  const group = btn.closest<HTMLElement>(".proceeding-group");
  const forum = group?.dataset.forum || "";
  return FORUM_LABEL[forum] || "";
}

function proceedingDisplayName(btn: HTMLButtonElement): string {
  const name = btn.querySelector("strong")?.textContent || "";
  const jur = jurisdictionFor(btn);
  return jur ? `${jur} ${name}` : name;
}

function activeProceedingButton(): HTMLButtonElement | null {
  return document.querySelector<HTMLButtonElement>(".proceeding-btn.active");
}

// Auto-calc plumbing — sequence + debounce mirror /tools/fristenrechner
// so rapid input changes never let a stale response overwrite a fresh
// one.
let calcSeq = 0;
let calcTimer: ReturnType<typeof setTimeout> | null = null;

function scheduleCalc(delayMs = 200) {
  if (calcTimer !== null) clearTimeout(calcTimer);
  calcTimer = setTimeout(() => {
    calcTimer = null;
    void doCalc();
  }, delayMs);
}

function showStep(n: number) {
  for (let i = 1; i <= 3; i++) {
    const el = document.getElementById(`step-${i}`);
    if (el) el.style.display = i <= n ? "block" : "none";
  }
}

// Read the proceeding-specific flag checkboxes and assemble the
// payload the calculator expects. Mirrors fristenrechner.ts so the
// gating semantics stay identical: with_amend on upc.inf.cfi is
// nested under with_ccr (R.30 is only available with a CCR);
// upc.rev.cfi exposes with_amend + with_cci as two independent
// gates. R.19 Einspruch is NOT flag-gated (mig 098, m's 2026-05-18
// call): it's just an always-available optional submission, so it
// has no checkbox.
function readFlags(): string[] {
  const ccr      = document.getElementById("ccr-flag")        as HTMLInputElement | null;
  const infAmend = document.getElementById("inf-amend-flag")  as HTMLInputElement | null;
  const revAmend = document.getElementById("rev-amend-flag")  as HTMLInputElement | null;
  const revCci   = document.getElementById("rev-cci-flag")    as HTMLInputElement | null;
  const flags: string[] = [];
  if (selectedType === "upc.inf.cfi") {
    if (ccr?.checked)                       flags.push("with_ccr");
    if (ccr?.checked && infAmend?.checked)  flags.push("with_amend");
  }
  if (selectedType === "upc.rev.cfi") {
    if (revAmend?.checked) flags.push("with_amend");
    if (revCci?.checked)   flags.push("with_cci");
  }
  return flags;
}

async function doCalc() {
  const seq = ++calcSeq;
  const dateInput = document.getElementById("trigger-date") as HTMLInputElement | null;
  const triggerDate = dateInput?.value || "";
  if (!triggerDate || !selectedType) return;

  const courtPickerRow = document.getElementById("court-picker-row");
  const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null;
  const courtId = courtPickerRow && courtPickerRow.style.display !== "none" && courtPicker?.value
    ? courtPicker.value
    : "";

  const overrides: Record<string, string> = {};
  for (const [code, date] of anchorOverrides) overrides[code] = date;

  const data = await calculateDeadlines({
    proceedingType: selectedType,
    triggerDate,
    flags: readFlags(),
    anchorOverrides: overrides,
    courtId,
  });
  if (seq !== calcSeq) return;
  if (!data) return;
  lastResponse = data;
  renderResults(data);
  showStep(3);
}

// triggerEventLabelFor picks the user-facing "Auslösendes Ereignis"
// label from the calc response. Precedence:
//
//   1. Server-supplied triggerEventLabel from proceeding_types
//      (mig 121, m/paliad#81). UPC Appeal sets this to
//      "Anfechtbare Entscheidung" / "Appealable Decision" — its rules
//      all carry a non-zero duration off the trigger date so none is
//      the root, and the proceedingName fallback ("Berufungsverfahren")
//      misnamed the input as the proceeding itself.
//   2. Root rule (isRootEvent=true) — the first event in the
//      proceeding, e.g. Klageerhebung for upc.inf.cfi,
//      Nichtigkeitsklage for upc.rev.cfi.
//   3. Active proceeding name — last-resort fallback. Language-aware
//      (m/paliad#58: prior code rendered DE on EN for sub-track
//      proceedings like upc.ccr.cfi which had no rules → no root).
function triggerEventLabelFor(data: DeadlineResponse): string {
  const lang = getLang();
  const curated = lang === "en"
    ? (data.triggerEventLabelEN || data.triggerEventLabel)
    : (data.triggerEventLabel || data.triggerEventLabelEN);
  if (curated) return curated;
  const root = data.deadlines.find((d) => d.isRootEvent);
  if (root) {
    return lang === "en" ? (root.nameEN || root.name) : (root.name || root.nameEN);
  }
  if (lang === "en") {
    return data.proceedingNameEN || data.proceedingName || "";
  }
  return data.proceedingName || data.proceedingNameEN || "";
}

function syncTriggerEventLabel() {
  const triggerEventEl = document.getElementById("trigger-event");
  if (!triggerEventEl) return;
  if (lastResponse) {
    triggerEventEl.textContent = triggerEventLabelFor(lastResponse);
  } else {
    triggerEventEl.textContent = "—";
  }
}

function renderResults(data: DeadlineResponse) {
  const container = document.getElementById("timeline-container");
  if (!container) return;
  const printBtn = document.getElementById("fristen-print-btn");
  const toggle = document.getElementById("fristen-view-toggle");

  // Header shows the picked proceeding with its jurisdiction prefix
  // so the user can tell UPC Verletzungsverfahren apart from DE
  // Verletzungsklage once the picker collapses.
  const activeBtn = activeProceedingButton();
  const procName = activeBtn ? proceedingDisplayName(activeBtn)
    : tDyn(`deadlines.${data.proceedingType.toLowerCase()}`);
  const headerHtml = `<div class="timeline-header">
    <strong>${procName}</strong>
    <span class="timeline-trigger-date">${t("deadlines.trigger.label")}: ${formatDate(data.triggerDate)}</span>
  </div>`;

  // Sub-track contextual note (m/paliad#58). Surfaces above the
  // timeline body when the server routed the user-picked proceeding
  // through a parent (e.g. upc.ccr.cfi → upc.inf.cfi with with_ccr).
  // Plain-text banner — server-side copy is plain text per the
  // SubTrackRouting contract.
  const noteText = getLang() === "en"
    ? (data.contextualNoteEN || data.contextualNote || "")
    : (data.contextualNote || data.contextualNoteEN || "");
  const noteHtml = noteText
    ? `<div class="timeline-context-note" role="note">${escHtml(noteText)}</div>`
    : "";

  const bodyHtml = procedureView === "columns"
    ? renderColumnsBody(data, {
        editable: true,
        showNotes,
        side: currentSide,
        appellant: hasAppellantAxis(selectedType) ? currentAppellant : null,
      })
    : renderTimelineBody(data, { showParty: true, editable: true, showNotes });

  container.innerHTML = headerHtml + noteHtml + bodyHtml;
  if (printBtn) printBtn.style.display = "block";
  if (toggle) toggle.style.display = "";

  syncTriggerEventLabel();
}

function setProceedingPickerCollapsed(collapsed: boolean, displayName?: string) {
  const groups = document.querySelectorAll<HTMLElement>(".proceeding-group");
  const summary = document.getElementById("proceeding-summary") as HTMLElement | null;
  const summaryName = document.getElementById("proceeding-summary-name");
  groups.forEach((g) => { g.style.display = collapsed ? "none" : ""; });
  if (summary) summary.style.display = collapsed ? "" : "none";
  if (summaryName && displayName) summaryName.textContent = displayName;
}

// syncFlagRows shows/hides the proceeding-specific checkbox rows
// based on selectedType. Same disposition as fristenrechner.ts —
// the with_amend nested-under-ccr semantic is enforced via
// syncInfAmendEnabled().
function syncFlagRows() {
  const show = (id: string, when: boolean) => {
    const el = document.getElementById(id);
    if (el) el.style.display = when ? "" : "none";
  };
  show("ccr-flag-row",       selectedType === "upc.inf.cfi");
  show("inf-amend-flag-row", selectedType === "upc.inf.cfi");
  show("rev-amend-flag-row", selectedType === "upc.rev.cfi");
  show("rev-cci-flag-row",   selectedType === "upc.rev.cfi");
  syncInfAmendEnabled();
}

// R.30 amendment-application is only available with a CCR — disable
// (and clear) the nested inf-amend checkbox while ccr is off so the
// calc payload stays coherent. Mirrors fristenrechner.ts.
function syncInfAmendEnabled() {
  const ccr      = document.getElementById("ccr-flag")       as HTMLInputElement | null;
  const infAmend = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
  if (!ccr || !infAmend) return;
  infAmend.disabled = !ccr.checked;
  if (!ccr.checked) infAmend.checked = false;
}

function selectProceeding(btn: HTMLButtonElement) {
  document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
  btn.classList.add("active");
  const nextType = btn.dataset.code || "";
  // Different proceeding tree → previously-set overrides reference
  // rule codes that don't exist in the new tree. Clear before the
  // next calc so the fresh proceeding starts unanchored.
  if (selectedType !== nextType) clearAnchorOverrides();
  selectedType = nextType;

  // Trigger-event label fires from the calc response (root rule).
  // Until step 3 renders, fall back to an em-dash placeholder.
  lastResponse = null;
  syncTriggerEventLabel();

  void populateCourtPicker("court-picker-row", "court-picker", selectedType);
  syncFlagRows();
  syncAppellantRowVisibility();

  setProceedingPickerCollapsed(true, proceedingDisplayName(btn));

  showStep(2);
  scheduleCalc(0);
}

// syncAppellantRowVisibility hides the appellant selector for
// proceedings that have no appellant axis (first-instance Inf, Rev,
// …). Clears the in-memory state and the URL param when hidden so a
// shared link with ?appellant= doesn't leak into an unrelated
// proceeding's render.
function syncAppellantRowVisibility() {
  const row = document.getElementById("appellant-row");
  if (!row) return;
  const visible = hasAppellantAxis(selectedType);
  row.style.display = visible ? "" : "none";
  if (!visible && currentAppellant !== null) {
    currentAppellant = null;
    writeAppellantToURL(null);
    syncRadioGroup("appellant", "");
  }
}

function syncRadioGroup(name: string, value: string) {
  document.querySelectorAll<HTMLInputElement>(`input[type=radio][name=${name}]`).forEach((input) => {
    input.checked = input.value === value;
  });
}

function applyVerfahrensablaufViewBodyClass(view: ProcedureView) {
  // Mirrors the events.ts pattern (body.events-view-*). The print
  // stylesheet keys `body.verfahrensablauf-view-timeline` to
  // `@page paliad-landscape`, so flipping this class is what lets a
  // user print the horizontal timeline in landscape without affecting
  // the columns view (which stays portrait).
  document.body.classList.toggle("verfahrensablauf-view-timeline", view === "timeline");
  document.body.classList.toggle("verfahrensablauf-view-columns", view === "columns");
}

function initViewToggle() {
  const toggle = document.getElementById("fristen-view-toggle");
  if (!toggle) return;

  const initial = new URLSearchParams(window.location.search).get("view");
  if (initial === "timeline") procedureView = "timeline";
  applyVerfahrensablaufViewBodyClass(procedureView);

  toggle.querySelectorAll<HTMLInputElement>("input[name=fristen-view]").forEach((input) => {
    input.checked = input.value === procedureView;
    input.addEventListener("change", () => {
      if (!input.checked) return;
      procedureView = input.value === "columns" ? "columns" : "timeline";
      applyVerfahrensablaufViewBodyClass(procedureView);
      const url = new URL(window.location.href);
      if (procedureView === "columns") {
        url.searchParams.delete("view");
      } else {
        url.searchParams.set("view", procedureView);
      }
      history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
      if (lastResponse) renderResults(lastResponse);
    });
  });

  toggle.style.display = "none";
}

// initPerspectiveControls hydrates side+appellant from the URL,
// reflects state into the radio inputs, and wires onchange handlers
// that update state + URL + re-render. Re-render path skips the
// /api/tools/fristenrechner round-trip — perspective is a pure
// projection of the last response, no backend involved.
function initPerspectiveControls() {
  currentSide = readSideFromURL();
  currentAppellant = readAppellantFromURL();
  syncRadioGroup("side", currentSide ?? "");
  syncRadioGroup("appellant", currentAppellant ?? "");

  document.querySelectorAll<HTMLInputElement>("input[type=radio][name=side]").forEach((input) => {
    input.addEventListener("change", () => {
      if (!input.checked) return;
      const v = input.value;
      currentSide = (v === "claimant" || v === "defendant") ? v : null;
      writeSideToURL(currentSide);
      if (lastResponse) renderResults(lastResponse);
    });
  });

  document.querySelectorAll<HTMLInputElement>("input[type=radio][name=appellant]").forEach((input) => {
    input.addEventListener("change", () => {
      if (!input.checked) return;
      const v = input.value;
      currentAppellant = (v === "claimant" || v === "defendant") ? v : null;
      writeAppellantToURL(currentAppellant);
      if (lastResponse) renderResults(lastResponse);
    });
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();

  document.querySelectorAll<HTMLButtonElement>(".proceeding-btn").forEach((btn) => {
    btn.addEventListener("click", () => selectProceeding(btn));
  });

  document.getElementById("proceeding-summary-reselect")?.addEventListener("click", () => {
    setProceedingPickerCollapsed(false);
  });

  document.getElementById("calculate-btn")?.addEventListener("click", () => scheduleCalc(0));

  const dateInput = document.getElementById("trigger-date") as HTMLInputElement | null;
  if (dateInput) {
    dateInput.addEventListener("change", () => scheduleCalc());
    dateInput.addEventListener("input", () => scheduleCalc());
    dateInput.addEventListener("keydown", (e) => {
      if ((e as KeyboardEvent).key === "Enter") scheduleCalc(0);
    });
  }

  const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null;
  if (courtPicker) courtPicker.addEventListener("change", () => scheduleCalc(0));

  // Flag-checkbox listeners — each flip triggers a fresh calc so the
  // timeline re-projects with the new gating. ccr-flag additionally
  // enables/disables the nested inf-amend row.
  const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
  if (ccrFlag) ccrFlag.addEventListener("change", () => {
    syncInfAmendEnabled();
    scheduleCalc(0);
  });
  (["inf-amend-flag", "rev-amend-flag", "rev-cci-flag"]).forEach((id) => {
    const cb = document.getElementById(id) as HTMLInputElement | null;
    if (cb) cb.addEventListener("change", () => scheduleCalc(0));
  });

  document.getElementById("fristen-print-btn")?.addEventListener("click", () => window.print());

  // Click-to-edit on timeline / column date cells — same delegated
  // pattern as /tools/fristenrechner. Survives renderResults()'s
  // innerHTML rewrites because the listener lives on the container.
  const timelineContainer = document.getElementById("timeline-container");
  if (timelineContainer) {
    wireDateEditClicks(timelineContainer, (ruleCode, newValue) => {
      if (newValue === "") {
        anchorOverrides.delete(ruleCode);
      } else {
        anchorOverrides.set(ruleCode, newValue);
      }
      scheduleCalc(0);
    });
  }

  // Notes toggle — restores last preference on load + re-renders when
  // the user flips it. Lives in the same toggle bar as the view picker.
  const notesShowCb = document.getElementById("fristen-notes-show") as HTMLInputElement | null;
  if (notesShowCb) {
    notesShowCb.checked = showNotes;
    notesShowCb.addEventListener("change", () => {
      showNotes = notesShowCb.checked;
      writeNotesPref(showNotes);
      if (lastResponse) renderResults(lastResponse);
    });
  }

  initViewToggle();
  initPerspectiveControls();

  onLangChange(() => {
    // Active-button name updates with language change (the data-i18n
    // pass swaps the inner <strong>'s text). Re-collapse the summary
    // chip and re-derive the trigger event label from the lang-current
    // calc response.
    const activeBtn = activeProceedingButton();
    if (activeBtn) {
      const summary = document.getElementById("proceeding-summary-name");
      if (summary) summary.textContent = proceedingDisplayName(activeBtn);
    }
    if (lastResponse) renderResults(lastResponse);
    syncTriggerEventLabel();
  });

  // Pre-select the first proceeding tile so users see a timeline
  // immediately on landing — matches /tools/fristenrechner behaviour.
  const firstBtn = document.querySelector<HTMLButtonElement>(".proceeding-btn");
  if (firstBtn) selectProceeding(firstBtn);
});
