import { initI18n, getLang, t } from "./i18n";
import { initSidebar } from "./sidebar";
import { renderResponseHTML } from "./paliadin-render";
import { pollForLateResponse, type LateTurn, type LatePollHandle } from "./paliadin-late-poll";

// Paliadin chat panel client (t-paliad-146 PoC).
//
// State machine: empty → typing → sending → streaming → done.
// History lives in localStorage under "paliadin:history:<sessionId>"
// — design §0.5.4 session-only persistence.
//
// SSE consumer subscribes to `event: meta`, `event: content`,
// `event: end`, `event: error`, `event: ping`. Backend currently
// emits one `content` blob per turn (real chunked streaming is
// production-v1; PoC simulates with a typewriter effect).

interface HistoryEntry {
  role: "user" | "assistant";
  text: string;
  meta?: {
    used_tools?: string[];
    rows_seen?: number[];
    classifier_tag?: string;
    duration_ms?: number;
    chip_count?: number;
  };
  ts: string; // ISO
}

const SESSION_KEY = "paliadin:session";
const HISTORY_PREFIX = "paliadin:history:";

let sessionId: string;
let history: HistoryEntry[] = [];
let currentEventSource: EventSource | null = null;
let currentTurnId: string | null = null;
// Late-response polls keyed by turn_id. Each entry runs until the
// response arrives or the 10-min cap expires. Stays alive across
// turns — m can keep chatting while we wait for the slow one.
const latePolls = new Map<string, LatePollHandle>();

document.addEventListener("DOMContentLoaded", () => {
  initI18n();
  initSidebar();
  bootSession();
  wireForm();
  wireStarters();
  wireReset();
  renderHistory();
  // Pull the canonical conversation from the DB so a turn typed in the
  // inline drawer (which shares this session id) shows up here on
  // mount. DB > localStorage when both have data.
  void hydrateFromServer();
});

function bootSession(): void {
  let s = localStorage.getItem(SESSION_KEY);
  if (!s) {
    s = crypto.randomUUID();
    localStorage.setItem(SESSION_KEY, s);
  }
  sessionId = s;
  const stored = localStorage.getItem(HISTORY_PREFIX + sessionId);
  if (stored) {
    try {
      history = JSON.parse(stored);
    } catch {
      history = [];
    }
  }
}

function wireForm(): void {
  const form = document.getElementById("paliadin-form") as HTMLFormElement | null;
  const input = document.getElementById("paliadin-input") as HTMLTextAreaElement | null;
  if (!form || !input) return;

  form.addEventListener("submit", (e) => {
    e.preventDefault();
    const text = input.value.trim();
    if (!text) return;
    input.value = "";
    sendTurn(text);
  });

  // Enter sends; Shift+Enter inserts newline.
  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      form.dispatchEvent(new Event("submit"));
    }
  });
}

function wireStarters(): void {
  const starters = document.querySelectorAll<HTMLButtonElement>(".paliadin-starter");
  starters.forEach((btn) => {
    btn.addEventListener("click", () => {
      const lang = getLang();
      const promptText = lang === "en"
        ? btn.dataset.promptEn || btn.textContent?.trim() || ""
        : btn.dataset.promptDe || btn.textContent?.trim() || "";
      if (promptText) sendTurn(promptText);
    });
  });
}

function wireReset(): void {
  const btn = document.getElementById("paliadin-reset");
  if (!btn) return;
  btn.addEventListener("click", async () => {
    history = [];
    saveHistory();
    renderHistory();
    try {
      await fetch("/api/paliadin/reset", { method: "POST", credentials: "same-origin" });
    } catch {
      // Reset failure is non-fatal — the next turn will spin up a fresh pane anyway.
    }
  });
}

async function sendTurn(text: string): Promise<void> {
  // Hide empty state on first send.
  const empty = document.getElementById("paliadin-empty");
  if (empty) empty.style.display = "none";

  // Append user bubble.
  history.push({ role: "user", text, ts: new Date().toISOString() });
  saveHistory();
  appendBubble("user", text);

  // Insert placeholder assistant bubble.
  const placeholder = appendBubble("assistant", "");
  placeholder.dataset.streaming = "true";
  placeholder.querySelector(".paliadin-bubble-text")!.textContent = "Paliadin denkt nach …";

  toggleStopButton(true);

  // Kick off the turn.
  let turnRes: { turn_id: string; sse_url: string };
  try {
    const r = await fetch("/api/paliadin/turn", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "same-origin",
      body: JSON.stringify({
        user_message: text,
        session_id: sessionId,
        page_origin: "/paliadin",
      }),
    });
    if (!r.ok) throw new Error("HTTP " + r.status);
    turnRes = await r.json();
  } catch (err) {
    placeholder.querySelector(".paliadin-bubble-text")!.textContent =
      t("paliadin.error.upstream");
    placeholder.dataset.streaming = "false";
    placeholder.classList.add("paliadin-bubble--error");
    toggleStopButton(false);
    return;
  }

  currentTurnId = turnRes.turn_id;

  // Open SSE.
  const es = new EventSource(turnRes.sse_url);
  currentEventSource = es;

  es.addEventListener("meta", () => {
    // Could surface a "thinking" indicator; placeholder text already does.
  });

  es.addEventListener("content", (ev) => {
    const data = JSON.parse((ev as MessageEvent).data);
    const text = String(data.text || "");
    // Cache the full text on the bubble so finishBubble can render the
    // complete response even when the typewriter is mid-flight when end
    // arrives. textContent reflects only what's been typed so far and
    // would otherwise truncate the rendered Markdown (m, 2026-05-08 —
    // saw "## Proje" instead of the full 1408-byte body).
    placeholder.dataset.fullText = text;
    typewriter(placeholder, text);
  });

  es.addEventListener("end", (ev) => {
    const data = JSON.parse((ev as MessageEvent).data);
    placeholder.dataset.streaming = "false";
    finishBubble(placeholder, data);
    history.push({
      role: "assistant",
      // Save the raw Markdown body (with [#deadline-OPEN:...] chip markers
      // intact), not the rendered textContent. Otherwise on reload the
      // chip-anchor text replaces the markers and renderResponseHTML can
      // no longer reconstruct the links (m, 2026-05-08 14:11 — links
      // disappeared on second load).
      text: placeholder.dataset.fullText ?? getBubbleText(placeholder),
      meta: {
        used_tools: data.used_tools,
        rows_seen: data.rows_seen,
        classifier_tag: data.classifier_tag,
        duration_ms: data.duration_ms,
        chip_count: data.chip_count,
      },
      ts: new Date().toISOString(),
    });
    saveHistory();
    cleanupTurn();
  });

  es.addEventListener("error", (ev) => {
    const errText = friendlyErrorMessage((ev as MessageEvent).data);
    // Annotate the error bubble with a "warten auf späte Antwort" hint
    // so m knows the turn isn't dead; if Claude finishes after the
    // 60 s window the Janitor (services.LocalPaliadinService.runJanitor)
    // patches the row and pollForLateResponse swaps in the real reply.
    placeholder.querySelector(".paliadin-bubble-text")!.textContent =
      errText + " " + t("paliadin.late.waiting");
    placeholder.classList.add("paliadin-bubble--error");
    placeholder.classList.add("paliadin-bubble--late-pending");
    placeholder.dataset.streaming = "false";
    placeholder.dataset.errorText = errText;
    if (currentTurnId) {
      placeholder.dataset.turnId = currentTurnId;
      startLatePoll(currentTurnId, placeholder);
    }
    cleanupTurn();
  });

  es.addEventListener("ping", () => {
    // heartbeat — no-op
  });
}

// Server emits SSE error events as JSON `{code, message}`. Map known
// codes to localised, user-friendly text; fall through to a generic
// "connection lost" for anything we don't recognise (including raw
// EventSource transport errors where data is absent).
function friendlyErrorMessage(data: unknown): string {
  if (typeof data !== "string" || data === "") {
    return t("paliadin.error.connection_lost");
  }
  try {
    const parsed = JSON.parse(data) as { code?: string };
    switch (parsed.code) {
      case "tmux_unavailable":
        // Local PoC path: paliad is running on a host without tmux/claude
        // (typically the legacy laptop-only build).
        return t("paliadin.error.local_only");
      case "mriver_unreachable":
        // t-paliad-151: prod path's mRiver is offline (laptop asleep, off
        // tailnet, or paliadin-shim missing).
        return t("paliadin.error.mriver_unreachable");
      case "shim_auth_failed":
        // SSH key wrong or authorized_keys drifted.
        return t("paliadin.error.shim_auth_failed");
      case "shim_error":
      case "bootstrap_failed":
        // Generic remote shim failure or system-prompt bootstrap error.
        return t("paliadin.error.shim_error");
      case "timeout":
        return t("paliadin.error.timeout");
    }
  } catch {
    // Not JSON — fall through to the generic connection-lost message
    // rather than leaking a raw payload into the bubble.
  }
  return t("paliadin.error.connection_lost");
}

function cleanupTurn(): void {
  if (currentEventSource) {
    currentEventSource.close();
    currentEventSource = null;
  }
  currentTurnId = null;
  toggleStopButton(false);
}

function toggleStopButton(streaming: boolean): void {
  const send = document.getElementById("paliadin-send") as HTMLButtonElement | null;
  const stop = document.getElementById("paliadin-stop") as HTMLButtonElement | null;
  if (send) send.style.display = streaming ? "none" : "";
  if (stop) {
    stop.style.display = streaming ? "" : "none";
    stop.onclick = () => {
      cleanupTurn();
    };
  }
}

function appendBubble(role: "user" | "assistant", text: string): HTMLElement {
  const stream = document.getElementById("paliadin-stream")!;
  const bubble = document.createElement("div");
  bubble.className = "paliadin-bubble paliadin-bubble--" + role;
  bubble.innerHTML = `
    <div class="paliadin-bubble-role">${role === "user" ? "Du" : "Paliadin"}</div>
    <div class="paliadin-bubble-text"></div>
    <div class="paliadin-bubble-meta" style="display:none"></div>
  `;
  bubble.querySelector(".paliadin-bubble-text")!.textContent = text;
  stream.appendChild(bubble);
  stream.scrollTop = stream.scrollHeight;
  return bubble;
}

// typewriter incrementally fills the bubble's text node so a one-shot
// content blob feels like streaming. ~5 ms per character; fast enough
// to keep up with even a 4k-char response.
function typewriter(bubble: HTMLElement, text: string): void {
  const node = bubble.querySelector(".paliadin-bubble-text")!;
  node.textContent = "";
  let i = 0;
  const speed = 6;
  const tick = () => {
    if (bubble.dataset.streaming !== "true") {
      // Streaming finished — finishBubble has already rendered the full
      // Markdown via dataset.fullText. Return without writing so we
      // don't replace the rendered HTML with raw text on a delayed tick.
      return;
    }
    if (i >= text.length) return;
    const next = Math.min(i + 8, text.length);
    node.textContent = text.slice(0, next);
    i = next;
    const stream = document.getElementById("paliadin-stream")!;
    stream.scrollTop = stream.scrollHeight;
    setTimeout(tick, speed);
  };
  tick();
}

function getBubbleText(bubble: HTMLElement): string {
  return bubble.querySelector(".paliadin-bubble-text")?.textContent || "";
}

// finishBubble parses the response for citation markers + tool-use
// evidence and renders both. Markers found in the text get replaced
// by anchor buttons; the meta row at the bottom shows
// "ran search_my_deadlines (3 results)".
function finishBubble(bubble: HTMLElement, data: any): void {
  const textNode = bubble.querySelector(".paliadin-bubble-text")! as HTMLElement;
  // Prefer the full text cached on the bubble at content-event time;
  // textContent may still reflect the typewriter's partial state.
  const raw = bubble.dataset.fullText ?? textNode.textContent ?? "";
  textNode.innerHTML = renderResponseHTML(raw);

  const metaEl = bubble.querySelector(".paliadin-bubble-meta") as HTMLElement | null;
  if (metaEl) {
    const tools = (data.used_tools || []) as string[];
    const rows = (data.rows_seen || []) as number[];
    if (tools.length > 0) {
      const parts = tools.map((t, i) => {
        const r = rows[i];
        return r != null ? `${t} (${r})` : t;
      });
      metaEl.innerHTML = "▸ " + parts.join(" · ");
      metaEl.style.display = "";
    }
  }
}


// startLatePoll registers the Janitor-patched row poller for one
// errored turn. When the row gains a response we swap the bubble's
// content + drop the error class + retroactively replace the history
// entry (which was never written for the failed turn — append now so
// reload renders the late reply).
function startLatePoll(turnId: string, bubble: HTMLElement): void {
  // Avoid duplicate pollers for the same turn (e.g. SSE error fires
  // twice in some browsers when the connection drops).
  latePolls.get(turnId)?.cancel();
  const handle = pollForLateResponse({
    turnId,
    onLateResponse: (turn) => {
      latePolls.delete(turnId);
      applyLateResponse(bubble, turn);
    },
    onGiveUp: () => {
      latePolls.delete(turnId);
    },
  });
  latePolls.set(turnId, handle);
}

function applyLateResponse(bubble: HTMLElement, turn: LateTurn): void {
  if (!turn.response) return;
  bubble.classList.remove("paliadin-bubble--error", "paliadin-bubble--late-pending");
  bubble.classList.add("paliadin-bubble--late");
  bubble.dataset.fullText = turn.response;
  bubble.dataset.streaming = "false";
  finishBubble(bubble, {
    used_tools: turn.used_tools,
    rows_seen: turn.rows_seen,
    classifier_tag: turn.classifier_tag,
    duration_ms: turn.duration_ms,
    chip_count: turn.chip_count,
  });
  // Inject a small "(verspätet)" marker into the meta row so it's
  // visible at a glance that this bubble was patched after the fact.
  const metaEl = bubble.querySelector(".paliadin-bubble-meta") as HTMLElement | null;
  if (metaEl) {
    const lateTag = document.createElement("span");
    lateTag.className = "paliadin-bubble-late-tag";
    lateTag.textContent = " · " + t("paliadin.late.marker");
    metaEl.appendChild(lateTag);
    metaEl.style.display = "";
  }
  // Persist so a reload shows the late response in place of the error.
  history.push({
    role: "assistant",
    text: turn.response,
    meta: {
      used_tools: turn.used_tools,
      rows_seen: turn.rows_seen,
      classifier_tag: turn.classifier_tag ?? undefined,
      duration_ms: turn.duration_ms ?? undefined,
      chip_count: turn.chip_count,
    },
    ts: new Date().toISOString(),
  });
  saveHistory();
}

function saveHistory(): void {
  localStorage.setItem(HISTORY_PREFIX + sessionId, JSON.stringify(history));
}

// PaliadinTurnRow mirrors the JSON returned by /api/paliadin/history
// (services.PaliadinTurn). Fields we don't render yet are skipped.
interface PaliadinTurnRow {
  turn_id: string;
  session_id: string;
  started_at: string;
  user_message: string;
  response?: string | null;
  used_tools?: string[] | null;
  rows_seen?: number[] | null;
  classifier_tag?: string | null;
  duration_ms?: number | null;
  chip_count?: number | null;
}

// Hydrate from /api/paliadin/history, replacing the localStorage cache
// when the DB returns rows. Fail-quiet on network / auth errors —
// localStorage is a perfectly good offline fallback.
async function hydrateFromServer(): Promise<void> {
  let rows: PaliadinTurnRow[] = [];
  try {
    const r = await fetch(
      "/api/paliadin/history?session=" + encodeURIComponent(sessionId) + "&limit=50",
      { credentials: "same-origin" },
    );
    if (!r.ok) return;
    const body = (await r.json()) as PaliadinTurnRow[] | null;
    rows = Array.isArray(body) ? body : [];
  } catch {
    return;
  }
  if (!rows.length) return;
  const reconstructed: HistoryEntry[] = [];
  for (const row of rows) {
    reconstructed.push({ role: "user", text: row.user_message, ts: row.started_at });
    if (typeof row.response === "string" && row.response.length > 0) {
      reconstructed.push({
        role: "assistant",
        text: row.response,
        ts: row.started_at,
        meta: {
          used_tools: row.used_tools ?? undefined,
          rows_seen: row.rows_seen ?? undefined,
          classifier_tag: row.classifier_tag ?? undefined,
          duration_ms: row.duration_ms ?? undefined,
          chip_count: row.chip_count ?? undefined,
        },
      });
    }
  }
  history = reconstructed;
  saveHistory();
  renderHistory();
}

function renderHistory(): void {
  const stream = document.getElementById("paliadin-stream");
  if (!stream) return;
  // Clear non-empty bubbles, keep the empty-state.
  Array.from(stream.children).forEach((el) => {
    if (!el.classList.contains("paliadin-empty")) el.remove();
  });
  if (history.length === 0) {
    const empty = document.getElementById("paliadin-empty");
    if (empty) empty.style.display = "";
    return;
  }
  const empty = document.getElementById("paliadin-empty");
  if (empty) empty.style.display = "none";
  history.forEach((h) => {
    const bubble = appendBubble(h.role, h.text);
    if (h.role === "assistant" && h.meta) {
      bubble.dataset.streaming = "false";
      finishBubble(bubble, {
        used_tools: h.meta.used_tools,
        rows_seen: h.meta.rows_seen,
      });
    }
  });
}
