// Per-event-card choice popover + chip indicator (t-paliad-265 /
// m/paliad#96).
//
// The shared rendering core (verfahrensablauf-core.ts) emits a caret
// button on cards that carry a non-empty `choices_offered` declaration
// and an inert chip span next to the title. This module:
//
//   1. Wires a delegated click handler on the result container so the
//      caret opens a popover with the offered choice-kinds.
//   2. Commits the user's pick — either by POSTing to the project-
//      bound endpoint or by mutating the in-memory state for the
//      unbound (no-project) case.
//   3. Rehydrates the chip on every render + after every commit so the
//      glanceable indicator matches the active state.
//
// Two consumer pages — /tools/verfahrensablauf (unbound) and
// /tools/fristenrechner (project-bound) — both wire this module
// once at boot via attachEventCardChoices().

import { escAttr, escHtml } from "./verfahrensablauf-core";
import { t } from "../i18n";

export type ChoiceKind = "appellant" | "include_ccr" | "skip";

export interface EventChoice {
  submission_code: string;
  choice_kind: ChoiceKind;
  choice_value: string;
}

// State surface — the page passes in callbacks that own persistence.
// commit / remove must trigger a recalc on the page side (the popover
// only owns its own visual state).
export interface EventCardChoicesOpts {
  container: HTMLElement;
  // Initial state: a list of choices. The page seeds this from the
  // server response (project-bound) or from URL params (unbound).
  initial: EventChoice[];
  // commit gets called for an UPSERT. The page POSTs to the API (or
  // mutates URL state) AND triggers a recalc.
  commit: (choice: EventChoice) => Promise<void> | void;
  // remove gets called when the user resets a choice.
  remove: (submissionCode: string, kind: ChoiceKind) => Promise<void> | void;
}

// One mutable bag per attach() call. The current implementation is a
// single-page singleton — paginated views (admin tables) are not in
// scope. Last-write-wins on the in-memory state.
interface AttachedState {
  opts: EventCardChoicesOpts;
  // active: submission_code → kind → value. Rebuilt from `initial`
  // on every reseed() call.
  active: Map<string, Map<ChoiceKind, string>>;
  popover: HTMLDivElement | null;
}

const states = new WeakMap<HTMLElement, AttachedState>();

// attachEventCardChoices wires the delegated click + popover lifecycle
// to the given container. Call once per page after mount; safe to call
// again with a fresh container.
export function attachEventCardChoices(opts: EventCardChoicesOpts): void {
  const state: AttachedState = {
    opts,
    active: new Map(),
    popover: null,
  };
  for (const c of opts.initial) {
    if (!state.active.has(c.submission_code)) {
      state.active.set(c.submission_code, new Map());
    }
    state.active.get(c.submission_code)!.set(c.choice_kind, c.choice_value);
  }
  states.set(opts.container, state);

  opts.container.addEventListener("click", (e) => {
    const targetEl = e.target as HTMLElement | null;
    const caret = targetEl?.closest<HTMLElement>(".event-card-choices-caret");
    if (caret) {
      e.stopPropagation();
      openPopover(state, caret);
      return;
    }
    // Outside-click closes the popover.
    if (state.popover && !state.popover.contains(e.target as Node)) {
      closePopover(state);
    }
  });

  // ESC also closes.
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && state.popover) {
      closePopover(state);
    }
  });

  // Repaint chips on every renderResults() call. The page is
  // responsible for calling reseedChips() after re-render so the chip
  // dom node (re-created by the renderer) picks the active state up.
  reseedChips(opts.container);
}

// reseedChips walks every chip span in the container and re-renders
// its content from the active state map. Idempotent.
export function reseedChips(container: HTMLElement): void {
  const state = states.get(container);
  if (!state) return;
  container.querySelectorAll<HTMLElement>(".event-card-choices-chip").forEach((chip) => {
    const code = chip.dataset.submissionCode || "";
    const kinds = state.active.get(code);
    if (!kinds || kinds.size === 0) {
      chip.innerHTML = "";
      chip.dataset.empty = "true";
      return;
    }
    chip.dataset.empty = "false";
    chip.innerHTML = renderChip(kinds);
  });
  // Skipped rows fade out via a class on the card-item ancestor.
  container.querySelectorAll<HTMLElement>(".event-card-choices-chip").forEach((chip) => {
    const code = chip.dataset.submissionCode || "";
    const skipped = state.active.get(code)?.get("skip") === "true";
    const itemEl = chip.closest<HTMLElement>(".timeline-item, .fr-col-item");
    if (itemEl) itemEl.classList.toggle("timeline-item--skipped", skipped);
  });
}

function renderChip(kinds: Map<ChoiceKind, string>): string {
  const parts: string[] = [];
  if (kinds.get("skip") === "true") {
    parts.push(`<span class="event-card-choices-chip-part event-card-choices-chip-part--skipped">${escHtml(t("choices.skipped.chip"))}</span>`);
  }
  const ap = kinds.get("appellant");
  if (ap && ap !== "" ) {
    let label = "";
    switch (ap) {
      case "claimant":  label = t("choices.appellant.claimant"); break;
      case "defendant": label = t("choices.appellant.defendant"); break;
      case "both":      label = t("choices.appellant.both"); break;
      case "none":      label = t("choices.appellant.none"); break;
    }
    if (label) {
      parts.push(`<span class="event-card-choices-chip-part">${escHtml(t("choices.appellant.chip"))} ${escHtml(label)}</span>`);
    }
  }
  if (kinds.get("include_ccr") === "true") {
    parts.push(`<span class="event-card-choices-chip-part">${escHtml(t("choices.include_ccr.chip"))}</span>`);
  }
  return parts.join(" ");
}

function openPopover(state: AttachedState, caret: HTMLElement): void {
  closePopover(state);
  const code = caret.dataset.submissionCode || "";
  if (!code) return;
  let offered: Record<string, unknown> = {};
  try {
    offered = JSON.parse(caret.dataset.choicesOffered || "{}");
  } catch {
    return;
  }
  const isHidden = caret.dataset.isHidden === "1";

  const pop = document.createElement("div");
  pop.className = "event-card-choices-popover";
  pop.setAttribute("role", "dialog");
  pop.setAttribute("aria-label", t("choices.caret.title"));

  const blocks: string[] = [];
  // t-paliad-293: hidden-card prominence. When the user opens the
  // popover on a re-surfaced hidden card, "Wieder einblenden" is the
  // most likely intent — surface it as a single high-contrast action
  // at the top of the popover (rather than burying it under the skip
  // toggle's reset link). Clicking it clears the `skip` choice, which
  // is the same wire effect as the legacy inline chip from t-paliad-290.
  if (isHidden) {
    blocks.push(renderUnhideBlock());
  }
  if (Array.isArray(offered.appellant)) {
    blocks.push(renderAppellantBlock(state, code, offered.appellant as unknown[]));
  }
  if (Array.isArray(offered.include_ccr)) {
    blocks.push(renderToggleBlock(state, code, "include_ccr"));
  }
  if (Array.isArray(offered.skip)) {
    blocks.push(renderToggleBlock(state, code, "skip"));
  }
  pop.innerHTML = blocks.join("");

  document.body.appendChild(pop);
  state.popover = pop;
  positionPopover(pop, caret);

  pop.addEventListener("click", async (e) => {
    const btn = (e.target as HTMLElement | null)?.closest<HTMLButtonElement>("button[data-choice-action]");
    if (!btn) return;
    e.stopPropagation();
    const kind = btn.dataset.choiceKind as ChoiceKind | undefined;
    const value = btn.dataset.choiceValue || "";
    const action = btn.dataset.choiceAction;
    if (!kind) return;
    try {
      if (action === "set") {
        await state.opts.commit({ submission_code: code, choice_kind: kind, choice_value: value });
        if (!state.active.has(code)) state.active.set(code, new Map());
        state.active.get(code)!.set(kind, value);
      } else if (action === "clear") {
        await state.opts.remove(code, kind);
        state.active.get(code)?.delete(kind);
      }
      reseedChips(state.opts.container);
      closePopover(state);
    } catch (err) {
      console.error("event card choice commit failed", err);
      // Surface a soft inline error inside the popover; do NOT close.
      const errEl = document.createElement("div");
      errEl.className = "event-card-choices-error";
      errEl.textContent = t("choices.commit.error");
      pop.appendChild(errEl);
    }
  });
}

function renderAppellantBlock(state: AttachedState, code: string, values: unknown[]): string {
  const current = state.active.get(code)?.get("appellant") || "";
  const buttons = values
    .filter((v): v is string => typeof v === "string")
    .map((v) => {
      const labelKey = `choices.appellant.${v}` as const;
      const isActive = v === current;
      return `<button type="button"
        data-choice-action="set"
        data-choice-kind="appellant"
        data-choice-value="${escAttr(v)}"
        class="event-card-choices-option${isActive ? " event-card-choices-option--active" : ""}">${escHtml(t(labelKey as any))}</button>`;
    })
    .join("");
  const reset = current
    ? `<button type="button" data-choice-action="clear" data-choice-kind="appellant"
        class="event-card-choices-reset">${escHtml(t("choices.reset"))}</button>`
    : "";
  return `<div class="event-card-choices-block">
    <div class="event-card-choices-title">${escHtml(t("choices.appellant.title"))}</div>
    <div class="event-card-choices-options">${buttons}</div>
    ${reset}
  </div>`;
}

function renderToggleBlock(state: AttachedState, code: string, kind: "include_ccr" | "skip"): string {
  const current = state.active.get(code)?.get(kind) || "false";
  const titleKey = kind === "include_ccr" ? "choices.include_ccr.title" : "choices.skip.title";
  const trueKey = kind === "include_ccr" ? "choices.include_ccr.true" : "choices.skip.true";
  const falseKey = kind === "include_ccr" ? "choices.include_ccr.false" : "choices.skip.false";
  const opt = (v: "true" | "false", labelKey: string) => `<button type="button"
    data-choice-action="set"
    data-choice-kind="${kind}"
    data-choice-value="${v}"
    class="event-card-choices-option${v === current ? " event-card-choices-option--active" : ""}">${escHtml(t(labelKey as any))}</button>`;
  const reset = state.active.get(code)?.has(kind)
    ? `<button type="button" data-choice-action="clear" data-choice-kind="${kind}"
        class="event-card-choices-reset">${escHtml(t("choices.reset"))}</button>`
    : "";
  return `<div class="event-card-choices-block">
    <div class="event-card-choices-title">${escHtml(t(titleKey as any))}</div>
    <div class="event-card-choices-options">
      ${opt("true", trueKey)}
      ${opt("false", falseKey)}
    </div>
    ${reset}
  </div>`;
}

// renderUnhideBlock is the popover's prominent "Wieder einblenden"
// action — surfaced only when the caret is opened on a re-surfaced
// hidden card (data-is-hidden="1" on the caret). Clicking it dispatches
// the same `clear` action as the skip-block reset link below, but
// labelled in the user's terms ("restore this card" rather than
// "reset skip choice"). Drops out of the popover automatically on
// non-hidden cards so the popover stays minimal. (t-paliad-293)
function renderUnhideBlock(): string {
  const label = t("choices.unhide.chip");
  return `<div class="event-card-choices-block event-card-choices-block--unhide">
    <button type="button"
      data-choice-action="clear"
      data-choice-kind="skip"
      class="event-card-choices-unhide-btn">${escHtml(label)}</button>
  </div>`;
}

function closePopover(state: AttachedState): void {
  if (state.popover) {
    state.popover.remove();
    state.popover = null;
  }
}

function positionPopover(pop: HTMLDivElement, caret: HTMLElement): void {
  const rect = caret.getBoundingClientRect();
  const scrollY = window.scrollY || document.documentElement.scrollTop;
  const scrollX = window.scrollX || document.documentElement.scrollLeft;
  pop.style.position = "absolute";
  pop.style.top = `${rect.bottom + scrollY + 4}px`;
  pop.style.left = `${Math.max(8, rect.right + scrollX - 240)}px`;
  pop.style.zIndex = "1000";
}

// Returns the current in-memory choice list for the given container —
// used by the unbound /tools/verfahrensablauf page to keep the URL
// param in sync.
export function currentChoices(container: HTMLElement): EventChoice[] {
  const state = states.get(container);
  if (!state) return [];
  const out: EventChoice[] = [];
  state.active.forEach((kinds, code) => {
    kinds.forEach((value, kind) => {
      out.push({ submission_code: code, choice_kind: kind, choice_value: value });
    });
  });
  return out;
}
