// Shared core for Fristenrechner-style proceeding-timeline rendering.
//
// Both /tools/fristenrechner (deadline determination) and
// /tools/verfahrensablauf (abstract browse — t-paliad-179 Slice 1) call
// POST /api/tools/fristenrechner and paint the result with the same
// renderers. The module is pure-functional: no shared mutable state, all
// language / overrides / editability flow in through args so the two
// pages can wire their own per-page concerns (Akte save, anchor edits,
// Pathway B etc. on fristenrechner; variant chips, compare etc. coming
// to verfahrensablauf in later slices) without leaking into each other.

import { t, tDyn, getLang } from "../i18n";

export interface AdjustmentHoliday {
  Date: string;
  Name: string;
  IsVacation: boolean;
  IsClosure: boolean;
}

export interface AdjustmentReason {
  kind: "weekend" | "public_holiday" | "vacation";
  holidays?: AdjustmentHoliday[];
  vacation_name?: string;
  vacation_start?: string;
  vacation_end?: string;
  original_weekday?: string;
}

export interface CalculatedDeadline {
  code: string;
  name: string;
  nameEN: string;
  party: string;
  // Priority is the canonical 4-way enum (Slice 8 made it canonical;
  // Slice 9 dropped the legacy isMandatory / isOptional pair from the
  // wire). priorityRendering(d) below branches on it.
  priority: "mandatory" | "recommended" | "optional" | "informational";
  ruleRef: string;
  legalSource?: string;
  // legalSourceDisplay is the pretty form ("UPC RoP R.220(1)") produced
  // by FormatLegalSourceDisplay on the backend. Renderer prefers this
  // over ruleRef when set; falls back to ruleRef otherwise.
  legalSourceDisplay?: string;
  // legalSourceURL is the youpc.org/laws permalink when the cited body
  // is hosted there (UPCRoP / UPCA / UPCS today). Empty for DE/EPA/EU
  // bodies — the renderer shows display text without a link.
  legalSourceURL?: string;
  notes?: string;
  notesEN?: string;
  dueDate: string;
  originalDate: string;
  wasAdjusted: boolean;
  adjustmentReason?: AdjustmentReason;
  isRootEvent: boolean;
  isCourtSet: boolean;
  isCourtSetIndirect?: boolean;
  isOverridden?: boolean;
  // conditionExpr surfaces the jsonb gate predicate (design §2.4) so
  // the rule-editor + admin views can render the rule's gating shape.
  // Frontend save-modal logic doesn't read this; the rule editor
  // (Slice 11) is the consumer. Unknown shape on this side — pass-through.
  conditionExpr?: unknown;
}

// priorityRendering returns the per-priority UX hints the save-modal
// uses. Maps the unified Priority enum to:
//   - preChecked: whether the save-modal pre-checks the row
//   - hideSave:   whether the row renders without a save button at all
//                 (informational = notice card, no save action)
//
// Phase 3 Slice 9 (t-paliad-195) dropped the legacy
// (isMandatory, isOptional) fallback that pre-Slice-8 backends
// emitted. The backend now always populates `priority`; an unknown
// value falls back to "render as mandatory" (safe default — never
// silently drop a rule).
export function priorityRendering(
  d: CalculatedDeadline,
): { preChecked: boolean; hideSave: boolean } {
  switch (d.priority) {
    case "mandatory":
    case "recommended":
      return { preChecked: true, hideSave: false };
    case "optional":
      return { preChecked: false, hideSave: false };
    case "informational":
      return { preChecked: false, hideSave: true };
  }
  // Unknown priority value: pre-Slice-8 backend or a forward-compat
  // future value. Safe default: render as mandatory so the rule is
  // surfaced + saved. Never silently drop.
  return { preChecked: true, hideSave: false };
}

export interface DeadlineResponse {
  proceedingType: string;
  proceedingName: string;
  triggerDate: string;
  deadlines: CalculatedDeadline[];
}

export interface CourtRow {
  id: string;
  code: string;
  nameDE: string;
  nameEN: string;
  country: string;
  regime?: string;
  courtType: string;
}

export interface CalcParams {
  proceedingType: string;
  triggerDate: string;
  priorityDate?: string;
  flags?: string[];
  anchorOverrides?: Record<string, string>;
  courtId?: string;
}

const PARTY_CLASS: Record<string, string> = {
  claimant: "party-claimant",
  defendant: "party-defendant",
  court: "party-court",
  both: "party-both",
};

// ─── small helpers ─────────────────────────────────────────────────────────

export function escAttr(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}

export function escHtml(s: string): string {
  const d = document.createElement("div");
  d.textContent = s;
  return d.innerHTML;
}

export function formatDate(dateStr: string): string {
  if (!dateStr) return "—";
  const d = new Date(dateStr + "T00:00:00");
  if (getLang() === "en") {
    const weekday = d.toLocaleDateString("en-US", { weekday: "short" });
    const yyyy = d.getFullYear();
    const mm = String(d.getMonth() + 1).padStart(2, "0");
    const dd = String(d.getDate()).padStart(2, "0");
    return `${weekday}, ${yyyy}-${mm}-${dd}`;
  }
  return d.toLocaleDateString("de-DE", {
    weekday: "short",
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  });
}

function formatDateSpan(startISO: string, endISO: string): string {
  const start = new Date(startISO + "T00:00:00");
  const end = new Date(endISO + "T00:00:00");
  if (getLang() === "en") {
    const fmt = (d: Date) => d.toLocaleDateString("en-US", { day: "numeric", month: "short" });
    return `${fmt(start)} – ${fmt(end)}`;
  }
  const fmt = (d: Date) => `${d.getDate()}.${d.getMonth() + 1}.`;
  return `${fmt(start)}–${fmt(end)}`;
}

function localizeWeekday(en: string): string {
  if (en === "Saturday") return t("deadlines.adjusted.weekend.saturday");
  if (en === "Sunday") return t("deadlines.adjusted.weekend.sunday");
  return en;
}

// Vacation names come straight from paliad.holidays (e.g. "UPC judicial
// vacation"). Not translated — they're proper names of court-set closures.
function localizeVacationName(name: string): string {
  return name;
}

function renderAdjustmentReason(r: AdjustmentReason): string {
  if (r.kind === "vacation" && r.vacation_name && r.vacation_start && r.vacation_end) {
    const span = formatDateSpan(r.vacation_start, r.vacation_end);
    return tDyn("deadlines.adjusted.vacation")
      .replace("{name}", localizeVacationName(r.vacation_name))
      .replace("{span}", span);
  }
  if (r.kind === "public_holiday" && r.holidays && r.holidays.length > 0) {
    return tDyn("deadlines.adjusted.holiday").replace("{name}", r.holidays[0].Name);
  }
  if (r.kind === "weekend" && r.original_weekday) {
    return localizeWeekday(r.original_weekday);
  }
  return t("deadlines.adjusted.weekend");
}

function formatAdjustedNote(dl: CalculatedDeadline): string {
  const arrow = `${formatDate(dl.originalDate)} → ${formatDate(dl.dueDate)}`;
  const reason = dl.adjustmentReason
    ? renderAdjustmentReason(dl.adjustmentReason)
    : t("deadlines.adjusted.reason");
  if (getLang() === "en") {
    return `${t("deadlines.adjusted")} (${reason}): ${arrow}`;
  }
  return `${t("deadlines.adjusted")} wegen ${reason}: ${arrow}`;
}

export function partyBadge(party: string): string {
  const cls = PARTY_CLASS[party] || "party-both";
  return `<span class="party-badge ${cls}">${tDyn("deadlines.party." + party)}</span>`;
}

// ─── card + body renderers ────────────────────────────────────────────────

export interface CardOpts {
  showParty: boolean;
  // editable=true wires the click-to-edit affordance: data-rule-code,
  // role=button, tabindex, hover hint. Fristenrechner enables it; the
  // verfahrensablauf abstract-browse surface keeps editable=false because
  // there's no anchor-override state on that page in Slice 1.
  editable?: boolean;
  // showNotes controls how the per-rule descriptive notes render:
  //   true  → expanded `<div class="timeline-notes">…</div>` below the card
  //   false → compact ⓘ icon next to the meta line, full text on hover
  //           (browser-native `title` attribute) and screen-reader-readable
  // Page shells expose a toggle ("Hinweise anzeigen") that flips this and
  // re-renders. Default false — notes are noisy on long timelines.
  showNotes?: boolean;
}

export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string {
  const wantsEditable = !!opts.editable;
  const editable = wantsEditable && !dl.isRootEvent && dl.code !== "";
  const overriddenClass = dl.isOverridden ? " timeline-date--overridden" : "";
  const editAttrs = editable
    ? ` data-rule-code="${escAttr(dl.code)}" data-current-date="${escAttr(dl.dueDate)}" role="button" tabindex="0" title="${escAttr(t("deadlines.date.edit.hint"))}"`
    : "";
  const courtLabelKey = dl.isCourtSetIndirect
    ? "deadlines.court.indirect"
    : "deadlines.court.set";
  const dateStr = dl.isCourtSet
    ? `<span class="timeline-court-set frist-date-edit"${editAttrs}>${t(courtLabelKey)}</span>`
    : `<span class="timeline-date${overriddenClass} frist-date-edit"${editAttrs}>${formatDate(dl.dueDate)}</span>`;

  // Slice 9 (t-paliad-195): the legacy boolean pair is gone — read
  // priority directly. Optional badge fires only on 'optional'
  // priority (RoP.151-style opt-in deadlines).
  const mandatoryBadge = dl.priority === "optional"
    ? '<span class="optional-badge">optional</span>'
    : "";

  const dlName = getLang() === "en" ? dl.nameEN : dl.name;

  const adjustedNote = dl.wasAdjusted
    ? `<div class="timeline-adjusted">⚠ ${formatAdjustedNote(dl)}</div>`
    : "";

  // Prefer the structured legalSource (pretty display + youpc.org link
  // when hosted there) over the bare rule_code fallback. UPC.RoP rules
  // link to /laws/UPCRoP/<n>; DE / EPA / EU bodies have no youpc home
  // yet so we render display text plain.
  const legalDisplay = dl.legalSourceDisplay || "";
  const legalURL = dl.legalSourceURL || "";
  let ruleRef = "";
  if (legalDisplay && legalURL) {
    ruleRef = `<a class="timeline-rule timeline-rule--link" href="${escAttr(legalURL)}" target="_blank" rel="noopener noreferrer">${escHtml(legalDisplay)}</a>`;
  } else if (legalDisplay) {
    ruleRef = `<span class="timeline-rule">${escHtml(legalDisplay)}</span>`;
  } else if (dl.ruleRef) {
    ruleRef = `<span class="timeline-rule">${escHtml(dl.ruleRef)}</span>`;
  }

  const noteText = getLang() === "en" ? (dl.notesEN || dl.notes) : dl.notes;
  const showNotes = opts.showNotes === true;
  const notesBlock = noteText && showNotes
    ? `<div class="timeline-notes">${noteText}</div>`
    : "";
  const noteHint = noteText && !showNotes
    ? `<span class="timeline-note-hint" tabindex="0" role="note" aria-label="${escAttr(noteText)}" title="${escAttr(noteText)}">ⓘ</span>`
    : "";

  const meta = (opts.showParty || ruleRef || noteHint)
    ? `<div class="timeline-meta">
        ${opts.showParty ? partyBadge(dl.party) : ""}
        ${ruleRef}
        ${noteHint}
      </div>`
    : "";

  return `<div class="timeline-item-header">
      <span class="timeline-name">
        ${dlName}
        ${mandatoryBadge}
      </span>
      ${dateStr}
    </div>
    ${meta}
    ${adjustedNote}
    ${notesBlock}`;
}

// ─── inline date editor (click-to-edit per-rule due date) ────────────────
//
// The renderer emits `<span class="frist-date-edit" data-rule-code="…"
// data-current-date="YYYY-MM-DD" role="button" tabindex="0">…</span>` when
// CardOpts.editable is true. Pages call wireDateEditClicks() on their
// result container once, and the delegated click/keydown handlers swap a
// clicked span for a `<input type="date">` editor via openInlineDateEditor.
// The caller's onCommit callback receives (ruleCode, newValue) — an empty
// newValue means "revert" (clear the anchor override and let the calculator
// re-project). The actual recompute is the caller's job — they own the
// anchor-overrides map + the calc dispatch.

export function openInlineDateEditor(
  span: HTMLElement,
  onCommit: (ruleCode: string, newValue: string) => void,
): void {
  const ruleCode = span.dataset.ruleCode || "";
  if (!ruleCode) return;
  const current = span.dataset.currentDate || "";
  const editor = document.createElement("input");
  editor.type = "date";
  editor.className = "frist-date-edit-input";
  editor.value = current;

  let done = false;
  const cancel = () => {
    if (done) return;
    done = true;
    editor.replaceWith(span);
  };
  const commit = (newValue: string) => {
    if (done) return;
    done = true;
    onCommit(ruleCode, newValue);
  };

  editor.addEventListener("blur", () => {
    if (editor.value !== current) commit(editor.value);
    else cancel();
  });
  editor.addEventListener("keydown", (e) => {
    const ke = e as KeyboardEvent;
    if (ke.key === "Enter") {
      e.preventDefault();
      editor.blur();
    } else if (ke.key === "Escape") {
      e.preventDefault();
      cancel();
    }
  });

  span.replaceWith(editor);
  editor.focus();
  if (editor.value) editor.select();
}

// wireDateEditClicks attaches delegated click + keyboard handlers to the
// timeline result container so click-to-edit survives every innerHTML
// rewrite the page does on recalc. Idempotent — re-calling on the same
// container does nothing (the dataset flag short-circuits).
export function wireDateEditClicks(
  container: HTMLElement,
  onCommit: (ruleCode: string, newValue: string) => void,
): void {
  if (container.dataset.dateEditWired === "1") return;
  container.dataset.dateEditWired = "1";
  container.addEventListener("click", (e) => {
    const target = (e.target as HTMLElement).closest<HTMLElement>(".frist-date-edit");
    if (!target || !target.dataset.ruleCode) return;
    openInlineDateEditor(target, onCommit);
  });
  container.addEventListener("keydown", (e) => {
    const ke = e as KeyboardEvent;
    if (ke.key !== "Enter" && ke.key !== " ") return;
    const target = (e.target as HTMLElement).closest<HTMLElement>(".frist-date-edit");
    if (!target || !target.dataset.ruleCode) return;
    e.preventDefault();
    openInlineDateEditor(target, onCommit);
  });
}

export function renderTimelineBody(data: DeadlineResponse, opts: CardOpts = { showParty: true }): string {
  let html = '<div class="timeline">';
  for (const dl of data.deadlines) {
    html += `
      <div class="timeline-item ${dl.isRootEvent ? "timeline-root" : ""}">
        <div class="timeline-dot-col">
          <div class="timeline-dot ${dl.isRootEvent ? "dot-root" : ""}"></div>
          <div class="timeline-line"></div>
        </div>
        <div class="timeline-content">
          ${deadlineCardHtml(dl, opts)}
        </div>
      </div>
    `;
  }
  html += "</div>";
  return html;
}

// Three-column timeline layout: Proactive (claimant) | Court | Reactive
// (defendant). Each grid row shares a dueDate so same-day events line up
// across columns; party=both renders in BOTH the Proactive and Reactive
// cells of the row. Undated rows (Urteil etc.) trail the dated tail, each
// keyed by sequence-order so e.g. Urteil precedes Berufungseinlegung.
export function renderColumnsBody(data: DeadlineResponse, opts: Omit<CardOpts, "showParty"> = {}): string {
  type Cell = CalculatedDeadline[];
  type Row = { proactive: Cell; court: Cell; reactive: Cell };

  const UNSCHEDULED_PREFIX = "__unscheduled__";
  const rowsMap = new Map<string, Row>();
  const ensureRow = (key: string): Row => {
    let r = rowsMap.get(key);
    if (!r) {
      r = { proactive: [], court: [], reactive: [] };
      rowsMap.set(key, r);
    }
    return r;
  };

  data.deadlines.forEach((dl, idx) => {
    const key = dl.dueDate || `${UNSCHEDULED_PREFIX}${String(idx).padStart(4, "0")}`;
    const row = ensureRow(key);
    switch (dl.party) {
      case "claimant":
        row.proactive.push(dl);
        break;
      case "defendant":
        row.reactive.push(dl);
        break;
      case "court":
        row.court.push(dl);
        break;
      case "both":
        row.proactive.push(dl);
        row.reactive.push(dl);
        break;
      default:
        row.court.push(dl);
    }
  });

  const datedKeys: string[] = [];
  const unscheduledKeys: string[] = [];
  for (const k of rowsMap.keys()) {
    if (k.startsWith(UNSCHEDULED_PREFIX)) unscheduledKeys.push(k);
    else datedKeys.push(k);
  }
  datedKeys.sort();
  unscheduledKeys.sort();
  const keys = [...datedKeys, ...unscheduledKeys];

  const cardOpts: CardOpts = { showParty: false, editable: opts.editable, showNotes: opts.showNotes };

  const renderCell = (items: CalculatedDeadline[]): string => {
    if (items.length === 0) {
      return `<div class="fr-col-cell fr-col-cell--empty"></div>`;
    }
    const cards = items
      .map((dl) => {
        const mirrorTag = dl.party === "both"
          ? `<div class="fr-col-mirror">↔ ${escHtml(t("deadlines.party.both.label"))}</div>`
          : "";
        return `<div class="fr-col-item ${dl.isRootEvent ? "fr-col-root" : ""}">
          ${deadlineCardHtml(dl, cardOpts)}
          ${mirrorTag}
        </div>`;
      })
      .join("");
    return `<div class="fr-col-cell">${cards}</div>`;
  };

  const headerCell = (label: string, cls: string) =>
    `<div class="fr-col-header ${cls}">${escHtml(label)}</div>`;

  let html = '<div class="fr-columns-view">';
  html += headerCell(t("deadlines.col.proactive"), "fr-col-proactive");
  html += headerCell(t("deadlines.col.court"), "fr-col-court");
  html += headerCell(t("deadlines.col.reactive"), "fr-col-reactive");

  for (const key of keys) {
    const row = rowsMap.get(key)!;
    html += renderCell(row.proactive);
    html += renderCell(row.court);
    html += renderCell(row.reactive);
  }
  html += "</div>";
  return html;
}

// ─── calculate fetch wrapper ──────────────────────────────────────────────

export async function calculateDeadlines(params: CalcParams): Promise<DeadlineResponse | null> {
  if (!params.proceedingType || !params.triggerDate) return null;
  try {
    const resp = await fetch("/api/tools/fristenrechner", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        proceedingType: params.proceedingType,
        triggerDate: params.triggerDate,
        priorityDate: params.priorityDate || undefined,
        flags: params.flags && params.flags.length > 0 ? params.flags : undefined,
        anchorOverrides: params.anchorOverrides && Object.keys(params.anchorOverrides).length > 0
          ? params.anchorOverrides
          : undefined,
        courtId: params.courtId || undefined,
      }),
    });
    if (!resp.ok) {
      const err = await resp.json().catch(() => ({}));
      console.error("API error:", err);
      return null;
    }
    return (await resp.json()) as DeadlineResponse;
  } catch (e) {
    console.error("Fetch error:", e);
    return null;
  }
}

// ─── court picker ─────────────────────────────────────────────────────────

const courtCache = new Map<string, CourtRow[]>();

export function courtTypesFor(proceedingType: string): string[] {
  if (proceedingType === "upc.apl.merits" || proceedingType === "upc.apl.order" || proceedingType === "upc.apl.cost") {
    return ["UPC-CoA"];
  }
  if (proceedingType === "upc.rev.cfi") {
    return ["UPC-CD", "UPC-LD"];
  }
  if (proceedingType.startsWith("upc.")) {
    return ["UPC-LD"];
  }
  return [];
}

export function defaultCourtFor(proceedingType: string): string {
  if (proceedingType === "upc.apl.merits" || proceedingType === "upc.apl.order" || proceedingType === "upc.apl.cost") {
    return "upc-coa-luxembourg";
  }
  if (proceedingType === "upc.rev.cfi") {
    return "upc-cd-paris";
  }
  return "upc-ld-muenchen";
}

export async function fetchCourts(courtType: string): Promise<CourtRow[]> {
  if (courtCache.has(courtType)) return courtCache.get(courtType)!;
  try {
    const resp = await fetch(`/api/tools/courts?courtType=${encodeURIComponent(courtType)}`);
    if (!resp.ok) return [];
    const rows = (await resp.json()) as CourtRow[];
    courtCache.set(courtType, rows);
    return rows;
  } catch {
    return [];
  }
}

// populateCourtPicker fills the <select> for the proceeding's compatible
// court types. The row + select IDs are passed in so each page can own
// its own DOM scope. Visible only when the proceeding has ≥2 compatible
// courts; otherwise hidden (server resolves the jurisdiction default).
export async function populateCourtPicker(
  rowId: string,
  selectId: string,
  proceedingType: string,
): Promise<void> {
  const row = document.getElementById(rowId);
  const select = document.getElementById(selectId) as HTMLSelectElement | null;
  if (!row || !select) return;

  const types = courtTypesFor(proceedingType);
  if (types.length === 0) {
    row.style.display = "none";
    select.innerHTML = "";
    return;
  }

  const lists = await Promise.all(types.map((c) => fetchCourts(c)));
  const courts = lists.flat();
  if (courts.length <= 1) {
    row.style.display = "none";
    select.innerHTML = "";
    return;
  }

  const lang = getLang();
  const defaultID = defaultCourtFor(proceedingType);
  select.innerHTML = courts.map((c) => {
    const name = lang === "en" ? c.nameEN : c.nameDE;
    return `<option value="${escAttr(c.id)}"${c.id === defaultID ? " selected" : ""}>${escHtml(name)}</option>`;
  }).join("");
  row.style.display = "";
}
