import type { Hono } from "hono";
import { readFileSync, readdirSync, statSync, existsSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { CLAUDE_PROJECTS_DIR, ARCHIVED_CHATS_FILE, CHAT_NAMES_FILE, USER_PROJECT_KEY } from "./paths";
import { loadThreadChats } from "./thread-chats";
import { loadSystemSessions } from "./system-sessions";

/** Markers that identify v1's structured-prompt boilerplate so we don't
 *  use those as the user-visible title for a chat. */
const SYSTEM_PROMPT_MARKERS = [
  "You are operating in COMMAND mode",
  "CRITICAL OUTPUT RULE",
  "<command-name>",
  "<system-reminder>",
  "Caveat: The messages below were generated",
];

/** Stable machine-readable sentinel prepended to every server-initiated
 *  agent prompt. Survives prompt rewrites; detected before the legacy
 *  natural-language prefixes below. */
export const KNITOR_SYSTEM_SENTINEL = "KNITOR:SYSTEM_SESSION";

/** Prefixes that uniquely identify a system-initiated session (check
 *  agent's per-item verdict run, or the scan-discovery agent). Detected
 *  by content so old sessions from before addSystemSession() was wired
 *  in get filtered too, without a backfill pass over the json log. */
const SYSTEM_SESSION_PREFIXES = [
  // Primary: stable sentinel prepended to all server-initiated prompts (new sessions)
  KNITOR_SYSTEM_SENTINEL,
  // Legacy fallback: single-item check agent (check.ts) — pre-sentinel sessions
  "You are evaluating ONE flagged item from Pavel Racu",
  // Legacy fallback: multi-item batch check (check-scheduler.ts)
  "You are evaluating ",
  // Legacy fallback: scan-discovery agent
  "You are scanning Pavel",
];

export type SessionMeta = {
  title: string;
  count: number;
  started?: number;
  preview?: string;
  tools?: string[];
  cwd?: string;
  /** True when the session's first user message is a known system-prompt
   *  prefix (check agent, scan-discovery agent). The chats endpoint
   *  filters these out by default so they don't clutter HistoryPanel. */
  isSystem?: boolean;
};

/**
 * mtime-keyed cache so we don't re-parse 298 JSONL files on every /api/chats hit.
 * Re-read only files whose mtime changed since last index. Cuts /api/chats from
 * 350-650ms (cold parse of all transcripts) to ~10ms after the initial warm-up.
 */
const metaCache = new Map<string, { mtime: number; meta: SessionMeta }>();

function readSessionMetaCached(file: string, mtimeMs: number): SessionMeta {
  const key = file;
  const stamp = Math.floor(mtimeMs);
  const cached = metaCache.get(key);
  if (cached && cached.mtime === stamp) return cached.meta;
  const meta = readSessionMeta(file);
  metaCache.set(key, { mtime: stamp, meta });
  return meta;
}

/**
 * Parse a single .jsonl chat log into the metadata the dashboard cares about:
 *   - title:    first non-system user message
 *   - preview:  first assistant text block
 *   - tools:    distinct tool names invoked (capped at 6 for display)
 *   - count:    total user+assistant messages
 *   - started:  earliest timestamp (unix seconds)
 *   - cwd:      working directory recorded in the session's init event
 */
export function readSessionMeta(file: string): SessionMeta {
  let title = "";
  let preview = "";
  let count = 0;
  let started: number | undefined;
  let cwd: string | undefined;
  let isSystem = false;
  const tools = new Set<string>();

  try {
    const fd = readFileSync(file, "utf8");
    const lines = fd.split("\n");
    for (const line of lines) {
      if (!line) continue;
      let m: any;
      try { m = JSON.parse(line); } catch { continue; }

      // cwd lives on either the top-level event or the system:init event
      if (!cwd && typeof m.cwd === "string") cwd = m.cwd;
      if (!cwd && m.type === "system" && m.subtype === "init" && typeof m.cwd === "string") cwd = m.cwd;

      if (m.type === "user" || m.type === "assistant") count++;

      if (!started && m.timestamp) {
        const t = Date.parse(m.timestamp);
        if (!isNaN(t)) started = Math.floor(t / 1000);
      }

      if (m.type === "user" && !title && m.message?.content) {
        let text = "";
        const c = m.message.content;
        if (typeof c === "string") text = c;
        else if (Array.isArray(c)) {
          for (const block of c) {
            if (block.type === "text" && typeof block.text === "string") {
              text = block.text;
              break;
            }
          }
        }
        // Tag system-initiated sessions on the first user message we see.
        if (text && !isSystem && SYSTEM_SESSION_PREFIXES.some(p => text.startsWith(p))) {
          isSystem = true;
        }
        if (text && !SYSTEM_PROMPT_MARKERS.some(p => text.startsWith(p) || text.includes(p))) {
          title = text.replace(/\s+/g, " ").trim().slice(0, 200);
        }
      }

      if (m.type === "assistant" && m.message?.content) {
        for (const block of m.message.content) {
          if (block.type === "text" && typeof block.text === "string") {
            if (!preview && block.text.trim()) {
              preview = block.text.replace(/\s+/g, " ").trim().slice(0, 220);
            }
          } else if (block.type === "tool_use" && block.name) {
            tools.add(String(block.name));
          }
        }
      }
    }
  } catch {}

  return { title, count, started, preview, tools: Array.from(tools).slice(0, 6), cwd, isSystem };
}

export type ChatRow = {
  sessionId: string;
  project: string;
  projectRaw: string;
  cwd: string;
  /** User-visible name. customName if set, else auto-derived from first user message. */
  title: string;
  /** True iff the user explicitly renamed this chat (tells the UI a custom name is in use). */
  hasCustomName?: boolean;
  preview: string;
  tools: string[];
  messageCount: number;
  size: number;
  mtime: number;
  started?: number;
  archived?: boolean;
  /** Set if this chat was started via "Work on this" — the thread it's bound to. */
  threadKey?: string;
};

type ArchivedEntry = { sessionId: string; projectRaw: string; archivedAt: string };

function loadArchived(): ArchivedEntry[] {
  if (!existsSync(ARCHIVED_CHATS_FILE)) return [];
  try {
    const data = JSON.parse(readFileSync(ARCHIVED_CHATS_FILE, "utf8"));
    return Array.isArray(data) ? data : [];
  } catch { return []; }
}
function saveArchived(list: ArchivedEntry[]) {
  writeFileSync(ARCHIVED_CHATS_FILE, JSON.stringify(list, null, 2));
}
function archivedSet(): Set<string> {
  return new Set(loadArchived().map((e) => e.sessionId));
}

/** Custom chat names — sessionId → user-supplied name. Empty string deletes. */
function loadChatNames(): Record<string, string> {
  if (!existsSync(CHAT_NAMES_FILE)) return {};
  try {
    const data = JSON.parse(readFileSync(CHAT_NAMES_FILE, "utf8"));
    return data && typeof data === "object" ? data : {};
  } catch { return {}; }
}
function saveChatNames(map: Record<string, string>) {
  writeFileSync(CHAT_NAMES_FILE, JSON.stringify(map, null, 2));
}

/**
 * Registers:
 *   GET /api/chats — list all chats from ~/.claude/projects, sorted by mtime desc
 *   GET /api/chat-session?project=<slug>&id=<uuid> — full message history (parsed)
 */
export function registerChatsRoutes(app: Hono) {
  app.get("/api/chats", async (c) => {
    const showArchived = c.req.query("archived") === "true";
    const archived = archivedSet();
    const customNames = loadChatNames();
    // System-initiated sessions (scan-now, per-thread checks) — filter out
    // unless ?system=true is explicitly set (debug/inspection only).
    const showSystem = c.req.query("system") === "true";
    const systemSessions = loadSystemSessions();
    // Reverse map: sessionId → threadKey, for the History panel's thread tag.
    const threadChats = loadThreadChats();
    const sessionToThread: Record<string, string> = {};
    for (const [threadKey, chats] of Object.entries(threadChats)) {
      for (const chat of chats) sessionToThread[chat.sessionId] = threadKey;
    }
    const out: ChatRow[] = [];
    let projects: string[] = [];
    try { projects = readdirSync(CLAUDE_PROJECTS_DIR); } catch { return c.json([]); }
    for (const proj of projects) {
      const projDir = join(CLAUDE_PROJECTS_DIR, proj);
      let files: string[] = [];
      try { files = readdirSync(projDir).filter((f) => f.endsWith(".jsonl")); } catch { continue; }
      for (const f of files) {
        const full = join(projDir, f);
        try {
          const st = statSync(full);
          if (st.size < 200) continue; // skip empty/queue-only files
          const sessionId = f.replace(/\.jsonl$/, "");
          const isArchived = archived.has(sessionId);
          // Filter logic: by default exclude archived. ?archived=true → only archived.
          if (showArchived ? !isArchived : isArchived) continue;
          const meta = readSessionMetaCached(full, st.mtimeMs);
          if (!meta.title) continue;
          // Hide system-initiated chats unless ?system=true. Catches BOTH
          // sessions tagged at run-time via addSystemSession() AND any
          // older sessions whose content matches a known system-prompt
          // prefix — so backfilling system_sessions.json isn't needed.
          if (!showSystem && (systemSessions.has(sessionId) || meta.isSystem)) continue;
          const custom = customNames[sessionId];
          out.push({
            sessionId,
            project: proj.replace(new RegExp(`^${USER_PROJECT_KEY}-?`), "") || "~",
            projectRaw: proj,
            cwd: meta.cwd || "",
            title: custom || meta.title,
            hasCustomName: !!custom,
            preview: meta.preview || "",
            tools: meta.tools || [],
            messageCount: meta.count,
            size: st.size,
            mtime: Math.floor(st.mtimeMs / 1000),
            started: meta.started,
            archived: isArchived,
            threadKey: sessionToThread[sessionId],
          });
        } catch {}
      }
    }
    out.sort((a, b) => b.mtime - a.mtime);
    return c.json(out);
  });

  app.post("/api/chats/archive", async (c) => {
    const { sessionId, projectRaw } = await c.req.json<{ sessionId: string; projectRaw: string }>();
    if (!sessionId || !projectRaw) return c.json({ ok: false, error: "missing fields" }, 400);
    const list = loadArchived();
    if (!list.some((e) => e.sessionId === sessionId)) {
      list.push({ sessionId, projectRaw, archivedAt: new Date().toISOString().slice(0, 10) });
      saveArchived(list);
    }
    return c.json({ ok: true });
  });

  app.post("/api/chats/unarchive", async (c) => {
    const { sessionId } = await c.req.json<{ sessionId: string }>();
    if (!sessionId) return c.json({ ok: false, error: "missing sessionId" }, 400);
    const list = loadArchived().filter((e) => e.sessionId !== sessionId);
    saveArchived(list);
    return c.json({ ok: true });
  });

  app.post("/api/chats/rename", async (c) => {
    const { sessionId, name } = await c.req.json<{ sessionId: string; name: string }>();
    if (!sessionId) return c.json({ ok: false, error: "missing sessionId" }, 400);
    const map = loadChatNames();
    const trimmed = (name || "").trim();
    if (trimmed) map[sessionId] = trimmed.slice(0, 200);
    else delete map[sessionId]; // empty name = remove custom, fall back to auto-derived
    saveChatNames(map);
    return c.json({ ok: true, name: trimmed });
  });

  app.get("/api/chat-session", (c) => {
    const projectRaw = c.req.query("project");
    const sessionId = c.req.query("id");
    if (!projectRaw || !sessionId) return c.text("missing params", 400);
    // Reject anything that could escape the projects dir
    if (projectRaw.includes("..") || sessionId.includes("..") || sessionId.includes("/")) {
      return c.text("forbidden", 403);
    }
    const file = join(CLAUDE_PROJECTS_DIR, projectRaw, `${sessionId}.jsonl`);
    try {
      const lines = readFileSync(file, "utf8").split("\n").filter(Boolean);
      const messages: any[] = [];
      for (const line of lines) {
        try {
          const m = JSON.parse(line);
          if (m.type === "user" || m.type === "assistant") {
            messages.push({
              role: m.message?.role || m.type,
              content: m.message?.content,
              timestamp: m.timestamp,
              isMeta: m.isMeta || false,
            });
          }
        } catch {}
      }
      return c.json({ messages });
    } catch (err) {
      return c.text(`error: ${err}`, 404);
    }
  });
}
