/**
 * OMK Chat Cockpit — shared helpers, types, and resource collection.
 */
import { readFile, readdir } from "fs/promises";
import { execFile } from "child_process";
import { promisify } from "util";
import { join } from "path";
import { getProjectRootAsync, getUserHome, pathExists, getRunPath, } from "../../util/fs.js";
import { sanitizeTerminalText } from "../../util/theme.js";
import { checkDeepSeekBalance } from "../../providers/deepseek/deepseek-balance.js";
import { getDeepSeekProviderStatus, resolveDeepSeekApiKey, } from "../../providers/deepseek/deepseek-config.js";
import { loadMergedMcpConfig } from "../../orchestration/routing/mcp-config.js";
import { parseGitStatusPorcelain } from "../hud.js";
const execFileAsync = promisify(execFile);
export function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)),
    ]);
}
// ── Constants ──
export const PANEL_HORIZONTAL_OVERHEAD = 4;
export const MIN_COCKPIT_FRAME_WIDTH = 20;
export const MAX_COCKPIT_FRAME_WIDTH = 180;
// ── Local helpers ──
export function truncateText(value, maxLength) {
    const clean = sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
    if (maxLength <= 0)
        return "";
    const chars = [...clean];
    if (chars.length <= maxLength)
        return clean;
    return `${chars.slice(0, Math.max(1, maxLength - 1)).join("")}…`;
}
export function statusRank(statusValue) {
    const normalized = statusValue.toLowerCase();
    switch (normalized) {
        case "running":
        case "in_progress": return 0;
        case "pending": return 1;
        case "blocked": return 2;
        case "failed": return 3;
        case "skipped": return 4;
        case "done":
        case "completed": return 5;
        default: return 6;
    }
}
export function normalizeRefreshMs(refreshMs) {
    if (refreshMs === undefined)
        return 2_000;
    if (!Number.isFinite(refreshMs))
        return 2_000;
    return Math.min(60_000, Math.max(250, Math.round(refreshMs)));
}
export function formatElapsed(ms) {
    if (!Number.isFinite(ms) || ms < 0)
        return "--";
    const totalSeconds = Math.max(0, Math.round(ms / 1000));
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    if (hours > 0)
        return `${hours}h${minutes}m`;
    if (minutes > 0)
        return `${minutes}m${seconds}s`;
    return `${seconds}s`;
}
export function gitStatusPriority(changeStatus) {
    const normalized = changeStatus.replace(/\s/g, "");
    if (normalized === "??")
        return 3;
    if (normalized.includes("D"))
        return 2;
    if (normalized.includes("A"))
        return 1;
    if (normalized.includes("R"))
        return 4;
    return 0;
}
export async function getGitChanges(root) {
    try {
        const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--untracked-files=normal"], { cwd: root, timeout: 5000, maxBuffer: 1024 * 1024, encoding: "utf-8" });
        return parseGitStatusPorcelain(String(stdout));
    }
    catch {
        return null;
    }
}
// ── Cache helpers ──
export function getCacheEntry(entry, ttlMs, now) {
    if (!entry)
        return undefined;
    if (now - entry.ts > ttlMs)
        return undefined;
    return entry.value;
}
// ── Resource helpers ──
export async function getCockpitResources(root, runId) {
    const resolvedRoot = root ?? await getProjectRootAsync();
    const harness = runId ? await readHarnessResources(runId).catch(() => null) : null;
    if (harness) {
        return applyMcpStatus(runId ?? null, harness);
    }
    const [mcp, skills, hooks] = await Promise.all([
        loadMergedMcpConfig(resolvedRoot, "all").catch(() => ({ servers: {}, sources: new Map() })),
        collectSkillEntries(resolvedRoot),
        collectHookEntries(resolvedRoot),
    ]);
    return {
        scope: "all",
        mcpServers: Object.keys(mcp.servers)
            .map((name) => ({ name, source: mcp.sources.get(name) ?? "project", status: "connected" }))
            .sort(compareResourceEntries),
        skills,
        hooks,
        checkedAt: Date.now(),
    };
}
export function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function readHarnessResources(runId) {
    const raw = await readFile(getRunPath(runId, "chat-agent-harness.json"), "utf-8");
    const parsed = JSON.parse(raw);
    if (!isRecord(parsed))
        return null;
    const resources = isRecord(parsed.resources) ? parsed.resources : null;
    const active = resources && isRecord(resources.active) ? resources.active : null;
    if (!active)
        return null;
    return {
        scope: "run",
        mcpServers: normalizeResourceEntries(active.mcp, "run"),
        skills: normalizeResourceEntries(active.skills, "run"),
        hooks: normalizeResourceEntries(active.hooks, "run"),
        checkedAt: Date.now(),
    };
}
function normalizeResourceEntries(value, source) {
    if (!Array.isArray(value))
        return [];
    return value
        .map((entry) => {
        if (typeof entry === "string") {
            return { name: entry, source, status: source === "run" ? "connected" : undefined };
        }
        if (!isRecord(entry))
            return null;
        const name = typeof entry.name === "string" ? entry.name : typeof entry.id === "string" ? entry.id : null;
        if (!name)
            return null;
        const statusValue = typeof entry.status === "string" ? entry.status : undefined;
        const status = statusValue === "connected" || statusValue === "connecting" || statusValue === "failed" || statusValue === "unknown"
            ? statusValue
            : source === "run" ? "connected" : undefined;
        const toolsCount = typeof entry.toolsCount === "number"
            ? entry.toolsCount
            : typeof entry.toolCount === "number"
                ? entry.toolCount
                : Array.isArray(entry.tools)
                    ? entry.tools.length
                    : undefined;
        return { name, source, status, toolsCount };
    })
        .filter((entry) => entry !== null)
        .sort(compareResourceEntries);
}
async function applyMcpStatus(runId, snapshot) {
    if (!runId)
        return snapshot;
    const statusEntries = await readMcpStatusEntries(runId).catch(() => []);
    if (statusEntries.length === 0)
        return snapshot;
    const byName = new Map(statusEntries.map((entry) => [entry.name, entry]));
    return {
        ...snapshot,
        mcpServers: snapshot.mcpServers.map((entry) => {
            const live = byName.get(entry.name);
            return live ? { ...entry, status: live.status ?? entry.status, toolsCount: live.toolsCount ?? entry.toolsCount } : entry;
        }),
        checkedAt: Date.now(),
    };
}
async function readMcpStatusEntries(runId) {
    const raw = await readFile(getRunPath(runId, "mcp-status.json"), "utf-8");
    const parsed = JSON.parse(raw);
    if (!isRecord(parsed))
        return [];
    const servers = parsed.servers ?? parsed.mcpServers;
    if (Array.isArray(servers))
        return normalizeResourceEntries(servers, "run");
    if (isRecord(servers)) {
        return Object.entries(servers)
            .map(([name, value]) => {
            const record = isRecord(value) ? value : {};
            const statusValue = typeof record.status === "string" ? record.status : undefined;
            const status = statusValue === "connected" || statusValue === "connecting" || statusValue === "failed" || statusValue === "unknown"
                ? statusValue
                : "unknown";
            const toolsCount = typeof record.toolsCount === "number"
                ? record.toolsCount
                : typeof record.toolCount === "number"
                    ? record.toolCount
                    : Array.isArray(record.tools)
                        ? record.tools.length
                        : undefined;
            return { name, source: "run", status, toolsCount };
        })
            .sort(compareResourceEntries);
    }
    return [];
}
async function collectSkillEntries(root) {
    return collectNamedDirs([
        { path: join(root, ".agents", "skills"), source: "project" },
        { path: join(root, ".kimi", "skills"), source: "project" },
        { path: join(root, ".omk", "skills"), source: "project" },
        { path: join(getUserHome(), ".codex", "skills"), source: "global" },
        { path: join(getUserHome(), ".agents", "skills"), source: "global" },
        { path: join(getUserHome(), ".kimi", "skills"), source: "global" },
    ], "SKILL.md");
}
async function collectHookEntries(root) {
    return collectNamedFiles([
        { path: join(root, ".omk", "hooks"), source: "project" },
        { path: join(root, ".kimi", "hooks"), source: "project" },
        { path: join(getUserHome(), ".kimi", "hooks"), source: "global" },
        { path: join(getUserHome(), ".codex", "hooks"), source: "global" },
    ]);
}
async function collectNamedDirs(dirs, requiredFile) {
    const byName = new Map();
    for (const dir of dirs) {
        const entries = await readdir(dir.path, { withFileTypes: true }).catch(() => []);
        for (const entry of entries) {
            if (!entry.isDirectory())
                continue;
            if (!(await pathExists(join(dir.path, entry.name, requiredFile))))
                continue;
            upsertResource(byName, { name: entry.name, source: dir.source });
        }
    }
    return [...byName.values()].sort(compareResourceEntries);
}
async function collectNamedFiles(dirs) {
    const byName = new Map();
    for (const dir of dirs) {
        const entries = await readdir(dir.path, { withFileTypes: true }).catch(() => []);
        for (const entry of entries) {
            if (!entry.isFile() && !entry.isSymbolicLink())
                continue;
            if (entry.name.startsWith(".") || entry.name.endsWith(".sample"))
                continue;
            upsertResource(byName, { name: entry.name, source: dir.source });
        }
    }
    return [...byName.values()].sort(compareResourceEntries);
}
function upsertResource(byName, entry) {
    const current = byName.get(entry.name);
    if (!current || (current.source === "global" && entry.source === "project")) {
        byName.set(entry.name, entry);
    }
}
function compareResourceEntries(a, b) {
    const rank = (source) => source === "run" ? 0 : source === "project" ? 1 : source === "builtin" ? 2 : 3;
    const sourceRank = rank(a.source) - rank(b.source);
    return sourceRank || a.name.localeCompare(b.name);
}
// ── DeepSeek helpers ──
export async function getCockpitDeepSeekSnapshot() {
    const [providerStatus, key] = await Promise.all([
        getDeepSeekProviderStatus(),
        resolveDeepSeekApiKey(),
    ]);
    const base = {
        enabled: providerStatus.enabled,
        apiKeySet: providerStatus.apiKeySet,
        apiKeySource: providerStatus.apiKeySource,
        checkedAt: Date.now(),
    };
    if (!providerStatus.enabled) {
        return {
            ...base,
            available: false,
            reason: providerStatus.disabledReason ?? "DeepSeek disabled",
            balances: [],
        };
    }
    if (!key.apiKey) {
        return {
            ...base,
            available: false,
            reason: `${key.apiKeyEnv} is not set`,
            balances: [],
        };
    }
    const balance = await checkDeepSeekBalance({ apiKey: key.apiKey, timeoutMs: 4_000 });
    return {
        ...base,
        available: balance.available,
        reason: balance.reason,
        balances: (balance.balance?.balance_infos ?? []).map((item) => ({
            currency: item.currency,
            total: item.total_balance,
            granted: item.granted_balance,
            toppedUp: item.topped_up_balance,
        })),
        checkedAt: balance.checkedAt,
    };
}
export function formatDeepSeekBalance(snapshot) {
    if (!snapshot.apiKeySet)
        return "key-missing";
    if (snapshot.balances.length === 0)
        return snapshot.available ? "unknown" : "n/a";
    return snapshot.balances
        .slice(0, 2)
        .map((balance) => `${balance.currency} ${formatBalanceValue(balance.total)}`)
        .join(",");
}
function formatBalanceValue(value) {
    const parsed = Number(value);
    if (!Number.isFinite(parsed))
        return truncateText(value, 10);
    return parsed.toFixed(parsed >= 100 ? 0 : 2);
}
export function computeCockpitLayout(args) {
    const cols = Math.max(80, args.cols);
    const rows = Math.max(24, args.rows);
    const gap = 1;
    const footerH = 1;
    const composerH = Math.max(2, args.composerHeight);
    const workingH = Math.max(0, args.workingHeight);
    const rightRailW = args.rightRailPinned && cols >= 110
        ? Math.min(44, Math.max(34, Math.floor(cols * 0.25)))
        : 0;
    const leftW = rightRailW > 0 ? cols - rightRailW - gap : cols;
    const leftPane = {
        x: 1,
        y: 1,
        w: leftW - 1,
        h: rows - footerH,
    };
    const composerY = Math.max(6, rows - footerH - composerH - Math.max(0, args.composerLiftRows));
    const workingY = Math.max(3, composerY - workingH - 1);
    const transcript = {
        x: leftPane.x,
        y: leftPane.y,
        w: leftPane.w,
        h: Math.max(1, workingY - leftPane.y - 1),
    };
    const working = {
        x: leftPane.x,
        y: workingY,
        w: leftPane.w,
        h: workingH,
    };
    const composer = {
        x: leftPane.x,
        y: composerY,
        w: leftPane.w,
        h: composerH,
    };
    const rightRail = rightRailW > 0
        ? {
            x: cols - rightRailW + 1,
            y: 1,
            w: rightRailW,
            h: rows - footerH,
        }
        : null;
    const footer = {
        x: 1,
        y: rows,
        w: cols,
        h: footerH,
    };
    return {
        leftPane,
        transcript,
        working,
        composer,
        rightRail,
        footer,
    };
}
