"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useMemo, useState } from "react";
import { FileChangeCaptureLevel, FileChangeType, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
import { cn } from "@stigmer/theme";
import { BoundedContent } from "../internal/BoundedContent.js";
import { computeDiff } from "../version-history/computeDiff.js";
import { DiffViewer } from "../version-history/DiffViewer.js";
import { DiffFileList } from "../version-history/DiffFileList.js";
import { DiffSummary } from "../version-history/DiffSummary.js";
import { UnifiedDiffView } from "../version-history/UnifiedDiffView.js";
import { FilePathLink } from "./FilePathLink.js";
import { EmptyChangeNotice } from "./EmptyChangeNotice.js";
import { useFileChangeContent } from "./useFileChangeContent.js";
/**
 * Consolidated, GitHub-style view of the files an agent changed: a summary
 * bar, a file selector (when more than one file changed), and the unified diff
 * for the selected file.
 *
 * Renders the runner's authoritative {@link FileChange} captures directly
 * (proto-first). Whole-file captures (native) become a true before/after diff;
 * hunk-only captures (Cursor today) render their unified diff; offloaded bodies
 * are fetched lazily only when their file is opened (see {@link FileChangeDiff}).
 *
 * Zero Console dependencies — safe for platform builder embedding.
 *
 * @example
 * ```tsx
 * const { fileChanges } = useSessionFileChanges(allExecutions);
 * <FileChangesView changes={fileChanges} />
 * ```
 */
export function FileChangesView({ changes, className }) {
    const entries = useMemo(() => changes.map(toFileDiffEntry), [changes]);
    const [selectedPath, setSelectedPath] = useState(changes[0]?.path ?? null);
    const selected = changes.find((c) => c.path === selectedPath) ?? changes[0] ?? null;
    const totals = useMemo(() => entries.reduce((acc, e) => ({
        additions: acc.additions + e.additions,
        deletions: acc.deletions + e.deletions,
    }), { additions: 0, deletions: 0 }), [entries]);
    if (changes.length === 0 || !selected)
        return null;
    return (_jsxs("div", { className: cn("flex flex-col gap-3", className), children: [_jsx(DiffSummary, { fileCount: changes.length, additions: totals.additions, deletions: totals.deletions }), changes.length > 1 && (_jsx("div", { className: "overflow-hidden rounded-lg border border-border", children: _jsx(DiffFileList, { files: entries, selectedPath: selected.path, onSelect: setSelectedPath }) })), _jsx(FileChangeDiff, { change: selected }, selected.path)] }));
}
/**
 * Renders the diff for a single {@link FileChange}, adapting to how completely
 * the runner captured it:
 *
 * - **Whole-file** (native): a true before/after diff via {@link DiffViewer}.
 *   Offloaded bodies are fetched lazily; loading, error, and too-large
 *   (server-truncated) states are surfaced honestly.
 * - **Hunk-only** (Cursor today): the captured unified diff, labeled as such.
 * - **Binary**: a "binary file changed" notice instead of a text diff.
 *
 * Importable on its own by platform builders composing custom change UIs.
 */
export function FileChangeDiff({ change, className, bodyClassName, bounded = false, showFileName = true, showStats = true, }) {
    const { beforeText, afterText, isBinary, isLoading, error, isTruncated, downloadUrl } = useFileChangeContent(change);
    const hunks = useMemo(() => {
        if (beforeText === null || afterText === null)
            return [];
        return computeDiff(beforeText, afterText);
    }, [beforeText, afterText]);
    const stats = useMemo(() => {
        if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
            return { additions: change.linesAdded, deletions: change.linesRemoved };
        }
        // Whole-file: count the rendered hunks (what the user actually sees). When
        // there are none to count — the body is offloaded/loading/truncated — fall
        // back to the capture-time counts, which are computed with the same diff
        // algorithm and so agree with the hunks whenever both exist.
        const computed = countHunks(hunks);
        if (computed.additions > 0 || computed.deletions > 0)
            return computed;
        return { additions: change.linesAdded, deletions: change.linesRemoved };
    }, [change.captureLevel, change.linesAdded, change.linesRemoved, hunks]);
    const showStatsRow = showStats && (stats.additions > 0 || stats.deletions > 0);
    return (_jsxs("div", { className: cn("flex flex-col gap-1.5", className), "data-cursor-target": "file-diff", children: [(showFileName || showStatsRow) && (_jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [showFileName &&
                        change.changeType === FileChangeType.RENAME &&
                        change.renameFrom && (_jsxs("span", { className: "min-w-0 truncate font-mono text-muted-foreground-faint", children: [change.renameFrom, " \u2192"] })), showFileName && (_jsx(FilePathLink, { path: change.path, dirDisplay: "dim", className: "text-xs" })), showStatsRow && (_jsxs("span", { className: "shrink-0 tabular-nums", children: [_jsxs("span", { className: "text-diff-added-fg", children: ["+", stats.additions] }), " ", _jsxs("span", { className: "text-diff-removed-fg", children: ["-", stats.deletions] })] }))] })), _jsx(FileChangeBody, { change: change, hunks: hunks, isBinary: isBinary, isLoading: isLoading, error: error, isTruncated: isTruncated, downloadUrl: downloadUrl, bodyClassName: bodyClassName, bounded: bounded })] }));
}
// ---------------------------------------------------------------------------
// Body — the capture-level / fetch-state switch
// ---------------------------------------------------------------------------
function FileChangeBody({ change, hunks, isBinary, isLoading, error, isTruncated, downloadUrl, bodyClassName, bounded, }) {
    // A CREATE with no renderable diff is a genuinely empty new file; anything
    // else with no content degrades to the non-committal "no preview" (we never
    // claim emptiness we cannot prove from the capture).
    const emptyKind = change.changeType === FileChangeType.CREATE ? "empty-create" : "no-preview";
    // When bounded, the actual diff renderers (and only those — short notices need
    // no clamp) are wrapped in the shared BoundedContent budget, so the gate's
    // diff matches the timeline edit/write diff and never crowds out the buttons.
    const bound = (node) => bounded ? (_jsx(BoundedContent, { cursorTarget: "file-diff-expand", children: node })) : (node);
    if (isBinary) {
        return _jsx(Notice, { children: "Binary file changed." });
    }
    if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
        return change.unifiedDiff ? (bound(_jsx(UnifiedDiffView, { patch: change.unifiedDiff, className: bodyClassName }))) : (_jsx(EmptyChangeNotice, { kind: emptyKind, className: bodyClassName }));
    }
    // Whole-file: content may be inline (ready) or offloaded (fetched lazily).
    if (isTruncated) {
        return (_jsxs(Notice, { children: ["This file is too large to diff inline.", " ", downloadUrl && (_jsx("a", { href: downloadUrl, target: "_blank", rel: "noreferrer", className: "font-medium text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: "Download the full file" }))] }));
    }
    if (isLoading) {
        return _jsx(Notice, { children: "Loading diff\u2026" });
    }
    if (error) {
        return _jsx(Notice, { variant: "error", children: "Could not load this file's contents." });
    }
    if (hunks.length === 0) {
        return _jsx(EmptyChangeNotice, { kind: emptyKind, className: bodyClassName });
    }
    return bound(_jsx(DiffViewer, { hunks: hunks, className: bodyClassName }));
}
function Notice({ children, variant = "muted", }) {
    return (_jsx("div", { role: "status", className: cn("rounded-md border border-border bg-muted-subtle px-3 py-2 text-xs", variant === "error" ? "text-destructive" : "text-muted-foreground"), children: children }));
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function countHunks(hunks) {
    let additions = 0;
    let deletions = 0;
    for (const hunk of hunks) {
        for (const line of hunk.lines) {
            if (line.type === "added")
                additions++;
            else if (line.type === "removed")
                deletions++;
        }
    }
    return { additions, deletions };
}
/**
 * Projects a {@link FileChange} into a {@link FileDiffEntry} for the file list.
 *
 * Counts: hunk-only uses the runner's authoritative numbers; whole-file inline
 * is derived from the content (identical to the runner's capture-time counts by
 * construction — same diff algorithm). A whole-file side with no synchronous
 * content (offloaded / binary) falls back to the runner's capture-time
 * `linesAdded`/`linesRemoved` — zero when no count exists, which the list
 * renders as no stat.
 */
function toFileDiffEntry(change) {
    const changeType = change.changeType === FileChangeType.CREATE
        ? "added"
        : change.changeType === FileChangeType.DELETE
            ? "removed"
            : "modified";
    if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
        return {
            path: change.path,
            changeType,
            additions: change.linesAdded,
            deletions: change.linesRemoved,
        };
    }
    const before = inlineSide(change.before);
    const after = inlineSide(change.after);
    if (before !== null && after !== null) {
        const { additions, deletions } = countHunks(computeDiff(before, after));
        return { path: change.path, changeType, additions, deletions };
    }
    return {
        path: change.path,
        changeType,
        additions: change.linesAdded,
        deletions: change.linesRemoved,
    };
}
/** Inline text of a side, or null when absent/offloaded/binary. */
function inlineSide(side) {
    if (!side)
        return "";
    if (side.isBinary)
        return null;
    return side.body.case === "inline" ? side.body.value : null;
}
//# sourceMappingURL=FileChangesView.js.map