/** Resolves the concrete review execution plan from host signals and llm config. */
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { detectCodexBinaryAvailable } from "../discovery/host-detection.js";
import { normalizeLlmModelSwitcher, } from "../llm/model-switcher.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_TIMEOUT_MS = Number(process.env.ONTO_LLM_TIMEOUT_MS) || 120_000;
const DEFAULT_MAX_ATTEMPTS = 2;
// ---------------------------------------------------------------------------
// Observability
// ---------------------------------------------------------------------------
/**
 * Plan-level STDERR log. Mirrors `[provider-ladder]` and `[model-call]`
 * patterns (PR #91 / PR #93) so operators can reconstruct the full decision
 * sequence from a single STDERR capture.
 *
 * No suppressor env var: decision rationale is load-bearing for review
 * reproducibility. Tests capture via vi.spyOn(process.stderr, "write").
 */
function emitPlanLog(line) {
    process.stderr.write(`[plan] ${line}\n`);
}
function readCodexAuthState() {
    const codexAuthPath = path.join(os.homedir(), ".codex", "auth.json");
    if (!fs.existsSync(codexAuthPath)) {
        return { chatgptOAuth: false, openaiApiKey: null };
    }
    try {
        const auth = JSON.parse(fs.readFileSync(codexAuthPath, "utf8"));
        const oauth = auth.auth_mode === "chatgpt" ||
            (auth.tokens && typeof auth.tokens.access_token === "string");
        const openaiKey = typeof auth.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY.length > 0
            ? auth.OPENAI_API_KEY
            : null;
        return { chatgptOAuth: Boolean(oauth), openaiApiKey: openaiKey };
    }
    catch {
        return { chatgptOAuth: false, openaiApiKey: null };
    }
}
// ---------------------------------------------------------------------------
// Ladder resolution
// ---------------------------------------------------------------------------
/**
 * Compute the ExecutionPlan by walking the context-separation ladder.
 *
 * Priority (higher rank = preferred, matches sketch §3.2):
 *   P0  Mock (ONTO_LLM_MOCK=1)                        — test only
 *   P1a Explicit --codex CLI flag                      — subprocess (S0)
 *   P1f env ONTO_HOST_RUNTIME override                 — any rank
 *   P2  Auto-detected Claude Code host (CLAUDECODE=1)  — host nested spawn (S2)
 *   P3  Auto-detected codex binary + auth.json         — subprocess (S0)
 *   P4  llm auth/provider switcher                     — external HTTP (S1) or codex OAuth
 *   Fail-fast when nothing is viable.
 *
 * The `review:` axis block owns topology. The `llm` switcher owns provider
 * and auth selection. Missing viable execution input returns `no_host`.
 */
export function resolveExecutionPlan(args) {
    const env = args.env ?? process.env;
    const claudeHost = args.claudeHost ?? env.CLAUDECODE === "1";
    const codexAvailable = args.codexAvailable ?? detectCodexBinaryAvailable();
    const trace = [];
    const log = (line) => {
        emitPlanLog(line);
        trace.push(line);
    };
    const retry_policy = {
        timeout_ms: DEFAULT_TIMEOUT_MS,
        max_attempts: DEFAULT_MAX_ATTEMPTS,
        backoff: "exponential",
    };
    // P0: Mock — test envelope.
    if (env.ONTO_LLM_MOCK === "1") {
        log("P0 mock: matched (ONTO_LLM_MOCK=1)");
        return {
            type: "resolved",
            plan: {
                separation_rank: "S3",
                execution_realization: "mock",
                host_runtime: "mock",
                provider_identity: "mock",
                retry_policy,
                plan_trace: trace,
            },
        };
    }
    // P1a: Explicit --codex flag. Overrides all other inputs.
    if (args.explicitCodex) {
        log("P1 explicit-codex: matched (--codex flag)");
        return resolveCodexPlan(log, trace, retry_policy, args.ontoConfig);
    }
    // P1f: env ONTO_HOST_RUNTIME override.
    const envHost = env.ONTO_HOST_RUNTIME?.trim().toLowerCase();
    if (envHost === "codex") {
        log("P1 env-override: ONTO_HOST_RUNTIME=codex");
        return resolveCodexPlan(log, trace, retry_policy, args.ontoConfig);
    }
    if (envHost === "claude") {
        log("P1 env-override: ONTO_HOST_RUNTIME=claude → agent-teams");
        return {
            type: "resolved",
            plan: {
                separation_rank: "S2",
                execution_realization: "agent-teams",
                host_runtime: "claude",
                provider_identity: "claude-code",
                retry_policy,
                plan_trace: trace,
            },
        };
    }
    if (envHost === "anthropic" || envHost === "openai" || envHost === "grok" || envHost === "lmstudio") {
        log(`P1 env-override: ONTO_HOST_RUNTIME=${envHost} → ts_inline_http`);
        return resolveExternalHttpPlan(log, trace, retry_policy, args.ontoConfig, { forcedProvider: envHost });
    }
    if (envHost === "standalone") {
        log("P1 env-override: ONTO_HOST_RUNTIME=standalone → ts_inline_http");
        return resolveExternalHttpPlan(log, trace, retry_policy, args.ontoConfig);
    }
    // P2: Auto-detected Claude Code host (stay-in-host).
    if (claudeHost) {
        log("P2 auto: claudeHost=true → agent-teams / host-nested-spawn");
        return {
            type: "resolved",
            plan: {
                separation_rank: "S2",
                execution_realization: "agent-teams",
                host_runtime: "claude",
                provider_identity: "claude-code",
                retry_policy,
                plan_trace: trace,
            },
        };
    }
    // P3: Auto-detected codex. Auth content is validated at invocation time.
    if (codexAvailable) {
        log("P3 auto: codex binary + auth.json present → subprocess");
        return resolveCodexPlan(log, trace, retry_policy, args.ontoConfig);
    }
    log("P3 auto: codex binary unavailable → skip");
    // P4: canonical llm switcher.
    const llmSelection = normalizeLlmModelSwitcher(args.ontoConfig.llm);
    if (llmSelection) {
        if (llmSelection.provider === "codex") {
            log("P4 llm switcher: auth=oauth provider=openai → codex subprocess");
            return resolveCodexPlan(log, trace, retry_policy, args.ontoConfig);
        }
        log(`P4 llm switcher: auth=${llmSelection.auth} provider=${llmSelection.provider} → ts_inline_http`);
        return resolveExternalHttpPlan(log, trace, retry_policy, args.ontoConfig);
    }
    log("final: no viable path → no_host");
    return {
        type: "no_host",
        plan_trace: trace,
        reason: buildNoHostReason(),
    };
}
// ---------------------------------------------------------------------------
// Sub-resolvers
// ---------------------------------------------------------------------------
function resolveCodexPlan(log, trace, retry_policy, config) {
    const selection = normalizeLlmModelSwitcher(config.llm);
    const modelId = selection?.provider === "codex" ? selection.model_id : undefined;
    log(`codex plan: separation_rank=S0 executor=subprocess model_id=${modelId ?? "(codex default)"}`);
    return {
        type: "resolved",
        plan: {
            separation_rank: "S0",
            execution_realization: "subagent",
            host_runtime: "codex",
            provider_identity: "codex",
            ...(modelId ? { model_id: modelId } : {}),
            retry_policy,
            plan_trace: trace,
        },
    };
}
function resolveExternalHttpPlan(log, trace, retry_policy, config, opts) {
    const providerField = pickExternalProviderField(config, opts);
    if (!providerField.provider) {
        log("external-http: no provider identified (llm.provider unset)");
        return {
            type: "no_host",
            plan_trace: trace,
            reason: buildMissingExternalProviderReason(),
        };
    }
    const provider = providerField.provider;
    const selection = normalizeLlmModelSwitcher(config.llm);
    const modelId = selection?.model_id;
    const base_url = selection?.base_url;
    log(`external-http plan: provider=${provider} source=${providerField.source} model_id=${modelId ?? "(unresolved)"} base_url=${base_url ?? "(default)"}`);
    return {
        type: "resolved",
        plan: {
            separation_rank: "S1",
            execution_realization: "ts_inline_http",
            host_runtime: provider,
            provider_identity: provider,
            ...(modelId ? { model_id: modelId } : {}),
            ...(base_url ? { base_url } : {}),
            retry_policy,
            plan_trace: trace,
        },
    };
}
function pickExternalProviderField(config, opts) {
    if (opts?.forcedProvider) {
        return { provider: opts.forcedProvider, source: "forced (env ONTO_HOST_RUNTIME)" };
    }
    const selection = normalizeLlmModelSwitcher(config.llm);
    if (selection && selection.provider !== "codex") {
        return { provider: selection.provider, source: "llm" };
    }
    return { provider: null, source: "(none)" };
}
// ---------------------------------------------------------------------------
// Error messages
// ---------------------------------------------------------------------------
function buildNoHostReason() {
    return [
        "Review execution plan을 해소할 수 없습니다.",
        "현재 감지 결과: Claude Code 세션 아님(CLAUDECODE unset), codex 바이너리 또는 ~/.codex/auth.json 부재, llm switcher 미설정.",
        "",
        "다음 중 한 가지로 해결하세요:",
        "  1. Claude Code 세션에서 `onto review` 재실행",
        "  2. codex CLI 설치 + `codex login` 후 재실행",
        "  3. `--codex` 플래그로 codex subprocess 강제",
        "  4. `.onto/config.yml` 에 `review:` block 추가 (`.onto/processes/configuration.md` 참고)",
        "  5. `.onto/config.yml` 에 llm: { auth, provider, model } 설정",
        "  6. local 실행은 llm.auth=local + llm.provider=lmstudio 로 설정",
    ].join("\n");
}
function buildMissingExternalProviderReason() {
    return [
        "LLM switcher 미지정.",
        "`.onto/config.yml` 에 llm block 을 추가하세요:",
        "  llm:",
        "    auth: api_key",
        "    provider: anthropic    # 또는 openai | grok | lmstudio",
        "    model: <model-id>",
    ].join("\n");
}
