import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
 * ChangedFilesView — Read-only "press D to see diffs" review pane shown
 * on the success outro.
 *
 * Two states:
 *   - list view    — file names with create/modify badges; arrow keys
 *                    move the cursor, Enter opens the diff for the
 *                    selected file
 *   - detail view  — `git diff --no-color HEAD -- <path>` output for one
 *                    file, scrollable line-by-line
 *
 * Esc navigates up: detail → list, list → close (handled by parent).
 *
 * Diffs are produced with `child_process.execFileSync` (NO shell) and
 * truncated at 10k chars to keep the render cheap on huge files. When
 * git is unavailable or the file isn't tracked, we surface a friendly
 * "(no git diff available — file may be new)" placeholder so the user
 * still sees the file existed without a confusing crash.
 *
 * Goal: users gain trust that the wizard did what it claimed without
 * leaving the terminal. Keep it simple — no syntax highlighting, no
 * cross-commit diffs, no editing. Strictly a review surface.
 */
import { Box, Text } from 'ink';
import { useMemo, useState } from 'react';
import { execFileSync } from 'node:child_process';
import { Colors, Icons } from '../styles.js';
import { useScreenInput } from '../hooks/useScreenInput.js';
import { useStdoutDimensions } from '../hooks/useStdoutDimensions.js';
import { analytics } from '../../../utils/analytics.js';
/** Hard cap on diff bytes shown inline. Anything larger is truncated. */
export const MAX_DIFF_CHARS = 10_000;
/** Rows consumed by chrome (header + footer + spacing). */
const CHROME_ROWS = 8;
/**
 * Build a sorted, de-duplicated `ChangedFile[]` from the registry's raw
 * `files.written` / `files.modified` arrays. `written` wins on conflict
 * so a path that appears in both lists is shown as a 'create'.
 *
 * Pure for unit testing.
 */
export function buildChangedFileList(written, modified) {
    const writtenSet = new Set(written);
    const out = [];
    for (const path of written)
        out.push({ path, kind: 'create' });
    for (const path of modified) {
        if (!writtenSet.has(path))
            out.push({ path, kind: 'modify' });
    }
    out.sort((a, b) => a.path.localeCompare(b.path));
    return out;
}
/**
 * Run `git diff --no-color HEAD -- <path>` synchronously and return the
 * output (or a placeholder string when git fails). Truncates to
 * `MAX_DIFF_CHARS` and appends a footer when truncation happens.
 *
 * Exported for test injection — the component calls this lazily on demand.
 */
export function readGitDiff(cwd, path) {
    try {
        const raw = execFileSync('git', ['diff', '--no-color', 'HEAD', '--', path], {
            cwd,
            encoding: 'utf-8',
            // Cap stdout so a runaway diff can't OOM the wizard. Anything
            // beyond the cap is dropped by Node and we surface our own
            // truncation footer below.
            maxBuffer: MAX_DIFF_CHARS * 2,
            stdio: ['ignore', 'pipe', 'ignore'],
        });
        if (!raw || raw.trim().length === 0) {
            return '(no git diff available — file may be new)';
        }
        if (raw.length > MAX_DIFF_CHARS) {
            return (raw.slice(0, MAX_DIFF_CHARS) + '\n\n(diff truncated, see file directly)');
        }
        return raw;
    }
    catch {
        return '(no git diff available — file may be new)';
    }
}
export const ChangedFilesView = ({ files, cwd, onClose, }) => {
    const [, rows] = useStdoutDimensions();
    const visibleLines = Math.max(5, rows - CHROME_ROWS);
    const [cursor, setCursor] = useState(0);
    const [openIndex, setOpenIndex] = useState(null);
    const [diffOffset, setDiffOffset] = useState(0);
    const open = openIndex !== null ? files[openIndex] : null;
    // Compute the diff lazily once per opened file. Memoizing on the file
    // PATH (a stable string) keeps arrow-key navigation in the diff pane
    // O(1) — we don't want to fork git on every keypress. Earlier the dep
    // was `open`, but `files` is recomputed in the parent's render body so
    // `files[openIndex]` returns a fresh `ChangedFile` reference each render
    // — that defeated the memo and re-shelled `git diff` on every j/k.
    // Path strings are referentially stable, so `[open?.path, cwd]` is the
    // correct dep set even if a new array is passed each render.
    const openPath = open?.path ?? null;
    const diffLines = useMemo(() => {
        if (!openPath)
            return [];
        return readGitDiff(cwd, openPath).split('\n');
    }, [openPath, cwd]);
    const maxOffset = Math.max(0, diffLines.length - visibleLines);
    useScreenInput((input, key) => {
        // Detail view ─────────────────────────────────────────────────────
        if (open) {
            if (key.escape) {
                setOpenIndex(null);
                setDiffOffset(0);
                return;
            }
            if (key.upArrow || input === 'k') {
                setDiffOffset((o) => Math.max(0, o - 1));
                return;
            }
            if (key.downArrow || input === 'j') {
                setDiffOffset((o) => Math.min(maxOffset, o + 1));
                return;
            }
            if (key.pageUp) {
                setDiffOffset((o) => Math.max(0, o - visibleLines));
                return;
            }
            if (key.pageDown) {
                setDiffOffset((o) => Math.min(maxOffset, o + visibleLines));
                return;
            }
            return;
        }
        // List view ───────────────────────────────────────────────────────
        if (key.escape) {
            onClose();
            return;
        }
        if (key.upArrow || input === 'k') {
            setCursor((c) => Math.max(0, c - 1));
            return;
        }
        if (key.downArrow || input === 'j') {
            setCursor((c) => Math.min(files.length - 1, c + 1));
            return;
        }
        if (key.return || input === 'd' || input === 'D') {
            if (files.length === 0)
                return;
            const idx = Math.max(0, Math.min(files.length - 1, cursor));
            analytics.wizardCapture('view changes file selected', {
                'file kind': files[idx].kind,
                'file index': idx,
                'file count': files.length,
            });
            setOpenIndex(idx);
            setDiffOffset(0);
        }
    });
    // ── Empty state ─────────────────────────────────────────────────────
    if (files.length === 0) {
        return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: Colors.accent, children: "Files changed" }), _jsx(Text, { color: Colors.muted, children: " (Esc to go back)" })] }), _jsx(Text, { color: Colors.muted, children: "No file changes were tracked for this run." })] }));
    }
    // ── Detail view ─────────────────────────────────────────────────────
    if (open) {
        const visible = diffLines.slice(diffOffset, diffOffset + visibleLines);
        return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: Colors.accent, children: [Icons.diamond, " ", open.path] }), _jsx(Text, { color: Colors.muted, children: "Esc to go back \u00B7 \u2191\u2193/jk to scroll \u00B7 PgUp/PgDn to page" })] }), _jsx(Box, { flexDirection: "column", height: visibleLines, children: visible.map((line, i) => (_jsx(Text, { color: diffLineColor(line), wrap: "truncate", children: line || ' ' }, diffOffset + i))) }), diffLines.length > visibleLines && (_jsxs(Text, { color: Colors.muted, children: [Icons.dot, " ", diffOffset + visible.length, "/", diffLines.length, " lines"] }))] }));
    }
    // ── List view ───────────────────────────────────────────────────────
    return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: Colors.accent, children: "Files changed" }), _jsxs(Text, { color: Colors.muted, children: [files.length, " file", files.length === 1 ? '' : 's', " \u00B7 \u2191\u2193 to move \u00B7 Enter to view diff \u00B7 Esc to go back"] })] }), _jsx(Box, { flexDirection: "column", children: files.map((file, i) => {
                    const isSelected = i === cursor;
                    return (_jsxs(Text, { color: isSelected ? Colors.heading : Colors.body, wrap: "truncate", children: [_jsxs(Text, { color: isSelected ? Colors.accent : Colors.subtle, children: [isSelected ? Icons.chevronRight : ' ', ' '] }), _jsx(Text, { color: file.kind === 'create'
                                    ? Colors.success
                                    : Colors.accentSecondary, bold: true, children: file.kind === 'create' ? '＋ new   ' : '~ modify ' }), file.path] }, file.path));
                }) })] }));
};
/**
 * Map a unified-diff line prefix to a semantic color. Pure helper so
 * the component body stays focused on layout. Exposed as a non-default
 * export only via the component module.
 */
function diffLineColor(line) {
    if (line.startsWith('+++') || line.startsWith('---'))
        return Colors.muted;
    if (line.startsWith('@@'))
        return Colors.accentSecondary;
    if (line.startsWith('+'))
        return Colors.success;
    if (line.startsWith('-'))
        return Colors.error;
    if (line.startsWith('diff ') || line.startsWith('index '))
        return Colors.muted;
    return Colors.body;
}
