"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from "react";
import { cn } from "@stigmer/theme";
import { FileChangeType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
import { ArtifactContentRenderer } from "../execution/ArtifactContentRenderer.js";
import { FileChangeDiff } from "../execution/FileChangesView.js";
import { useWorkspaceFileContent } from "./useWorkspaceFileContent.js";
/** Line-skeleton widths, mirroring `FileContentStateView` in `ArtifactPreviewModal`. */
const SKELETON_LINE_WIDTHS = [85, 72, 90, 65, 78, 88, 70, 82];
/**
 * Read-only viewer for a single workspace file.
 *
 * A thin shell (no new rendering): it resolves the open file, drives
 * {@link useWorkspaceFileContent}, and — for displayable text — delegates to
 * the shared {@link ArtifactContentRenderer} (markdown / YAML / JSON /
 * line-numbered text). Its state body deliberately mirrors
 * `FileContentStateView` in `ArtifactPreviewModal` (same skeleton, error, and
 * delegation), extended with the three states an execution artifact never
 * has — binary, too-large, and unsupported-substrate. The two are **not**
 * merged into one shared component: their surrounding chrome (an artifact's
 * copy/download/apply bar vs. this viewer's header) differs enough that a
 * shared shell would need context-flag props. Keep them as sibling shells that
 * share the one renderer.
 *
 * Framework-agnostic and themed via `--stgm-*` tokens (DD-004/DD-005): a
 * platform builder can mount it directly given a `reader` and a selection.
 *
 * @example
 * ```tsx
 * <FileViewer
 *   selectedFile={{ entryId: entry.id, path: "src/index.ts" }}
 *   entries={workspace.entries}
 *   reader={workspaceFileReader}
 *   onClose={() => setSelected(null)}
 * />
 * ```
 */
export const FileViewer = forwardRef(function FileViewer({ selectedFile, entries, reader, change, reveal, onClose, showHeader = true, className }, ref) {
    const entry = entries.find((e) => e.id === selectedFile.entryId) ?? null;
    const path = selectedFile.path;
    // The hook is called unconditionally — a missing entry passes `null` and the
    // hook stays idle, so hook order is stable across the not-found branch.
    const { content, isLoading, error, isUnsupported, refetch } = useWorkspaceFileContent({ entry, path, reader });
    useImperativeHandle(ref, () => ({ refresh: refetch }), [refetch]);
    // View mode is meaningful only for a changed file. A deleted file has no live
    // bytes to browse, and browsing needs a reader, so the live "File" view is
    // gated on both; otherwise a changed file shows the diff alone (no lone
    // control). The default is the authoritative diff (DD-06). The mount site
    // remounts the viewer per file (`key`), so this initializes correctly for
    // each opened file without deriving during render.
    const canBrowseLive = reader !== undefined && change?.changeType !== FileChangeType.DELETE;
    const showViewToggle = change !== undefined && canBrowseLive;
    // A reveal (jump-to-line) opens in File view even for a changed file — the
    // line number refers to current source, not the diff (DR-1). Absent a reveal,
    // a changed file still defaults to the authoritative diff (DD-06).
    const [viewMode, setViewMode] = useState(reveal ? "file" : change !== undefined ? "diff" : "file");
    // A new reveal on an already-mounted viewer (a second search hit in the same
    // file, so no remount) must return to File view to honor the line.
    useEffect(() => {
        if (reveal)
            setViewMode("file");
        // Keyed on the reveal nonce: only a fresh jump-to-line request re-forces
        // File view, so a manual switch to Diff afterward sticks.
    }, [reveal?.nonce]);
    const effectiveView = change
        ? canBrowseLive
            ? viewMode
            : "diff"
        : "file";
    const handleKeyDown = useCallback((e) => {
        if (e.key === "Escape" && onClose) {
            e.stopPropagation();
            onClose();
        }
    }, [onClose]);
    const lastSlash = path.lastIndexOf("/");
    const basename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
    const dir = lastSlash >= 0 ? path.slice(0, lastSlash) : "";
    // The owning entry is gone (removed while its file was open).
    if (!entry) {
        return (_jsxs("div", { role: "region", "aria-label": "File viewer", className: cn("flex h-full flex-col", className), onKeyDown: handleKeyDown, children: [showHeader && (_jsx(ViewerHeader, { basename: basename, dir: dir, onRefresh: undefined, onClose: onClose })), _jsx("div", { className: "flex flex-1 items-center justify-center p-8 text-center text-xs text-muted-foreground", children: "This file is no longer in the workspace." })] }));
    }
    return (_jsxs("div", { role: "region", "aria-label": "File viewer", 
        // Focusable (programmatically / on click) so the Escape-to-close handler
        // fires even when the read-only body holds no natively focusable element.
        tabIndex: -1, className: cn("flex h-full flex-col focus:outline-none", className), onKeyDown: handleKeyDown, children: [showHeader && (_jsx(ViewerHeader, { basename: basename, dir: dir, onRefresh: effectiveView === "file" && !isUnsupported ? refetch : undefined, onClose: onClose })), showViewToggle && (_jsx(ViewerModeToggle, { value: viewMode, onChange: setViewMode })), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: effectiveView === "diff" && change ? (_jsx("div", { className: "p-3", children: _jsx(FileChangeDiff, { change: change, showFileName: false, showStats: true }) })) : (_jsxs(_Fragment, { children: [change && (_jsx("p", { className: "border-b border-border-muted px-4 py-1.5 text-[0.65rem] text-muted-foreground", children: "Live \u2014 may differ from the reviewed change." })), _jsx(FileViewerBody, { basename: basename, content: content, isLoading: isLoading, isUnsupported: isUnsupported, error: error, onRetry: refetch, reveal: reveal })] })) })] }));
});
// ---------------------------------------------------------------------------
// View toggle — Diff vs live File
// ---------------------------------------------------------------------------
/**
 * Segmented control for a changed file's two views: the authoritative reviewed
 * **Diff** and the live **File**. A mutually-exclusive choice, so it follows
 * the platform's radiogroup segmented-control pattern (see `library/ScopeToggle`)
 * rather than a tablist — it must not nest a second tablist inside the
 * inspector's own tabs.
 */
function ViewerModeToggle({ value, onChange, }) {
    const options = [
        { value: "diff", label: "Diff" },
        { value: "file", label: "File" },
    ];
    // Selection follows focus, matching ActivityRail / SearchModeToggle.
    const buttonRefs = useRef([]);
    const selectValue = useCallback((next) => {
        onChange(next);
        buttonRefs.current[next === "diff" ? 0 : 1]?.focus();
    }, [onChange]);
    const handleKeyDown = useCallback((e) => {
        if (e.key === "ArrowRight" || e.key === "ArrowDown") {
            e.preventDefault();
            selectValue("file");
        }
        else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
            e.preventDefault();
            selectValue("diff");
        }
    }, [selectValue]);
    return (_jsx("div", { className: "border-b border-border px-3 py-1.5", children: _jsx("div", { role: "radiogroup", "aria-label": "File view", className: "inline-flex rounded-md bg-muted p-0.5", children: options.map((option, index) => {
                const isSelected = value === option.value;
                return (_jsx("button", { ref: (el) => {
                        buttonRefs.current[index] = el;
                    }, type: "button", role: "radio", "aria-checked": isSelected, tabIndex: isSelected ? 0 : -1, onClick: () => onChange(option.value), onKeyDown: handleKeyDown, className: cn("inline-flex cursor-pointer items-center rounded-sm px-2 py-0.5 text-xs font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isSelected
                        ? "bg-background text-foreground shadow-sm"
                        : "text-muted-foreground hover:text-foreground"), children: option.label }, option.value));
            }) }) }));
}
// ---------------------------------------------------------------------------
// Body — exactly one state, mirroring FileContentStateView + workspace extras
// ---------------------------------------------------------------------------
function FileViewerBody({ basename, content, isLoading, isUnsupported, error, onRetry, reveal, }) {
    // Unsupported comes first: a missing reader means there is nothing to load.
    if (isUnsupported) {
        return (_jsx(MessageState, { children: "Preview isn\u2019t available for this file here." }));
    }
    if (isLoading) {
        return (_jsx("div", { role: "status", className: "space-y-2 p-4", "aria-busy": "true", "aria-label": "Loading file", children: SKELETON_LINE_WIDTHS.map((width, i) => (_jsx("div", { className: "h-4 animate-pulse rounded bg-muted", style: { width: `${width}%` }, "aria-hidden": "true" }, i))) }));
    }
    if (error) {
        return (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 p-8 text-center", children: [_jsx(ErrorAlertIcon, {}), _jsx("p", { className: "text-sm text-destructive", children: error.message }), _jsx("button", { type: "button", onClick: onRetry, className: "text-xs font-medium text-foreground underline underline-offset-2 hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm", children: "Retry" })] }));
    }
    if (!content) {
        // Not loading, not unsupported, no error, yet no content — defensive.
        return _jsx(MessageState, { children: "Content not available for preview." });
    }
    if (content.isBinary) {
        return (_jsxs(MessageState, { children: ["Binary file \u2014 not shown", content.size ? ` (${formatBytes(content.size)})` : "", "."] }));
    }
    if (content.text === null && content.truncated) {
        return (_jsxs(MessageState, { children: ["This file is too large to preview", content.size ? ` (${formatBytes(content.size)})` : "", "."] }));
    }
    if (content.text === null) {
        return _jsx(MessageState, { children: "This file can\u2019t be displayed as text." });
    }
    return (_jsx(ArtifactContentRenderer, { content: content.text, fileName: basename, isTruncated: content.truncated, reveal: reveal }));
}
// ---------------------------------------------------------------------------
// Header
// ---------------------------------------------------------------------------
function ViewerHeader({ basename, dir, onRefresh, onClose, }) {
    return (_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3 py-2", children: [_jsx(FileIcon, {}), _jsxs("div", { className: "flex min-w-0 flex-1 items-baseline gap-1.5", children: [_jsx("span", { className: "truncate text-xs font-semibold text-foreground", title: dir ? `${dir}/${basename}` : basename, children: basename }), dir && (_jsx("span", { className: "truncate text-[0.65rem] text-muted-foreground", children: dir }))] }), onRefresh && (_jsx("button", { type: "button", onClick: onRefresh, "aria-label": `Reload ${basename}`, className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", children: _jsx(RefreshIcon, {}) })), onClose && (_jsx("button", { type: "button", onClick: onClose, "aria-label": "Close file viewer", className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", children: _jsx(CloseIcon, {}) }))] }));
}
function MessageState({ children }) {
    return (_jsx("div", { className: "flex h-full items-center justify-center p-8 text-center text-xs text-muted-foreground", children: children }));
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Human-readable byte size for the too-large / binary states. */
function formatBytes(n) {
    if (n < 1024)
        return `${n} B`;
    if (n < 1024 * 1024)
        return `${(n / 1024).toFixed(1)} KB`;
    return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
// ---------------------------------------------------------------------------
// Inline SVG icons (SDK independence — no lucide dependency)
// ---------------------------------------------------------------------------
function FileIcon() {
    return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: [_jsx("path", { d: "M9.5 1.5H5C4.17 1.5 3.5 2.17 3.5 3V13C3.5 13.83 4.17 14.5 5 14.5H11C11.83 14.5 12.5 13.83 12.5 13V4.5L9.5 1.5Z" }), _jsx("path", { d: "M9.5 1.5V4.5H12.5" })] }));
}
function CloseIcon() {
    return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M3 3L11 11" }), _jsx("path", { d: "M11 3L3 11" })] }));
}
function RefreshIcon() {
    return (_jsxs("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M1.5 6a4.5 4.5 0 017.794-3.073M10.5 6a4.5 4.5 0 01-7.794 3.073" }), _jsx("path", { d: "M9.5 1v2.5H7M2.5 11V8.5H5" })] }));
}
function ErrorAlertIcon() {
    return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "text-destructive", "aria-hidden": "true", children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5.5V8.5" }), _jsx("circle", { cx: "8", cy: "11", r: "0.5", fill: "currentColor", stroke: "none" })] }));
}
//# sourceMappingURL=FileViewer.js.map