import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense } from "react";
import { ArrowLeft, Check, Copy, Download, Plus } from "lucide-react";

import type { CapturedLog } from "../proxy/schemas";
import { exportLogsAsZip } from "../lib/export-logs";
import { formatTokens } from "../lib/utils";
import packageJson from "../../package.json";
import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";

import { CrabLogo } from "./ui/crab-logo";
import { crabVariants } from "./ui/crab-variants";
import { McpLogo } from "./ui/mcp-logo";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
import { SettingsDialog } from "./providers/SettingsDialog";
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";

function truncateSessionId(id: string): string {
  if (id.length <= 30) return id;
  return id.slice(0, 12) + "…" + id.slice(-12);
}

function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
  let totalIn = 0;
  let totalOut = 0;
  for (const log of logs) {
    if (log.inputTokens !== null) totalIn += log.inputTokens;
    if (log.outputTokens !== null) totalOut += log.outputTokens;
  }
  return { totalIn, totalOut };
}

function formatTimeRange(logs: CapturedLog[]): string | null {
  const first = logs[0];
  const last = logs[logs.length - 1];
  if (first === undefined || last === undefined) return null;
  const format = (iso: string): string =>
    new Date(iso).toLocaleTimeString([], {
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    });
  return `${format(first.timestamp)} - ${format(last.timestamp)}`;
}

function getFirstUserAgent(logs: CapturedLog[]): string | null {
  for (const log of logs) {
    if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
      return log.userAgent;
    }
  }
  return null;
}

function CopyableCommand({ command }: { command: string }): JSX.Element {
  const [copied, setCopied] = useState(false);

  const handleCopy = useCallback(() => {
    void window.navigator.clipboard.writeText(command).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  }, [command]);

  return (
    <div className="inline-flex items-center gap-2 bg-muted rounded-md px-3 py-2">
      <pre className="text-blue-500 font-mono text-sm m-0">{command}</pre>
      <button
        type="button"
        onClick={handleCopy}
        className="text-muted-foreground hover:text-foreground transition-colors shrink-0 cursor-pointer"
        aria-label="Copy command"
      >
        {copied ? (
          <svg
            width="16"
            height="16"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path d="M20 6 9 17l-5-5" />
          </svg>
        ) : (
          <svg
            width="16"
            height="16"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
            <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
          </svg>
        )}
      </button>
    </div>
  );
}

function McpReadyBadge(): JSX.Element {
  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <span className="inline-flex h-7 items-center gap-2 rounded-md border border-cyan-400/30 bg-cyan-500/10 px-2.5 font-mono text-[11px] font-medium text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.08)]">
            <span className="size-1.5 rounded-full bg-emerald-300 shadow-[0_0_8px_rgba(110,231,183,0.8)]" />
            MCP Ready
            <span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
          </span>
        </TooltipTrigger>
        <TooltipContent sideOffset={8} className="max-w-[320px] text-left leading-relaxed">
          Coding agents can inspect logs, replay requests, test providers, and debug sessions
          through MCP at /api/mcp.
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}

function SessionContextBar({
  sessionId,
  logs,
  totalIn,
  totalOut,
}: {
  sessionId: string;
  logs: CapturedLog[];
  totalIn: number;
  totalOut: number;
}): JSX.Element {
  const [copied, setCopied] = useState(false);
  const timeRange = useMemo(() => formatTimeRange(logs), [logs]);
  const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);

  const handleCopyLink = useCallback(() => {
    void window.navigator.clipboard.writeText(window.location.href).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  }, []);

  return (
    <div className="mb-4 flex items-center gap-3 border border-border rounded-md bg-muted/20 px-3 py-2 text-xs">
      <a
        href="/"
        className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
        aria-label="Back to all sessions"
        title="Back to all sessions"
      >
        <ArrowLeft className="size-3.5" />
      </a>
      <div className="min-w-0 flex-1">
        <div className="flex min-w-0 items-center gap-2">
          <span className="font-mono font-semibold text-purple-400/90 truncate" title={sessionId}>
            {truncateSessionId(sessionId)}
          </span>
          {userAgent !== null && (
            <span
              className="font-mono text-muted-foreground truncate max-w-[220px]"
              title={userAgent}
            >
              {userAgent}
            </span>
          )}
        </div>
        <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
          <span>
            {logs.length} request{logs.length !== 1 ? "s" : ""}
          </span>
          {timeRange !== null && <span>{timeRange}</span>}
          {(totalIn > 0 || totalOut > 0) && (
            <span className="font-mono">
              {formatTokens(totalIn)} in / {formatTokens(totalOut)} out
            </span>
          )}
        </div>
      </div>
      <button
        type="button"
        onClick={handleCopyLink}
        className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
        aria-label={copied ? "Copied session link" : "Copy session link"}
        title={copied ? "Copied session link" : "Copy session link"}
      >
        {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
      </button>
    </div>
  );
}

export type ProxyViewerProps = {
  logs: CapturedLog[];
  sessions: string[];
  models: string[];
  selectedSession: string;
  selectedModel: string;
  onSessionChange: (session: string) => void;
  onModelChange: (model: string) => void;
  onClearAll: () => void;
  /** Clear only the logs whose ids are passed. Called by the per-group
   *  Clear button on each conversation header. */
  onClearGroup: (ids: number[]) => void;
  viewMode: "simple" | "full";
  onViewModeChange: (mode: "simple" | "full") => void;
  /** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
  strip: boolean;
  /** Slow-response threshold in seconds. `0` disables the warning indicator. */
  slowResponseThresholdSeconds: number;
  /** Hide the session filter dropdown. Used on `/session/$id` routes where
   *  the session is already pinned by the URL and the dropdown would just
   *  fight the URL state. */
  hideSessionFilter?: boolean;
  /** Session id pinned by a `/session/$id` route. Enables session-page chrome. */
  pinnedSessionId?: string;
};

export function ProxyViewer({
  logs,
  sessions,
  models,
  selectedSession,
  selectedModel,
  onSessionChange,
  onModelChange,
  onClearAll,
  onClearGroup,
  viewMode,
  onViewModeChange,
  strip,
  slowResponseThresholdSeconds,
  hideSessionFilter = false,
  pinnedSessionId,
}: ProxyViewerProps): JSX.Element {
  const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
  const [exporting, setExporting] = useState(false);
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
    "hidden",
  );
  const logListRef = useRef<HTMLDivElement>(null);
  const logListWrapperRef = useRef<HTMLDivElement>(null);
  useKeyboardNavigation(logListRef, logListWrapperRef);

  useEffect(() => {
    const perCrabDuration = 400;
    const startDelay = 50;
    const playingDone = startDelay + crabVariants.length * perCrabDuration;
    const t1 = setTimeout(() => setCrabEntrancePhase("playing"), startDelay);
    const t2 = setTimeout(() => setCrabEntrancePhase("done"), playingDone);
    return () => {
      clearTimeout(t1);
      clearTimeout(t2);
    };
  }, []);

  useEffect(() => {
    if (pinnedSessionId === undefined) {
      document.title = "LLM Inspector";
      return;
    }
    const requestLabel = logs.length === 1 ? "1 req" : `${logs.length} req`;
    document.title = `${truncateSessionId(pinnedSessionId)} - ${requestLabel} - LLM Inspector`;
  }, [logs.length, pinnedSessionId]);

  const handleExport = useCallback(async () => {
    setExporting(true);
    try {
      await exportLogsAsZip(logs);
    } finally {
      setExporting(false);
    }
  }, [logs]);
  // Close the compare drawer when the user changes the session or model
  // filter, since the predecessor relationship may no longer be meaningful.
  useEffect(() => {
    setComparePair(null);
  }, [selectedSession, selectedModel]);

  const closeCompare = useCallback(() => {
    setComparePair(null);
  }, []);

  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
  const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
  const handleCompareWithPrevious = useCallback(
    (log: CapturedLog) => {
      const predecessor = comparisonPredecessors.get(log.id);
      if (predecessor !== undefined) setComparePair([predecessor, log]);
    },
    [comparisonPredecessors],
  );

  return (
    <div className="max-w-[1400px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto px-6 pb-6">
      {/* Brand row */}
      <div className="grid grid-cols-[1fr_auto_1fr] items-start gap-3 pt-6 pb-8">
        <div />
        <h1 className="flex min-w-0 flex-col items-center gap-2 text-center">
          <span className="flex max-w-[calc(100vw-7rem)] items-end gap-2 whitespace-nowrap">
            {/* Crab family — hover to animate together */}
            <span className="flex shrink-0 items-end gap-1 group cursor-default" aria-hidden="true">
              <CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
              <span className="hidden items-end gap-0.5 sm:flex">
                {crabVariants.map((Crab, i) => {
                  const color = [
                    "text-amber-500",
                    "text-rose-500",
                    "text-sky-500",
                    "text-emerald-500",
                    "text-violet-500",
                    "text-orange-500",
                    "text-cyan-500",
                    "text-pink-500",
                    "text-lime-500",
                    "text-blue-500",
                    "text-yellow-500",
                    "text-fuchsia-500",
                  ][i];
                  const entranceClass =
                    crabEntrancePhase === "hidden"
                      ? "opacity-0 scale-0"
                      : crabEntrancePhase === "playing"
                        ? "animate-crab-piano-pop"
                        : "";
                  return (
                    <Crab
                      key={i}
                      className={`size-5 ${color} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1 ${entranceClass}`}
                      style={{
                        transitionDelay: `${i * 50}ms`,
                        ...(crabEntrancePhase === "playing"
                          ? { animationDelay: `${i * 400}ms` }
                          : {}),
                      }}
                    />
                  );
                })}
              </span>
            </span>
            <span className="flex min-w-0 items-baseline gap-2 pl-1">
              <span className="truncate text-lg font-bold">LLM Inspector</span>
              <span className="shrink-0 font-mono text-xs font-semibold text-muted-foreground">
                v{packageJson.version}
              </span>
            </span>
            <Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
            <McpLogo className="size-10 shrink-0" />
          </span>
          <McpReadyBadge />
        </h1>
        <div className="justify-self-end">
          <SettingsDialog />
        </div>
      </div>

      {pinnedSessionId !== undefined && (
        <SessionContextBar
          sessionId={pinnedSessionId}
          logs={logs}
          totalIn={totalIn}
          totalOut={totalOut}
        />
      )}

      {/* Controls + Filters */}
      <div className="flex items-center gap-3 mb-4">
        {!hideSessionFilter && (
          <Select value={selectedSession} onValueChange={onSessionChange}>
            <SelectTrigger className="flex-1 max-w-[350px] text-xs">
              <SelectValue placeholder="All sessions" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="__all__">All sessions</SelectItem>
              {sessions.map((s) => (
                <SelectItem key={s} value={s}>
                  {truncateSessionId(s)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        )}
        <Select value={selectedModel} onValueChange={onModelChange}>
          <SelectTrigger className="flex-1 max-w-[250px] text-xs">
            <SelectValue placeholder="All models" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="__all__">All models</SelectItem>
            {models.map((m) => (
              <SelectItem key={m} value={m}>
                {m}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <div className="flex items-center border border-border rounded-md overflow-hidden">
          <button
            type="button"
            onClick={() => onViewModeChange("simple")}
            className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
              viewMode === "simple"
                ? "bg-muted text-foreground"
                : "text-muted-foreground hover:bg-muted/50"
            }`}
          >
            Simple
          </button>
          <button
            type="button"
            onClick={() => onViewModeChange("full")}
            className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
              viewMode === "full"
                ? "bg-muted text-foreground"
                : "text-muted-foreground hover:bg-muted/50"
            }`}
          >
            Full
          </button>
        </div>
        <div className="flex-1" />
        <span className="text-muted-foreground text-xs font-mono">
          {logs.length} request{logs.length !== 1 ? "s" : ""}
          {totalIn > 0 || totalOut > 0
            ? ` · ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
            : ""}
        </span>
        {logs.length > 0 && (
          <button
            type="button"
            onClick={() => {
              void handleExport();
            }}
            disabled={exporting}
            className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
            title="Export all logs as JSON ZIP"
          >
            {exporting ? (
              <span>Exporting...</span>
            ) : (
              <>
                <Download className="size-3.5" />
                <span>Export</span>
              </>
            )}
          </button>
        )}
        <button
          type="button"
          onClick={onClearAll}
          className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer rounded-md hover:bg-muted"
          title="Clear all logs"
        >
          Clear
        </button>
      </div>

      {/* Log list */}
      <div>
        {logs.length === 0 ? (
          selectedSession !== "__all__" ? (
            <div className="text-center text-muted-foreground py-16 space-y-4">
              <p className="text-sm font-medium">Session not found</p>
              <p className="text-xs font-mono bg-muted px-3 py-1 rounded inline-block max-w-[500px] break-all">
                {truncateSessionId(selectedSession)}
              </p>
              <p className="text-xs">
                This session may have been cleared or never existed.{" "}
                {hideSessionFilter ? (
                  <a href="/" className="underline hover:text-foreground transition-colors">
                    Back to all sessions
                  </a>
                ) : (
                  <button
                    type="button"
                    onClick={() => onSessionChange("__all__")}
                    className="underline hover:text-foreground transition-colors"
                  >
                    Show all sessions
                  </button>
                )}
              </p>
            </div>
          ) : (
            <div className="text-center text-muted-foreground py-16 space-y-4">
              <p className="text-sm">No requests captured yet.</p>
              <p className="text-xs">Route AI coding tools through the proxy:</p>
              <CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
            </div>
          )
        ) : (
          <div
            ref={logListWrapperRef}
            tabIndex={0}
            className="flex flex-col gap-2 focus:outline-none"
          >
            <div ref={logListRef}>
              {groups.map((group) => (
                <ConversationGroup
                  key={group.id}
                  group={group}
                  viewMode={viewMode}
                  strip={strip}
                  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
                  cacheTrends={cacheTrends}
                  onCompareWithPrevious={handleCompareWithPrevious}
                  comparisonPredecessors={comparisonPredecessors}
                  onClearGroup={onClearGroup}
                  standalone={groups.length === 1}
                />
              ))}
            </div>
          </div>
        )}
      </div>

      {/* Compare drawer — sibling of the log list, not a route change. */}
      {comparePair !== null && (
        <Suspense fallback={null}>
          <LazyCompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
        </Suspense>
      )}
    </div>
  );
}
