// Universal search dropdown for the Litigation Builder (m/paliad#153 B3).
//
// PRD §2.2 + §3.1 + §6.3: the page-header search box ("Suche") drives
// a typed dropdown returning grouped event / scenario / project hits.
// Picking an event lands the user on a scratch scenario with one
// triplet anchored on that event's proceeding type. Picking a scenario
// loads it; picking a project (Akte) is deferred to B4 (the dropdown
// row renders but pick falls through to a console hint until B4 wires
// project-backed scenarios).
//
// The controller is owned by builder.ts; this module exports
// `mountBuilderSearch` which wires the input + dropdown lifecycle and
// invokes the supplied callbacks. No module-level state — re-mounting
// is safe.

import { t } from "./i18n";

export interface EventSearchHit {
  id: string;
  code: string;
  name_de: string;
  name_en: string;
  event_kind?: string | null;
  primary_party?: string | null;
  anchor_rule_id: string;
  follow_up_count: number;
  proceeding_type: {
    id: number;
    code: string;
    name_de: string;
    name_en: string;
    jurisdiction?: string | null;
  };
}

export interface ScenarioSearchHit {
  id: string;
  name: string;
  status: string;
  updated_at: string;
}

export interface ProjectSearchHit {
  id: string;
  type: string;
  title: string;
  reference?: string | null;
  case_number?: string | null;
  matter_number?: string | null;
  client_number?: string | null;
}

export interface UniversalSearchResponse {
  query: string;
  events: EventSearchHit[];
  scenarios: ScenarioSearchHit[];
  projects: ProjectSearchHit[];
  counts: { events: number; scenarios: number; projects: number };
}

export interface BuilderSearchCallbacks {
  onPickEvent: (hit: EventSearchHit) => void | Promise<void>;
  onPickScenario: (hit: ScenarioSearchHit) => void | Promise<void>;
  onPickProject?: (hit: ProjectSearchHit) => void | Promise<void>;
}

interface Controller {
  input: HTMLInputElement;
  dropdown: HTMLElement;
  open: boolean;
  abort: AbortController | null;
  debounceTimer: number | null;
  lang: "de" | "en";
}

let active: Controller | null = null;

// mountBuilderSearch wires the universal search behavior onto an
// existing <input>. Idempotent — re-calling tears down the previous
// dropdown and rebinds. Returns a controller exposing focus() so the
// entry-mode toggle in builder.ts can land on the search input.
export function mountBuilderSearch(
  input: HTMLInputElement,
  cb: BuilderSearchCallbacks,
): { focus: () => void; close: () => void } {
  teardown();

  const lang: "de" | "en" = document.documentElement.lang === "en" ? "en" : "de";

  // Single dropdown container, anchored under the input. Positioned
  // absolutely so it floats above the canvas without reflowing layout.
  const dropdown = document.createElement("div");
  dropdown.className = "builder-search-dropdown";
  dropdown.setAttribute("role", "listbox");
  dropdown.hidden = true;
  document.body.appendChild(dropdown);

  active = {
    input,
    dropdown,
    open: false,
    abort: null,
    debounceTimer: null,
    lang,
  };

  input.addEventListener("input", onInput);
  input.addEventListener("focus", onFocus);
  input.addEventListener("keydown", onKeydown);
  document.addEventListener("click", onOutsideClick, true);
  window.addEventListener("resize", reposition);
  window.addEventListener("scroll", reposition, true);

  // Click handler is wired once on the dropdown root via event
  // delegation; per-row data attributes identify the hit type.
  dropdown.addEventListener("click", (ev) => {
    const row = (ev.target as HTMLElement).closest<HTMLElement>(".builder-search-row");
    if (!row) return;
    const kind = row.getAttribute("data-hit-kind");
    const payload = row.getAttribute("data-hit-payload");
    if (!kind || !payload) return;
    try {
      const hit = JSON.parse(payload);
      ev.stopPropagation();
      closeDropdown();
      if (kind === "event") void cb.onPickEvent(hit);
      else if (kind === "scenario") void cb.onPickScenario(hit);
      else if (kind === "project" && cb.onPickProject) void cb.onPickProject(hit);
    } catch (err) {
      console.error("builder-search: bad payload", err);
    }
  });

  return {
    focus: () => {
      input.focus();
      // Open the dropdown on focus even when input is empty — show the
      // "start typing" hint per PRD §2.2 (search box auto-focuses).
      openDropdown();
      renderHint(t("builder.search.hint.start"));
    },
    close: closeDropdown,
  };
}

function teardown(): void {
  if (!active) return;
  if (active.abort) active.abort.abort();
  if (active.debounceTimer !== null) window.clearTimeout(active.debounceTimer);
  active.dropdown.remove();
  active.input.removeEventListener("input", onInput);
  active.input.removeEventListener("focus", onFocus);
  active.input.removeEventListener("keydown", onKeydown);
  document.removeEventListener("click", onOutsideClick, true);
  window.removeEventListener("resize", reposition);
  window.removeEventListener("scroll", reposition, true);
  active = null;
}

function onInput(): void {
  if (!active) return;
  const q = active.input.value.trim();
  if (active.debounceTimer !== null) window.clearTimeout(active.debounceTimer);
  if (q.length === 0) {
    openDropdown();
    renderHint(t("builder.search.hint.start"));
    return;
  }
  if (q.length < 2) {
    openDropdown();
    renderHint(t("builder.search.hint.short"));
    return;
  }
  active.debounceTimer = window.setTimeout(() => {
    void runSearch(q);
  }, 180);
}

function onFocus(): void {
  if (!active) return;
  const q = active.input.value.trim();
  if (q.length === 0) {
    openDropdown();
    renderHint(t("builder.search.hint.start"));
  } else if (q.length >= 2) {
    void runSearch(q);
  }
}

function onKeydown(ev: KeyboardEvent): void {
  if (!active) return;
  if (ev.key === "Escape") {
    closeDropdown();
    return;
  }
  if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
    const rows = Array.from(active.dropdown.querySelectorAll<HTMLElement>(".builder-search-row"));
    if (rows.length === 0) return;
    ev.preventDefault();
    const current = active.dropdown.querySelector<HTMLElement>(".builder-search-row.is-focus");
    let idx = current ? rows.indexOf(current) : -1;
    idx = ev.key === "ArrowDown"
      ? Math.min(rows.length - 1, idx + 1)
      : Math.max(0, idx - 1);
    rows.forEach((r) => r.classList.remove("is-focus"));
    rows[idx].classList.add("is-focus");
    rows[idx].scrollIntoView({ block: "nearest" });
    return;
  }
  if (ev.key === "Enter") {
    const focused = active.dropdown.querySelector<HTMLElement>(".builder-search-row.is-focus");
    if (focused) {
      ev.preventDefault();
      focused.click();
    }
  }
}

function onOutsideClick(ev: Event): void {
  if (!active) return;
  const target = ev.target as Node;
  if (active.input.contains(target)) return;
  if (active.dropdown.contains(target)) return;
  closeDropdown();
}

async function runSearch(q: string): Promise<void> {
  if (!active) return;
  // Cancel any in-flight request so a slow earlier query can't clobber
  // a faster newer one.
  if (active.abort) active.abort.abort();
  const ctl = new AbortController();
  active.abort = ctl;

  openDropdown();
  renderHint(t("builder.search.hint.loading"));

  try {
    const url = "/api/builder/search?q=" + encodeURIComponent(q);
    const resp = await fetch(url, { signal: ctl.signal });
    if (!resp.ok) {
      renderHint(t("builder.search.hint.error"));
      return;
    }
    const data = (await resp.json()) as UniversalSearchResponse;
    if (active.abort !== ctl) return;
    renderResults(data);
  } catch (err) {
    if ((err as { name?: string })?.name === "AbortError") return;
    console.error("builder-search error:", err);
    renderHint(t("builder.search.hint.error"));
  }
}

function renderHint(message: string): void {
  if (!active) return;
  active.dropdown.innerHTML = `<div class="builder-search-hint">${escHtml(message)}</div>`;
  reposition();
}

function renderResults(data: UniversalSearchResponse): void {
  if (!active) return;
  const lang = active.lang;

  const total = data.events.length + data.scenarios.length + data.projects.length;
  if (total === 0) {
    renderHint(t("builder.search.hint.empty"));
    return;
  }

  // Result-count summary per PRD §2.2: "N Ereignisse · M Szenarios · K Akten"
  const counts = `<div class="builder-search-summary">` +
    escHtml(tCount("builder.search.summary.events", data.events.length)) +
    ` · ` +
    escHtml(tCount("builder.search.summary.scenarios", data.scenarios.length)) +
    ` · ` +
    escHtml(tCount("builder.search.summary.projects", data.projects.length)) +
    `</div>`;

  const sections: string[] = [counts];

  if (data.events.length > 0) {
    sections.push(renderGroup(
      t("builder.search.group.events"),
      data.events.map((e) => renderEventRow(e, lang)).join(""),
    ));
  }
  if (data.scenarios.length > 0) {
    sections.push(renderGroup(
      t("builder.search.group.scenarios"),
      data.scenarios.map((s) => renderScenarioRow(s)).join(""),
    ));
  }
  if (data.projects.length > 0) {
    sections.push(renderGroup(
      t("builder.search.group.projects"),
      data.projects.map((p) => renderProjectRow(p, lang)).join(""),
    ));
  }

  active.dropdown.innerHTML = sections.join("");
  reposition();
}

function renderGroup(label: string, rowsHtml: string): string {
  return `<section class="builder-search-group">` +
    `<header class="builder-search-group-label">${escHtml(label)}</header>` +
    rowsHtml +
    `</section>`;
}

function renderEventRow(hit: EventSearchHit, lang: "de" | "en"): string {
  const name = lang === "en" ? (hit.name_en || hit.name_de) : (hit.name_de || hit.name_en);
  const ptName = lang === "en"
    ? (hit.proceeding_type.name_en || hit.proceeding_type.name_de)
    : (hit.proceeding_type.name_de || hit.proceeding_type.name_en);
  const party = hit.primary_party ? `<span class="builder-search-party">${escHtml(hit.primary_party)}</span>` : "";
  const kind = hit.event_kind ? `<span class="builder-search-kind">${escHtml(hit.event_kind)}</span>` : "";
  // Payload for the click handler — we embed the full hit so builder.ts
  // doesn't need a second lookup. JSON-encoded into a data attribute,
  // attr-escaped on the way in.
  const payload = escAttr(JSON.stringify(hit));
  return `<div class="builder-search-row" data-hit-kind="event" data-hit-payload="${payload}" tabindex="-1" role="option">` +
    `<div class="builder-search-row-main">` +
      `<span class="builder-search-pt-code">${escHtml(hit.proceeding_type.code)}</span>` +
      `<span class="builder-search-event-name">${escHtml(name)}</span>` +
    `</div>` +
    `<div class="builder-search-row-meta">` +
      `<span class="builder-search-pt-name">${escHtml(ptName)}</span>` +
      kind + party +
    `</div>` +
  `</div>`;
}

function renderScenarioRow(hit: ScenarioSearchHit): string {
  const payload = escAttr(JSON.stringify(hit));
  return `<div class="builder-search-row" data-hit-kind="scenario" data-hit-payload="${payload}" tabindex="-1" role="option">` +
    `<div class="builder-search-row-main">` +
      `<span class="builder-search-scenario-name">${escHtml(hit.name)}</span>` +
    `</div>` +
    `<div class="builder-search-row-meta">` +
      `<span class="builder-search-status">${escHtml(hit.status)}</span>` +
    `</div>` +
  `</div>`;
}

function renderProjectRow(hit: ProjectSearchHit, _lang: "de" | "en"): string {
  const meta: string[] = [];
  if (hit.case_number) meta.push(hit.case_number);
  if (hit.matter_number) meta.push(hit.matter_number);
  if (hit.client_number) meta.push(hit.client_number);
  if (hit.reference) meta.push(hit.reference);
  const metaText = meta.length > 0 ? meta.join(" · ") : "";
  const payload = escAttr(JSON.stringify(hit));
  return `<div class="builder-search-row" data-hit-kind="project" data-hit-payload="${payload}" tabindex="-1" role="option">` +
    `<div class="builder-search-row-main">` +
      `<span class="builder-search-project-type">${escHtml(hit.type)}</span>` +
      `<span class="builder-search-project-title">${escHtml(hit.title)}</span>` +
    `</div>` +
    `<div class="builder-search-row-meta">${escHtml(metaText)}</div>` +
  `</div>`;
}

function openDropdown(): void {
  if (!active) return;
  active.dropdown.hidden = false;
  active.open = true;
  reposition();
}

function closeDropdown(): void {
  if (!active) return;
  active.dropdown.hidden = true;
  active.open = false;
  if (active.abort) {
    active.abort.abort();
    active.abort = null;
  }
}

function reposition(): void {
  if (!active || !active.open) return;
  const rect = active.input.getBoundingClientRect();
  const top = rect.bottom + window.scrollY + 4;
  const left = rect.left + window.scrollX;
  const width = Math.max(rect.width, 380);
  active.dropdown.style.position = "absolute";
  active.dropdown.style.top = `${top}px`;
  active.dropdown.style.left = `${left}px`;
  active.dropdown.style.width = `${width}px`;
  active.dropdown.style.zIndex = "60";
}

// tCount applies a simple plural pick: keys ".one" / ".other" carry
// the singular/plural variants; the caller's key is the bare stem.
function tCount(key: string, n: number): string {
  const variant = n === 1 ? `${key}.one` : `${key}.other`;
  return t(variant).replace("{n}", String(n));
}

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;");
}
