import { Command } from "commander";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { findProjectRoot, log, GLOBAL_ENV_PATH } from "../utils.js";
import { fetchWithProxy } from "@actalk/inkos-core";
import { ensureNodeRuntimePinFiles, evaluateSqliteMemorySupport, inspectNodeRuntimePinFiles, } from "../runtime-requirements.js";
function buildDoctorProbePlans(preferredApiFormat, preferredStream) {
    const plans = [];
    const seen = new Set();
    const push = (apiFormat, stream) => {
        const key = `${apiFormat}:${stream ? "1" : "0"}`;
        if (seen.has(key))
            return;
        seen.add(key);
        plans.push({ apiFormat, stream });
    };
    if (preferredApiFormat) {
        push(preferredApiFormat, preferredStream ?? false);
        push(preferredApiFormat, !(preferredStream ?? false));
    }
    const alternate = preferredApiFormat === "responses" ? "chat" : "responses";
    push(alternate, false);
    push(alternate, true);
    push("chat", false);
    push("chat", true);
    push("responses", false);
    push("responses", true);
    return plans;
}
export function buildDoctorModelCandidates(preferredModel, discoveredModels) {
    const seen = new Set();
    const candidates = [];
    const push = (value) => {
        if (!value || value.trim().length === 0)
            return;
        const model = value.trim();
        if (seen.has(model))
            return;
        seen.add(model);
        candidates.push(model);
    };
    push(preferredModel);
    for (const model of discoveredModels)
        push(model.id);
    push("gpt-5.4");
    push("gpt-4o");
    push("claude-sonnet-4-6");
    push("MiniMax-M2.7");
    push("kimi-k2.5");
    push("gemini-2.5-flash");
    return candidates;
}
export function resolveDoctorModelsBaseUrl(service, baseUrl, resolveServiceModelsBaseUrl) {
    if (!service || service.length === 0) {
        return baseUrl;
    }
    return resolveServiceModelsBaseUrl(service) ?? baseUrl;
}
async function fetchDoctorModels(modelsBaseUrl, apiKey, proxyUrl) {
    const modelsUrl = modelsBaseUrl.replace(/\/$/, "") + "/models";
    try {
        const res = await fetchWithProxy(modelsUrl, {
            headers: { Authorization: `Bearer ${apiKey}` },
            signal: AbortSignal.timeout(10_000),
        }, proxyUrl);
        if (!res.ok)
            return [];
        const json = await res.json();
        return (json.data ?? []).map((model) => ({ id: model.id, name: model.id }));
    }
    catch {
        return [];
    }
}
export const doctorCommand = new Command("doctor")
    .description("Check environment and project health")
    .option("--repair-node-runtime", "Write .nvmrc and .node-version pinned to Node 22 for this project")
    .action(async (opts) => {
    const checks = [];
    const root = findProjectRoot();
    if (opts.repairNodeRuntime) {
        const repair = await ensureNodeRuntimePinFiles(root);
        checks.push({
            name: "Node runtime pin files repaired",
            ok: true,
            detail: repair.updated
                ? `Wrote ${repair.written.join(", ")} -> Node 22`
                : "Already pinned to Node 22",
        });
    }
    // 1. Check Node.js version
    const nodeVersion = process.version;
    const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
    checks.push({
        name: "Node.js >= 20",
        ok: major >= 20,
        detail: nodeVersion,
    });
    checks.push({
        name: "SQLite memory index (Node 22+)",
        ...evaluateSqliteMemorySupport({ nodeVersion }),
    });
    checks.push({
        name: "Node runtime pin files",
        ...await inspectNodeRuntimePinFiles(root),
    });
    // 2. Check inkos.json exists
    try {
        await readFile(join(root, "inkos.json"), "utf-8");
        checks.push({ name: "inkos.json", ok: true, detail: "Found" });
    }
    catch {
        checks.push({ name: "inkos.json", ok: false, detail: "Not found. Run 'inkos init'" });
    }
    // 3. Check .env exists
    try {
        await readFile(join(root, ".env"), "utf-8");
        checks.push({ name: ".env", ok: true, detail: "Found" });
    }
    catch {
        checks.push({ name: ".env", ok: false, detail: "Not found" });
    }
    // 4. Check global config
    {
        let hasGlobal = false;
        try {
            const globalContent = await readFile(GLOBAL_ENV_PATH, "utf-8");
            hasGlobal = globalContent.includes("INKOS_LLM_API_KEY=") && !globalContent.includes("your-api-key-here");
        }
        catch { /* no global config */ }
        checks.push({
            name: "Global Config",
            ok: hasGlobal,
            detail: hasGlobal ? `Found (${GLOBAL_ENV_PATH})` : "Not set. Run 'inkos config set-global'",
        });
    }
    // 5. Check effective LLM config (Studio project base + env/CLI overlay, or legacy env)
    {
        const { loadConfigWithDiagnostics } = await import("../utils.js");
        const { isApiKeyOptionalForEndpoint } = await import("@actalk/inkos-core");
        let configResult;
        try {
            configResult = await loadConfigWithDiagnostics({ requireApiKey: false });
            checks.push({
                name: "LLM Config Mode",
                ok: true,
                detail: `${configResult.diagnostics.configMode} (service=${configResult.diagnostics.serviceSource}, model=${configResult.diagnostics.modelSource}, key=${configResult.diagnostics.apiKeySource})`,
            });
            for (const warning of configResult.diagnostics.warnings) {
                checks.push({ name: "  Config Hint", ok: true, detail: warning });
            }
        }
        catch {
            // The API connectivity check below will report the concrete config failure.
        }
        const provider = configResult?.llm.provider;
        const baseUrl = configResult?.llm.baseUrl;
        const apiKey = configResult?.llm.apiKey;
        const apiKeyOptional = isApiKeyOptionalForEndpoint({ provider, baseUrl });
        const hasKey = apiKeyOptional || (!!apiKey && apiKey.length > 10 && apiKey !== "your-api-key-here");
        checks.push({
            name: "LLM API Key",
            ok: hasKey,
            detail: apiKeyOptional
                ? "Optional for local/self-hosted endpoint"
                : hasKey
                    ? "Configured"
                    : "Missing — save a Studio service key or set env for CLI/daemon/deploy",
        });
    }
    // 5. Check books directory
    try {
        const { StateManager } = await import("@actalk/inkos-core");
        const state = new StateManager(root);
        const books = await state.listBooks();
        checks.push({
            name: "Books",
            ok: true,
            detail: `${books.length} book(s) found`,
        });
    }
    catch {
        checks.push({ name: "Books", ok: true, detail: "0 books" });
    }
    // 5b. Check version migration status
    {
        const { existsSync } = await import("node:fs");
        const hasStructuredState = existsSync(join(root, "books"));
        if (hasStructuredState) {
            const { StateManager } = await import("@actalk/inkos-core");
            const sm = new StateManager(root);
            const bookIds = await sm.listBooks();
            let legacyCount = 0;
            for (const bid of bookIds) {
                const stateDir = join(sm.bookDir(bid), "story", "state");
                const hasNewState = existsSync(stateDir);
                if (!hasNewState)
                    legacyCount++;
            }
            if (legacyCount > 0) {
                checks.push({
                    name: "Version Migration",
                    ok: false,
                    detail: `${legacyCount} book(s) using legacy format (pre-v0.6). Run 'inkos write next' on each to auto-migrate, or re-init with 'inkos init'.`,
                });
            }
            else if (bookIds.length > 0) {
                checks.push({
                    name: "Version Migration",
                    ok: true,
                    detail: "All books use current format",
                });
            }
        }
    }
    // 6. API connectivity test
    try {
        const { createLLMClient, chatCompletion, LLMConfigSchema, isApiKeyOptionalForEndpoint, resolveServiceModelsBaseUrl } = await import("@actalk/inkos-core");
        const { loadConfig } = await import("../utils.js");
        let llmConfig;
        try {
            const config = await loadConfig();
            llmConfig = config.llm;
        }
        catch {
            // No project config — try building from global env
            const { config: loadDotenv } = await import("dotenv");
            loadDotenv({ path: GLOBAL_ENV_PATH });
            const env = process.env;
            const apiKeyOptional = isApiKeyOptionalForEndpoint({
                provider: env.INKOS_LLM_PROVIDER,
                baseUrl: env.INKOS_LLM_BASE_URL,
            });
            if ((env.INKOS_LLM_API_KEY || apiKeyOptional) && env.INKOS_LLM_BASE_URL && env.INKOS_LLM_MODEL) {
                llmConfig = LLMConfigSchema.parse({
                    provider: env.INKOS_LLM_PROVIDER ?? "custom",
                    baseUrl: env.INKOS_LLM_BASE_URL,
                    apiKey: env.INKOS_LLM_API_KEY ?? "",
                    model: env.INKOS_LLM_MODEL,
                });
            }
        }
        if (!llmConfig) {
            checks.push({
                name: "API Connectivity",
                ok: false,
                detail: "No LLM config available (no project config or global .env)",
            });
            checks.push({
                name: "  Hint",
                ok: false,
                detail: "Run `inkos setup`, `inkos config set-global`, or add LLM settings to the project .env file.",
            });
        }
        else {
            checks.push({
                name: "LLM Config",
                ok: true,
                detail: `provider=${llmConfig.provider} model=${llmConfig.model} stream=${llmConfig.stream ?? true} baseUrl=${llmConfig.baseUrl}`,
            });
            log("\n  [..] Testing API connectivity...");
            let connected = false;
            let detectedDetail = "";
            let lastError = "Unknown error";
            const modelsBaseUrl = resolveDoctorModelsBaseUrl(typeof llmConfig.service === "string" ? llmConfig.service : undefined, llmConfig.baseUrl, resolveServiceModelsBaseUrl);
            const discoveredModels = (llmConfig.apiKey && modelsBaseUrl)
                ? await fetchDoctorModels(modelsBaseUrl, llmConfig.apiKey, llmConfig.proxyUrl)
                : [];
            const modelCandidates = (llmConfig.provider === "openai" || discoveredModels.length > 0)
                ? buildDoctorModelCandidates(llmConfig.model, discoveredModels)
                : [llmConfig.model];
            const plans = llmConfig.provider === "openai"
                ? buildDoctorProbePlans(llmConfig.apiFormat, llmConfig.stream)
                : [{ apiFormat: (llmConfig.apiFormat ?? "chat"), stream: llmConfig.stream ?? true }];
            for (const model of modelCandidates) {
                for (const plan of plans) {
                    try {
                        const client = createLLMClient({
                            ...llmConfig,
                            model,
                            apiFormat: plan.apiFormat,
                            stream: plan.stream,
                        });
                        const response = await chatCompletion(client, model, [
                            { role: "user", content: "Say OK" },
                        ], { maxTokens: 16 });
                        connected = true;
                        detectedDetail = `OK (model: ${model}, apiFormat=${plan.apiFormat}, stream=${plan.stream}, tokens: ${response.usage.totalTokens})`;
                        break;
                    }
                    catch (error) {
                        lastError = error instanceof Error ? error.message : String(error);
                    }
                }
                if (connected) {
                    break;
                }
            }
            checks.push({
                name: "API Connectivity",
                ok: connected,
                detail: connected ? detectedDetail : lastError.split("\n")[0],
            });
            if (!connected && /\b(?:401|403|429)\b|unauthorized|forbidden|quota|额度|余额|配额/i.test(lastError)) {
                checks.push({
                    name: "  Hint",
                    ok: false,
                    detail: "检查 API Key 是否正确、模型是否可用，以及账号余额或配额是否足够。",
                });
            }
            if (!connected && llmConfig.provider === "openai") {
                checks.push({
                    name: "  Hint",
                    ok: false,
                    detail: "当前已自动尝试 chat/responses 与流式开关组合；如果仍失败，问题更可能在模型名、baseUrl 路径或服务商兼容性本身。",
                });
            }
        }
    }
    catch (e) {
        const errMsg = String(e);
        const hints = [];
        if (errMsg.includes("Connection error") || errMsg.includes("ECONNREFUSED") || errMsg.includes("fetch failed")) {
            hints.push("baseUrl 可能不正确，检查 INKOS_LLM_BASE_URL 是否包含完整路径（如 /v1）");
        }
        if (errMsg.includes("400")) {
            hints.push("检查提供方文档，确认该接口要求 stream=true、stream=false，还是根本不支持 stream");
            hints.push("检查模型名称是否正确（INKOS_LLM_MODEL）");
        }
        if (errMsg.includes("401")) {
            hints.push("API Key 无效，检查 INKOS_LLM_API_KEY");
        }
        checks.push({
            name: "API Connectivity",
            ok: false,
            detail: errMsg.split("\n")[0],
        });
        if (hints.length > 0) {
            for (const hint of hints) {
                checks.push({ name: "  Hint", ok: false, detail: hint });
            }
        }
    }
    // Output
    log("\nInkOS Doctor\n");
    for (const check of checks) {
        const icon = check.ok ? "[OK]" : "[!!]";
        log(`  ${icon} ${check.name}: ${check.detail}`);
    }
    const failed = checks.filter((c) => !c.ok);
    if (failed.length > 0) {
        log(`\n${failed.length} issue(s) found.`);
    }
    else {
        log("\nAll checks passed.");
    }
});
//# sourceMappingURL=doctor.js.map