import { useVirtualizer } from "@tanstack/react-virtual";
import { Columns2, Rows3 } from "lucide-react";
import { memo, useRef, useState, type JSX } from "react";
import { cn } from "../../../lib/utils";
import type { DiffLine, DiffResult } from "./computeDiff";

type DiffMode = "unified" | "split";

/**
 * Approximate line height (px) used for the virtualizer's `estimateSize`.
 * Matches the `py-1` + `text-xs` (12px line-height ~ 18px) row padding so
 * the estimate is close enough that we rarely need to re-measure.
 */
const ESTIMATED_LINE_HEIGHT = 22;

/**
 * Cap on how many diff lines we render. Bodies larger than this still get
 * the diff computed (so the line count is accurate) but only the first
 * `MAX_RENDERED_LINES` are shown, with a notice explaining the truncation.
 * This protects the tab from freezing on accidental large payloads.
 */
const MAX_RENDERED_LINES = 10_000;

type DiffViewProps = {
  result: DiffResult;
  /** Optional caption shown in the empty state. */
  emptyLabel?: string;
};

function kindPrefix(kind: "context" | "added" | "removed"): string {
  if (kind === "added") return "+";
  if (kind === "removed") return "-";
  return " ";
}

function kindClass(kind: "context" | "added" | "removed"): string {
  if (kind === "added") {
    return "bg-green-500/10 text-green-700 dark:text-green-300";
  }
  if (kind === "removed") {
    return "bg-red-500/10 text-red-700 dark:text-red-300";
  }
  return "text-foreground/80";
}

/**
 * Render a `DiffResult` in unified or split mode. The unified view is a
 * single column with `+` / `-` / ` ` prefixes; the split view pairs up
 * consecutive removed+added runs into "changed" rows for side-by-side
 * reading.
 *
 * Virtualized because request bodies with full conversation history can be
 * tens of thousands of lines long.
 */
const DiffViewInner = function DiffView({ result, emptyLabel }: DiffViewProps): JSX.Element {
  const [mode, setMode] = useState<DiffMode>("unified");
  const scrollRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: result.lines.length,
    getScrollElement: () => scrollRef.current,
    estimateSize: () => ESTIMATED_LINE_HEIGHT,
    overscan: 10,
  });

  if (result.isEmpty) {
    return (
      <div
        className="rounded border border-dashed border-border bg-muted/30 px-4 py-6 text-center text-xs text-muted-foreground"
        data-testid="diff-empty"
      >
        {emptyLabel ?? "No transformation applied — raw and processed are identical."}
      </div>
    );
  }

  const totalLines = result.lines.length;
  const truncated = totalLines > MAX_RENDERED_LINES;
  const visibleLines = truncated ? result.lines.slice(0, MAX_RENDERED_LINES) : result.lines;

  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between text-xs text-muted-foreground">
        <span>
          {totalLines.toLocaleString()} line{totalLines === 1 ? "" : "s"}
          {truncated && ` (showing first ${MAX_RENDERED_LINES.toLocaleString()})`}
        </span>
        <div className="inline-flex rounded-md border border-border overflow-hidden">
          <button
            type="button"
            onClick={() => setMode("unified")}
            aria-pressed={mode === "unified"}
            className={cn(
              "flex items-center gap-1 px-2 py-1 transition-colors",
              mode === "unified" ? "bg-muted text-foreground" : "hover:bg-muted/50",
            )}
            title="Unified diff (single column)"
          >
            <Rows3 className="size-3" />
            Unified
          </button>
          <button
            type="button"
            onClick={() => setMode("split")}
            aria-pressed={mode === "split"}
            className={cn(
              "flex items-center gap-1 px-2 py-1 transition-colors border-l border-border",
              mode === "split" ? "bg-muted text-foreground" : "hover:bg-muted/50",
            )}
            title="Split diff (side by side)"
          >
            <Columns2 className="size-3" />
            Split
          </button>
        </div>
      </div>
      <div
        ref={scrollRef}
        className="max-h-[60vh] overflow-auto rounded border border-border bg-background font-mono text-xs"
        data-testid="diff-viewport"
      >
        {mode === "unified" ? (
          <UnifiedRows virtualizer={virtualizer} lines={visibleLines} />
        ) : (
          <SplitRows lines={visibleLines} />
        )}
      </div>
    </div>
  );
};

function UnifiedRows({
  virtualizer,
  lines,
}: {
  virtualizer: ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;
  lines: ReadonlyArray<DiffLine>;
}): JSX.Element {
  return (
    <div
      style={{
        height: `${virtualizer.getTotalSize()}px`,
        width: "100%",
        position: "relative",
      }}
    >
      {virtualizer.getVirtualItems().map((virtualRow) => {
        const line = lines[virtualRow.index];
        if (line === undefined) return null;
        return (
          <div
            key={virtualRow.key}
            data-index={virtualRow.index}
            ref={virtualizer.measureElement}
            className={cn("flex items-start gap-3 px-2", kindClass(line.kind))}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <span className="select-none text-muted-foreground/60 w-10 text-right tabular-nums">
              {line.oldLineNumber ?? ""}
            </span>
            <span className="select-none text-muted-foreground/60 w-10 text-right tabular-nums">
              {line.newLineNumber ?? ""}
            </span>
            <span
              className={cn(
                "select-none w-3 text-right font-bold",
                line.kind === "added" && "text-green-600 dark:text-green-400",
                line.kind === "removed" && "text-red-600 dark:text-red-400",
              )}
            >
              {kindPrefix(line.kind)}
            </span>
            <span className="whitespace-pre-wrap break-all flex-1">{line.text}</span>
          </div>
        );
      })}
    </div>
  );
}

/**
 * Convert a stream of context / removed / added lines into "rows" suitable
 * for a split view:
 *  - A run of `removed` followed by a run of `added` is paired line-by-line
 *    as a "changed" row (the shorter side is padded with blanks).
 *  - A `removed`-only run becomes rows marked as deletion-only.
 *  - An `added`-only run becomes rows marked as addition-only.
 *  - `context` lines become rows where both sides show the same text.
 */
type SplitRow = {
  left: { text: string; lineNumber: number | null } | null;
  right: { text: string; lineNumber: number | null } | null;
  /** "equal" | "changed" | "removed" | "added" */
  kind: "equal" | "changed" | "removed" | "added";
};

function buildSplitRows(lines: ReadonlyArray<DiffLine>): SplitRow[] {
  const rows: SplitRow[] = [];
  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    if (line === undefined) break;
    if (line.kind === "context") {
      rows.push({
        left: { text: line.text, lineNumber: line.oldLineNumber },
        right: { text: line.text, lineNumber: line.newLineNumber },
        kind: "equal",
      });
      i += 1;
      continue;
    }
    // Collect a run of removes followed by a run of adds.
    const removes: DiffLine[] = [];
    const adds: DiffLine[] = [];
    while (i < lines.length && lines[i]?.kind === "removed") {
      const removed = lines[i];
      if (removed !== undefined) removes.push(removed);
      i += 1;
    }
    while (i < lines.length && lines[i]?.kind === "added") {
      const added = lines[i];
      if (added !== undefined) adds.push(added);
      i += 1;
    }
    const pairCount = Math.max(removes.length, adds.length);
    for (let p = 0; p < pairCount; p += 1) {
      const r = removes[p];
      const a = adds[p];
      if (r !== undefined && a !== undefined) {
        rows.push({
          left: { text: r.text, lineNumber: r.oldLineNumber },
          right: { text: a.text, lineNumber: a.newLineNumber },
          kind: "changed",
        });
      } else if (r !== undefined) {
        rows.push({
          left: { text: r.text, lineNumber: r.oldLineNumber },
          right: null,
          kind: "removed",
        });
      } else if (a !== undefined) {
        rows.push({
          left: null,
          right: { text: a.text, lineNumber: a.newLineNumber },
          kind: "added",
        });
      }
    }
  }
  return rows;
}

function SplitRowClass(kind: SplitRow["kind"]): string {
  switch (kind) {
    case "changed":
      return "bg-amber-500/10";
    case "removed":
      return "bg-red-500/10";
    case "added":
      return "bg-green-500/10";
    case "equal":
      return "";
  }
}

function SplitRows({ lines }: { lines: ReadonlyArray<DiffLine> }): JSX.Element {
  const rows = buildSplitRows(lines);
  return (
    <div data-testid="diff-split">
      {rows.map((row, idx) => (
        <div
          // idx is stable for a given diff result; the rows array is rebuilt
          // when the input `lines` change, so React's reconciliation handles
          // re-renders correctly.
          // biome-ignore lint/suspicious/noArrayIndexKey: see above
          key={idx}
          className={cn("grid grid-cols-2 font-mono text-xs", SplitRowClass(row.kind))}
        >
          <div className="flex items-start gap-2 px-2 border-r border-border/50">
            <span className="select-none text-muted-foreground/60 w-8 text-right tabular-nums">
              {row.left?.lineNumber ?? ""}
            </span>
            <span
              className={cn(
                "select-none w-3 text-right font-bold",
                (row.kind === "removed" || row.kind === "changed") &&
                  "text-red-600 dark:text-red-400",
              )}
            >
              {row.left !== null ? "-" : " "}
            </span>
            <span className="whitespace-pre-wrap break-all flex-1">{row.left?.text ?? ""}</span>
          </div>
          <div className="flex items-start gap-2 px-2">
            <span className="select-none text-muted-foreground/60 w-8 text-right tabular-nums">
              {row.right?.lineNumber ?? ""}
            </span>
            <span
              className={cn(
                "select-none w-3 text-right font-bold",
                (row.kind === "added" || row.kind === "changed") &&
                  "text-green-600 dark:text-green-400",
              )}
            >
              {row.right !== null ? "+" : " "}
            </span>
            <span className="whitespace-pre-wrap break-all flex-1">{row.right?.text ?? ""}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

export const DiffView = memo(DiffViewInner);
