// ProceedingTriplet renderer for the Litigation Builder.
//
// PRD §3.3 + §3.4 + §6.1: one triplet = jurisdiction badge + name +
// perspective + Detailgrad + columnar `proaktiv | court | reaktiv`
// body.
//
// B2 wires the live controls — perspective radio, scenario-flag strip,
// remove button, collapse — and the per-event-card overlays (3-state
// machine, action buttons, optional-horizon chip). The 3-column body
// itself is still produced by verfahrensablauf-core.renderColumnsBody;
// per-card overlays are layered on top after innerHTML write via the
// data-rule-id hooks added in the same slice.

import { t, tDyn, getLang } from "./i18n";
import type { DeadlineResponse, Side } from "./views/verfahrensablauf-core";
import type { BuilderProceeding, BuilderEvent } from "./builder";
import type { ProceedingTypeMeta } from "./builder-picker";

export interface ScenarioFlagCatalogEntry {
  flag_key: string;
  label_de: string;
  label_en: string;
  description?: string;
  hidden_unless_set: boolean;
}

export interface TripletViewInput {
  proceeding: BuilderProceeding;
  meta: ProceedingTypeMeta;
  data: DeadlineResponse | null;
  side: Side;
  // Flag catalog filtered to the keys the active proceeding actually
  // references via its rules' condition_expr. B2 passes the global
  // catalog and lets the user toggle any — flags that don't gate any
  // rule are simply no-ops on this triplet.
  flagCatalog: ScenarioFlagCatalogEntry[];
  // Map keyed by sequencing_rule_id (lowercased UUID) → BuilderEvent
  // for the per-card state machine. Cards whose rule is absent default
  // to "planned".
  eventsByRule: Map<string, BuilderEvent>;
  // Per-card optional-horizon registry. Each rule with optional
  // children carries a `+N Optionen` chip; the chip's count comes from
  // here (defaults to scenario_events.horizon_optional, falls back to
  // proceeding-level when not stored per-card).
  columnsHtml: string;
  isChild: boolean;
}

// Triplet header + controls + columns body. Pure-string render; the
// caller (builder.ts) wires click handlers on top.
export function renderTriplet(input: TripletViewInput): string {
  const lang = getLang();
  const procLabel = lang === "en"
    ? (input.meta.nameEN || input.meta.name)
    : (input.meta.name || input.meta.nameEN);
  const flagsBadge = activeFlagsBadge(input.proceeding.scenario_flags);

  const body = input.data
    ? input.columnsHtml
    : `<div class="builder-triplet-loading">${escHtml(t("builder.triplet.loading"))}</div>`;

  const controls = renderControls(input);
  const flagStrip = renderFlagStrip(input);

  return `
    <header class="builder-triplet-header">
      <span class="builder-triplet-jurisdiction">${escHtml(jurisdictionFor(input.meta))}</span>
      <span class="builder-triplet-code">${escHtml(input.meta.code)}</span>
      <span class="builder-triplet-name">${escHtml(procLabel)}</span>
      ${flagsBadge}
    </header>
    ${controls}
    ${flagStrip}
    <div class="builder-triplet-body">
      ${body}
    </div>
  `;
}

function renderControls(input: TripletViewInput): string {
  const perspective = input.side ?? "";
  const detailgrad = input.proceeding.detailgrad || "selected";

  const radio = (value: string, key: string, current: string): string => {
    const active = value === current ? " is-active" : "";
    return `<button type="button" class="builder-triplet-perspective-btn${active}"
              data-action="perspective" data-value="${escAttr(value)}">${escHtml(tDyn(key))}</button>`;
  };
  const detailBtn = (value: string, key: string, current: string): string => {
    const active = value === current ? " is-active" : "";
    return `<button type="button" class="builder-triplet-detailgrad-btn${active}"
              data-action="detailgrad" data-value="${escAttr(value)}">${escHtml(tDyn(key))}</button>`;
  };

  return `<div class="builder-triplet-controls">
    <span class="builder-triplet-controls-label">${escHtml(t("builder.triplet.perspective.label"))}</span>
    <div class="builder-triplet-perspective">
      ${radio("", "builder.triplet.perspective.none", perspective)}
      ${radio("claimant", "builder.triplet.perspective.claimant", perspective)}
      ${radio("defendant", "builder.triplet.perspective.defendant", perspective)}
    </div>
    <span class="builder-triplet-controls-label">${escHtml(t("builder.triplet.detailgrad.label"))}</span>
    <div class="builder-triplet-detailgrad">
      ${detailBtn("selected", "builder.triplet.detailgrad.selected", detailgrad)}
      ${detailBtn("all_options", "builder.triplet.detailgrad.all_options", detailgrad)}
    </div>
    <button type="button" class="builder-triplet-remove" data-action="remove">
      ${escHtml(t("builder.triplet.remove"))}
    </button>
  </div>`;
}

function renderFlagStrip(input: TripletViewInput): string {
  // B2 ships the full global catalog. Flags that don't gate any of the
  // active proceeding's rules are still toggle-able but have no effect
  // on the calc result (the engine simply doesn't read them).
  const lang = getLang();
  const flags = input.proceeding.scenario_flags || {};
  if (input.flagCatalog.length === 0) {
    return `<div class="builder-triplet-flagstrip">
      <span class="builder-triplet-flag-empty">${escHtml(t("builder.triplet.no_flags"))}</span>
    </div>`;
  }
  const toggles = input.flagCatalog.map((entry) => {
    const label = lang === "en" ? entry.label_en : entry.label_de;
    const isOn = flags[entry.flag_key] === true;
    return `<label class="builder-triplet-flag-toggle">
      <input type="checkbox"
             data-action="flag"
             data-flag-key="${escAttr(entry.flag_key)}"
             ${isOn ? "checked" : ""} />
      <span>${escHtml(label)}</span>
    </label>`;
  }).join("");
  return `<div class="builder-triplet-flagstrip">${toggles}</div>`;
}

function jurisdictionFor(meta: ProceedingTypeMeta): string {
  if (meta.jurisdiction) return meta.jurisdiction;
  if (meta.group) return meta.group;
  const dot = meta.code.indexOf(".");
  if (dot > 0) return meta.code.slice(0, dot).toUpperCase();
  return meta.code.toUpperCase();
}

function activeFlagsBadge(flags: Record<string, unknown>): string {
  const active = Object.entries(flags).filter(([, v]) => v === true).map(([k]) => k);
  if (active.length === 0) return "";
  const label = t("builder.triplet.flags.label");
  const chips = active.map((f) =>
    `<span class="builder-triplet-flag-chip">${escHtml(f)}</span>`,
  ).join("");
  return `<span class="builder-triplet-flags">${escHtml(label)} ${chips}</span>`;
}

// overlayEventStates walks the rendered .fr-col-item nodes and:
//   - sets data-builder-state from eventsByRule lookup;
//   - appends a per-card action row (file / skip / reset);
//   - shows a +N Optionen chip when the rule has optional children
//     (the chip placeholder; B2 ships the per-card horizon control —
//     the actual horizon-count→render expansion lands when the calc
//     engine surfaces "available optionals" for a parent rule, which
//     pasteur's Options.IncludeOptional flag already exposes server-
//     side; full wiring is a follow-up). Cards without optional
//     children get no chip.
export function overlayEventStates(
  root: HTMLElement,
  eventsByRule: Map<string, BuilderEvent>,
  on: {
    onAction: (ruleId: string, action: "file" | "skip" | "reset", payload?: { date?: string; reason?: string }) => void;
    onHorizon: (ruleId: string, delta: 1 | -1) => void;
  },
): void {
  const items = root.querySelectorAll<HTMLElement>(".fr-col-item[data-rule-id]");
  items.forEach((item) => {
    const ruleId = item.getAttribute("data-rule-id");
    if (!ruleId) return;
    const ev = eventsByRule.get(ruleId.toLowerCase());
    const state = ev?.state || "planned";
    item.setAttribute("data-builder-state", state);

    // Append actions (idempotent: clear any prior overlay first).
    item.querySelectorAll(".builder-event-actions, .builder-event-horizon-chip").forEach((n) => n.remove());

    const actions = document.createElement("div");
    actions.className = "builder-event-actions";
    actions.innerHTML = actionButtonsHtml(state);
    item.appendChild(actions);

    actions.addEventListener("click", (ev) => {
      const btn = (ev.target as HTMLElement).closest<HTMLElement>(".builder-event-action");
      if (!btn) return;
      const action = btn.getAttribute("data-action") as "file" | "skip" | "reset" | null;
      if (!action) return;
      ev.stopPropagation();
      if (action === "file") {
        const today = new Date().toISOString().slice(0, 10);
        const v = window.prompt(t("builder.event.actual_date.prompt"), today);
        if (v === null) return;
        on.onAction(ruleId, "file", { date: v.trim() || today });
      } else if (action === "skip") {
        const reason = window.prompt(t("builder.event.skip_reason.prompt"), "");
        if (reason === null) return;
        on.onAction(ruleId, "skip", { reason: reason.trim() });
      } else {
        on.onAction(ruleId, "reset");
      }
    });

    // Per-card optional horizon chip. The PRD §3.4 places the chip on
    // every card with optional children; until the calc surface exposes
    // an "optionals available count" on each parent rule, the chip is
    // shown only when the card has a stored non-zero horizon (so the
    // user can see and reduce a previously-set horizon). This is the
    // graceful B2 baseline; the full surface lands once the engine
    // emits an optionalsAvailable counter (PRD §3.4 follow-up).
    const horizonCount = ev?.horizon_optional ?? 0;
    if (horizonCount > 0) {
      const chip = document.createElement("button");
      chip.type = "button";
      chip.className = "builder-event-horizon-chip";
      chip.setAttribute("data-action", "horizon-toggle");
      chip.textContent = t("builder.event.horizon.label").replace("{n}", String(horizonCount));
      chip.addEventListener("click", (e) => {
        e.stopPropagation();
        on.onHorizon(ruleId, -1);
      });
      item.appendChild(chip);
    } else {
      // Inline "+ Optionen" affordance — adds a horizon entry when
      // first clicked. Tagged as data-builder-feature so the cleanup
      // sweep can rip it out if the calc surface lands a counter.
      const chip = document.createElement("button");
      chip.type = "button";
      chip.className = "builder-event-horizon-chip";
      chip.setAttribute("data-action", "horizon-add");
      chip.setAttribute("data-builder-feature", "horizon-add");
      chip.textContent = "+ " + t("builder.event.horizon.label").replace("+{n} ", "");
      chip.addEventListener("click", (e) => {
        e.stopPropagation();
        on.onHorizon(ruleId, 1);
      });
      item.appendChild(chip);
    }
  });
}

function actionButtonsHtml(state: BuilderEvent["state"]): string {
  // Re-render the action row per state. Cards in the planned state
  // show "File / Skip"; filed/skipped cards show "Reset to planned".
  if (state === "planned") {
    return `
      <button type="button" class="builder-event-action" data-action="file">${escHtml(t("builder.event.action.file"))}</button>
      <button type="button" class="builder-event-action" data-action="skip">${escHtml(t("builder.event.action.skip"))}</button>
    `;
  }
  return `<button type="button" class="builder-event-action" data-action="reset">${escHtml(t("builder.event.action.reset"))}</button>`;
}

function escHtml(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

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