/**
 * @license
 * Copyright 2026 HopCode Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { useState, useEffect, useRef, useCallback } from 'react';
import { exec } from 'child_process';
import { createDebugLogger } from '@hoptrendy/hopcode-core';
import { useSettings } from '../contexts/SettingsContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
const debugLog = createDebugLogger('STATUS_LINE');
// Footer's bottom row (hint/mode indicator) occupies 1 line, so the status
// line gets at most 2 to keep the total footer height at 3 rows max.
export const MAX_STATUS_LINES = 2;
function getStatusLineConfig(settings) {
    const raw = settings.merged.ui?.statusLine;
    if (raw &&
        typeof raw === 'object' &&
        'type' in raw &&
        raw.type === 'command' &&
        'command' in raw &&
        typeof raw.command === 'string' &&
        raw.command.trim().length > 0) {
        const config = {
            type: 'command',
            command: raw.command,
        };
        if (typeof raw.refreshInterval === 'number' &&
            Number.isFinite(raw.refreshInterval) &&
            raw.refreshInterval >= 1) {
            config.refreshInterval = raw.refreshInterval;
        }
        return config;
    }
    return undefined;
}
function buildMetricsPayload(m) {
    const models = {};
    for (const [id, mm] of Object.entries(m.models)) {
        models[id] = {
            api: {
                total_requests: mm.api.totalRequests,
                total_errors: mm.api.totalErrors,
                total_latency_ms: mm.api.totalLatencyMs,
            },
            tokens: {
                prompt: mm.tokens.prompt,
                completion: mm.tokens.candidates,
                total: mm.tokens.total,
                cached: mm.tokens.cached,
                thoughts: mm.tokens.thoughts,
            },
        };
    }
    return {
        models,
        files: {
            total_lines_added: m.files.totalLinesAdded,
            total_lines_removed: m.files.totalLinesRemoved,
        },
    };
}
/**
 * Hook that executes a user-configured shell command and returns its output
 * for display in the status line. The command receives structured JSON context
 * via stdin.
 *
 * Updates are debounced (300ms) and triggered by state changes (model switch,
 * new messages, vim mode toggle) rather than blind polling. When the config
 * sets `refreshInterval` (seconds, >= 1), the command is additionally re-run
 * on a timer so external data (git branch, quota, clock) stays fresh even
 * when no Agent state has changed.
 */
export function useStatusLine() {
    const settings = useSettings();
    const uiState = useUIState();
    const config = useConfig();
    const { vimEnabled, vimMode } = useVimMode();
    const statusLineConfig = getStatusLineConfig(settings);
    const statusLineCommand = statusLineConfig?.command;
    const refreshInterval = statusLineConfig?.refreshInterval;
    const [output, setOutput] = useState([]);
    // Keep latest values in refs so the stable doUpdate callback can read them
    // without being recreated on every render.
    const uiStateRef = useRef(uiState);
    uiStateRef.current = uiState;
    const configRef = useRef(config);
    configRef.current = config;
    const vimEnabledRef = useRef(vimEnabled);
    vimEnabledRef.current = vimEnabled;
    const vimModeRef = useRef(vimMode);
    vimModeRef.current = vimMode;
    const statusLineCommandRef = useRef(statusLineCommand);
    statusLineCommandRef.current = statusLineCommand;
    const debounceTimerRef = useRef(undefined);
    // Track previous trigger values to detect actual changes.
    // Initialized with current values so the state-change effect
    // does not fire redundantly on mount.
    const { lastPromptTokenCount } = uiState.sessionStats;
    const { currentModel, branchName } = uiState;
    const totalToolCalls = uiState.sessionStats.metrics.tools.totalCalls;
    const totalLinesAdded = uiState.sessionStats.metrics.files.totalLinesAdded;
    const totalLinesRemoved = uiState.sessionStats.metrics.files.totalLinesRemoved;
    const effectiveVim = vimEnabled ? vimMode : undefined;
    const prevStateRef = useRef({
        promptTokenCount: lastPromptTokenCount,
        currentModel,
        effectiveVim,
        branchName,
        totalToolCalls,
        totalLinesAdded,
        totalLinesRemoved,
    });
    // Guard: when true, the mount effect has already called doUpdate so the
    // command-change effect should skip its first run to avoid a double exec.
    const hasMountedRef = useRef(false);
    // Track the active child process so we can kill it on new updates / unmount.
    const activeChildRef = useRef(undefined);
    const generationRef = useRef(0);
    const doUpdate = useCallback(() => {
        const cmd = statusLineCommandRef.current;
        if (!cmd) {
            setOutput([]);
            return;
        }
        const ui = uiStateRef.current;
        const cfg = configRef.current;
        const stats = ui.sessionStats;
        const m = stats.metrics;
        const contextWindowSize = cfg.getContentGeneratorConfig()?.contextWindowSize || 0;
        const usedPercentage = contextWindowSize > 0
            ? Math.min(100, Math.max(0, Math.round((stats.lastPromptTokenCount / contextWindowSize) * 1000) / 10))
            : 0;
        let totalInputTokens = 0;
        let totalOutputTokens = 0;
        for (const mm of Object.values(m.models)) {
            totalInputTokens += mm.tokens.prompt;
            totalOutputTokens += mm.tokens.candidates;
        }
        const input = {
            session_id: stats.sessionId,
            version: cfg.getCliVersion() || 'unknown',
            model: {
                display_name: ui.currentModel || cfg.getModel() || 'unknown',
            },
            context_window: {
                context_window_size: contextWindowSize,
                used_percentage: usedPercentage,
                remaining_percentage: Math.round((100 - usedPercentage) * 10) / 10,
                current_usage: stats.lastPromptTokenCount,
                total_input_tokens: totalInputTokens,
                total_output_tokens: totalOutputTokens,
            },
            workspace: {
                current_dir: cfg.getTargetDir(),
            },
            ...(ui.branchName && {
                git: {
                    branch: ui.branchName,
                },
            }),
            metrics: buildMetricsPayload(m),
            ...(vimEnabledRef.current && {
                vim: { mode: vimModeRef.current },
            }),
        };
        // Kill the previous child process if still running.
        if (activeChildRef.current) {
            activeChildRef.current.kill();
            activeChildRef.current = undefined;
        }
        // Bump generation so earlier in-flight callbacks are ignored.
        const gen = ++generationRef.current;
        // exec() can throw synchronously: libuv reports a handful of spawn
        // errors (EACCES, ENOENT, …) via the async 'error' event, but anything
        // else — including EBADF, reported on macOS Node 22 in issue #3264 — is
        // thrown from ChildProcess.spawn. Without this guard the throw escapes
        // the setTimeout callback and crashes the CLI as uncaughtException.
        let child;
        try {
            child = exec(cmd, { cwd: cfg.getTargetDir(), timeout: 5000, maxBuffer: 1024 * 10 }, (error, stdout) => {
                if (gen !== generationRef.current)
                    return; // stale
                activeChildRef.current = undefined;
                const nextLines = !error && stdout
                    ? stdout
                        .replace(/\r?\n$/, '')
                        .split(/\r?\n/)
                        .filter(Boolean)
                        .slice(0, MAX_STATUS_LINES)
                    : [];
                // Skip the state update if the output is unchanged — avoids a
                // Footer re-render each periodic tick, which cuts wasted work
                // and reduces the window for Ink to miscount rows in narrow
                // terminals when `refreshInterval` runs at 1s (see #3383).
                setOutput((prev) => {
                    if (prev.length === nextLines.length &&
                        prev.every((v, i) => v === nextLines[i])) {
                        return prev;
                    }
                    return nextLines;
                });
            });
        }
        catch (err) {
            debugLog.error('statusline exec error:', err.message);
            setOutput([]);
            return;
        }
        activeChildRef.current = child;
        // Pass structured JSON context via stdin.
        // Guard against EPIPE if the child exits before we finish writing.
        if (child.stdin) {
            child.stdin.on('error', (err) => {
                if (err.code !== 'EPIPE') {
                    debugLog.error('statusline stdin error:', err.message);
                }
            });
            child.stdin.write(JSON.stringify(input));
            child.stdin.end();
        }
    }, []); // No deps — reads everything from refs
    const scheduleUpdate = useCallback(() => {
        if (debounceTimerRef.current !== undefined) {
            clearTimeout(debounceTimerRef.current);
        }
        debounceTimerRef.current = setTimeout(() => {
            debounceTimerRef.current = undefined;
            doUpdate();
        }, 300);
    }, [doUpdate]);
    // Trigger update when meaningful state changes
    useEffect(() => {
        if (!statusLineCommand) {
            // Command removed — kill any in-flight process and discard callbacks.
            activeChildRef.current?.kill();
            activeChildRef.current = undefined;
            generationRef.current++;
            if (debounceTimerRef.current !== undefined) {
                clearTimeout(debounceTimerRef.current);
                debounceTimerRef.current = undefined;
            }
            setOutput([]);
            return;
        }
        const prev = prevStateRef.current;
        if (lastPromptTokenCount !== prev.promptTokenCount ||
            currentModel !== prev.currentModel ||
            effectiveVim !== prev.effectiveVim ||
            branchName !== prev.branchName ||
            totalToolCalls !== prev.totalToolCalls ||
            totalLinesAdded !== prev.totalLinesAdded ||
            totalLinesRemoved !== prev.totalLinesRemoved) {
            prev.promptTokenCount = lastPromptTokenCount;
            prev.currentModel = currentModel;
            prev.effectiveVim = effectiveVim;
            prev.branchName = branchName;
            prev.totalToolCalls = totalToolCalls;
            prev.totalLinesAdded = totalLinesAdded;
            prev.totalLinesRemoved = totalLinesRemoved;
            scheduleUpdate();
        }
    }, [
        statusLineCommand,
        lastPromptTokenCount,
        currentModel,
        effectiveVim,
        branchName,
        totalToolCalls,
        totalLinesAdded,
        totalLinesRemoved,
        scheduleUpdate,
    ]);
    // Re-execute immediately when the command itself changes (hot reload).
    // Skip the first run — the mount effect below already handles it.
    useEffect(() => {
        if (!hasMountedRef.current)
            return;
        if (statusLineCommand) {
            // Clear any pending debounce so we don't get a redundant second run.
            if (debounceTimerRef.current !== undefined) {
                clearTimeout(debounceTimerRef.current);
                debounceTimerRef.current = undefined;
            }
            doUpdate();
        }
        // Cleanup when command is removed is handled by the state-change effect.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [statusLineCommand]);
    // Periodic refresh — re-run the command every `refreshInterval` seconds.
    // The tick yields if a previous exec is still running: unlike state-change
    // triggers (which legitimately need to preempt stale data), the periodic
    // tick exists only to keep external data fresh, so killing an in-flight
    // child would starve commands that run longer than `refreshInterval` and
    // the statusline would never update. The 5s exec timeout still caps the
    // wait, and state-change triggers still go through `doUpdate` directly.
    useEffect(() => {
        if (!statusLineCommand || !refreshInterval)
            return;
        const timer = setInterval(() => {
            if (activeChildRef.current)
                return;
            doUpdate();
        }, refreshInterval * 1000);
        return () => {
            clearInterval(timer);
        };
    }, [statusLineCommand, refreshInterval, doUpdate]);
    // Initial execution + cleanup
    useEffect(() => {
        hasMountedRef.current = true;
        const genRef = generationRef;
        const debounceRef = debounceTimerRef;
        const childRef = activeChildRef;
        doUpdate();
        return () => {
            // Kill active child process and invalidate callbacks
            childRef.current?.kill();
            childRef.current = undefined;
            genRef.current++;
            if (debounceRef.current !== undefined) {
                clearTimeout(debounceRef.current);
                debounceRef.current = undefined;
            }
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    return { lines: output };
}
//# sourceMappingURL=useStatusLine.js.map