// Global search palette — client-side controller for the sidebar search input.
// Shared by every page that renders <Sidebar />. Owns debounce, keyboard
// shortcuts, overlay rendering, and navigation.
//
// Cmd+K / Ctrl+K (t-paliad-044) opens the palette in "discoverability mode":
// the action catalog is rendered with no entity fetch. Once the user types,
// the entity search fires alongside a substring filter on the actions.
// "/" is preserved as a second trigger (existing muscle memory; t-paliad-026).

import { t, tDyn, onLangChange } from "./i18n";
import {
  PALETTE_ACTIONS,
  type PaletteAction,
  filterActions,
  iconForAction,
  labelFor,
  runAction,
} from "./palette-actions";

type ResultType =
  | "project"
  | "deadline"
  | "appointment"
  | "glossary"
  | "court"
  | "checklist_template"
  | "checklist_instance"
  | "link"
  | "user";

interface SearchResult {
  id: string;
  type: ResultType;
  title: string;
  subtitle?: string;
  url: string;
}

interface SearchResponse {
  query: string;
  projects: SearchResult[];
  deadlines: SearchResult[];
  appointments: SearchResult[];
  glossary: SearchResult[];
  courts: SearchResult[];
  checklists: SearchResult[];
  links: SearchResult[];
  users: SearchResult[];
}

// Active item is either a palette action or an entity result. ↵ dispatches
// based on `kind`.
type ActiveItem =
  | { kind: "action"; action: PaletteAction }
  | { kind: "entity"; result: SearchResult };

// Debounce is tight enough to feel responsive but coarse enough to avoid
// hammering the backend on every keystroke. 200ms matches the task brief.
const DEBOUNCE_MS = 200;
const MIN_QUERY = 1;

const GROUP_ORDER: Array<{ key: keyof SearchResponse; i18n: string; fallback: string }> = [
  { key: "projects", i18n: "search.group.projects", fallback: "Projekte" },
  { key: "deadlines", i18n: "search.group.deadlines", fallback: "Fristen" },
  { key: "appointments", i18n: "search.group.appointments", fallback: "Termine" },
  { key: "glossary", i18n: "search.group.glossary", fallback: "Glossar" },
  { key: "courts", i18n: "search.group.courts", fallback: "Gerichte" },
  { key: "checklists", i18n: "search.group.checklists", fallback: "Checklisten" },
  { key: "links", i18n: "search.group.links", fallback: "Links" },
  { key: "users", i18n: "search.group.users", fallback: "Kolleg:innen" },
];

let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let inFlight: AbortController | null = null;
let flatResults: ActiveItem[] = [];
let activeIndex = -1;
let currentResponse: SearchResponse | null = null;
let currentQuery = "";

export function initGlobalSearch(): void {
  const input = document.getElementById("global-search-input") as HTMLInputElement | null;
  const overlay = document.getElementById("global-search-overlay") as HTMLElement | null;
  if (!input || !overlay) return;

  input.addEventListener("input", () => {
    const q = input.value.trim();
    currentQuery = q;
    if (debounceTimer) clearTimeout(debounceTimer);
    if (q.length < MIN_QUERY) {
      // Empty input but palette still open — show all actions, no entity fetch.
      if (overlay.style.display === "block") {
        currentResponse = null;
        renderPalette(q, null, overlay);
      } else {
        closeOverlay(overlay);
      }
      return;
    }
    debounceTimer = setTimeout(() => runSearch(q, overlay), DEBOUNCE_MS);
  });

  input.addEventListener("focus", () => {
    // Render whatever state we currently have. If the user just clicked into
    // the input with nothing typed, this opens the palette in empty-action
    // mode — same UX as Cmd+K.
    const q = input.value.trim();
    currentQuery = q;
    if (q.length < MIN_QUERY) {
      currentResponse = null;
      renderPalette(q, null, overlay);
    }
  });

  input.addEventListener("keydown", (e) => {
    switch (e.key) {
      case "Escape":
        e.preventDefault();
        input.value = "";
        closeOverlay(overlay);
        input.blur();
        break;
      case "ArrowDown":
        e.preventDefault();
        moveActive(1, overlay);
        break;
      case "ArrowUp":
        e.preventDefault();
        moveActive(-1, overlay);
        break;
      case "Enter":
        e.preventDefault();
        openActive();
        break;
    }
  });

  // Global "/" shortcut — skip when the user is already typing in a form
  // field so we don't hijack their input. Standard pattern across apps that
  // offer slash-to-search (GitHub, Linear, etc.).
  document.addEventListener("keydown", (e) => {
    if (e.key !== "/") return;
    const target = e.target as HTMLElement | null;
    if (!target) return;
    if (isTextInput(target)) return;
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    e.preventDefault();
    openPalette(input, overlay);
  });

  // Cmd+K / Ctrl+K — power-user trigger. Unlike "/", we DON'T skip when the
  // user is in a text input: the explicit Cmd+K combo signals intent to open
  // the palette regardless of focus context. preventDefault + stopPropagation
  // suppress browser-native Ctrl+K behavior (Firefox: focus URL-bar search
  // submenu).
  document.addEventListener("keydown", (e) => {
    const isCmdK = (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "k";
    if (!isCmdK) return;
    e.preventDefault();
    e.stopPropagation();
    openPalette(input, overlay);
  });

  // Clicking outside closes; ignore clicks inside the input or overlay
  // themselves (those either retype or navigate).
  document.addEventListener("click", (e) => {
    const t2 = e.target as Node;
    if (overlay.contains(t2) || input.contains(t2)) return;
    closeOverlay(overlay);
  });

  // Language switch — re-render group headers and action labels without
  // re-fetching.
  onLangChange(() => {
    if (overlay.style.display === "block") {
      renderPalette(currentQuery, currentResponse, overlay);
    }
  });
}

function openPalette(input: HTMLInputElement, overlay: HTMLElement): void {
  input.focus();
  input.select();
  const q = input.value.trim();
  currentQuery = q;
  if (q.length < MIN_QUERY) {
    currentResponse = null;
    renderPalette(q, null, overlay);
  } else {
    runSearch(q, overlay);
  }
}

function isTextInput(el: HTMLElement): boolean {
  const tag = el.tagName;
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
  if (el.isContentEditable) return true;
  return false;
}

async function runSearch(query: string, overlay: HTMLElement): Promise<void> {
  if (inFlight) inFlight.abort();
  inFlight = new AbortController();
  try {
    const res = await fetch(
      `/api/search?q=${encodeURIComponent(query)}`,
      { signal: inFlight.signal, credentials: "same-origin" },
    );
    if (!res.ok) {
      // Even on a backend hiccup we still render the action catalog filtered
      // by the query — the palette stays useful.
      currentResponse = null;
      renderPalette(query, null, overlay);
      return;
    }
    const data: SearchResponse = await res.json();
    currentResponse = data;
    renderPalette(query, data, overlay);
  } catch (err) {
    if ((err as Error).name === "AbortError") return;
    currentResponse = null;
    renderPalette(query, null, overlay);
  }
}

function renderPalette(query: string, data: SearchResponse | null, overlay: HTMLElement): void {
  flatResults = [];
  activeIndex = -1;

  const sections: string[] = [];

  // Actions section — always first, always rendered (even on empty query).
  const actions = filterActions(query, tDyn);
  if (actions.length > 0) {
    sections.push(renderActionsSection(actions, query));
  }

  // Entity section — only when we have data (i.e., the user typed something
  // and the fetch succeeded).
  if (data) {
    for (const group of GROUP_ORDER) {
      const items = data[group.key] as SearchResult[];
      if (!items || items.length === 0) continue;
      sections.push(renderEntityGroup(group, items, query));
    }
  }

  if (sections.length === 0) {
    overlay.innerHTML = `<div class="search-empty">${esc(t("search.no_results") || "Keine Ergebnisse")}</div>`;
  } else {
    overlay.innerHTML = sections.join("") + renderFooter();
    bindResultClicks(overlay);
  }
  overlay.style.display = "block";

  // Auto-select the first item so ↵ works without an arrow press first.
  if (flatResults.length > 0) {
    setActive(0, overlay);
  }
}

function renderActionsSection(actions: PaletteAction[], query: string): string {
  const header = `<div class="search-group-header">${esc(t("palette.section.actions") || "Aktionen")}</div>`;
  const lis = actions.map((action) => {
    const idx = flatResults.length;
    flatResults.push({ kind: "action", action });
    const label = labelFor(action, tDyn);
    return (
      `<button type="button" class="search-result palette-action" role="option" data-index="${idx}" data-action-id="${esc(action.id)}">` +
        `<span class="search-result-icon">${iconForAction(action.iconKey)}</span>` +
        `<span class="search-result-body">` +
          `<span class="search-result-title">${highlight(label, query)}</span>` +
        `</span>` +
      `</button>`
    );
  }).join("");
  return `<div class="search-group search-group-actions">${header}${lis}</div>`;
}

function renderEntityGroup(
  group: { key: keyof SearchResponse; i18n: string; fallback: string },
  items: SearchResult[],
  query: string,
): string {
  const header = `<div class="search-group-header">${esc(tDyn(group.i18n) || group.fallback)}</div>`;
  const lis = items.map((r) => {
    const idx = flatResults.length;
    flatResults.push({ kind: "entity", result: r });
    const subtitle = r.subtitle
      ? `<span class="search-result-subtitle">${highlight(r.subtitle, query)}</span>`
      : "";
    return (
      `<a class="search-result" role="option" data-index="${idx}" href="${encodeURI(r.url)}" data-type="${r.type}">` +
        `<span class="search-result-icon">${iconFor(r.type)}</span>` +
        `<span class="search-result-body">` +
          `<span class="search-result-title">${highlight(r.title, query)}</span>` +
          subtitle +
        `</span>` +
      `</a>`
    );
  }).join("");
  return `<div class="search-group">${header}${lis}</div>`;
}

function renderFooter(): string {
  return (
    `<div class="palette-footer">` +
      `<span><kbd>&uarr;&darr;</kbd> <span>${esc(t("palette.footer.navigate") || "Navigieren")}</span></span>` +
      `<span><kbd>&crarr;</kbd> <span>${esc(t("palette.footer.open") || "Öffnen")}</span></span>` +
      `<span><kbd>Esc</kbd> <span>${esc(t("palette.footer.close") || "Schließen")}</span></span>` +
    `</div>`
  );
}

function bindResultClicks(overlay: HTMLElement): void {
  overlay.querySelectorAll<HTMLElement>(".search-result").forEach((el) => {
    el.addEventListener("mouseenter", () => {
      const idx = parseInt(el.dataset.index ?? "-1", 10);
      if (!isNaN(idx)) setActive(idx, overlay);
    });
    el.addEventListener("click", (ev) => {
      // Action buttons have no href — dispatch via runAction. Anchor entity
      // results navigate natively, so we just close the overlay (after the
      // browser handles the click).
      if (el.classList.contains("palette-action")) {
        ev.preventDefault();
        const id = el.dataset.actionId;
        if (id) {
          closeOverlay(overlay);
          runAction(id);
        }
      } else {
        closeOverlay(overlay);
      }
    });
  });
}

function moveActive(delta: number, overlay: HTMLElement): void {
  if (flatResults.length === 0) return;
  if (overlay.style.display === "none") return;
  let next = activeIndex + delta;
  if (next < 0) next = flatResults.length - 1;
  if (next >= flatResults.length) next = 0;
  setActive(next, overlay);
}

function setActive(idx: number, overlay: HTMLElement): void {
  activeIndex = idx;
  overlay.querySelectorAll<HTMLElement>(".search-result").forEach((el, i) => {
    const match = i === idx;
    el.classList.toggle("active", match);
    if (match) {
      el.scrollIntoView({ block: "nearest" });
    }
  });
}

function openActive(): void {
  if (activeIndex < 0 || activeIndex >= flatResults.length) {
    if (flatResults.length === 1) dispatchItem(flatResults[0]);
    return;
  }
  dispatchItem(flatResults[activeIndex]);
}

function dispatchItem(item: ActiveItem): void {
  if (item.kind === "action") {
    runAction(item.action.id);
  } else {
    window.location.href = item.result.url;
  }
}

function closeOverlay(overlay: HTMLElement): void {
  overlay.style.display = "none";
  overlay.innerHTML = "";
  flatResults = [];
  activeIndex = -1;
  currentResponse = null;
  currentQuery = "";
  if (inFlight) {
    inFlight.abort();
    inFlight = null;
  }
}

// highlight wraps occurrences of the query (case-insensitive) in <mark>. All
// input is escaped first so user-entered queries can't inject HTML.
function highlight(text: string, query: string): string {
  const safeText = esc(text);
  const q = query.trim();
  if (!q) return safeText;
  const esQ = esc(q);
  const idx = safeText.toLowerCase().indexOf(esQ.toLowerCase());
  if (idx < 0) return safeText;
  return (
    safeText.slice(0, idx) +
    "<mark>" + safeText.slice(idx, idx + esQ.length) + "</mark>" +
    safeText.slice(idx + esQ.length)
  );
}

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

// iconFor returns a tiny inline SVG for each entity result type. Matches the
// sidebar icon language — stroke-based, 1.5 width, currentColor.
function iconFor(type: ResultType): string {
  const attrs = 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"';
  switch (type) {
    case "project":
      return `<svg ${attrs}><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
    case "deadline":
      return `<svg ${attrs}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`;
    case "appointment":
      return `<svg ${attrs}><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`;
    case "glossary":
      return `<svg ${attrs}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`;
    case "court":
      return `<svg ${attrs}><path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"/><path d="M16 9h3a2 2 0 0 1 2 2v10"/></svg>`;
    case "checklist_template":
    case "checklist_instance":
      return `<svg ${attrs}><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`;
    case "link":
      return `<svg ${attrs}><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
    case "user":
      return `<svg ${attrs}><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/></svg>`;
  }
  return "";
}
