/**
 * Activation readiness for a routine, composed from the target-aware execution
 * context ({@link resolveJobExecutionContext}) plus the harness/target checks a
 * caller can perform on this box (agent installed). This is the gate `add`,
 * `edit`, `doctor`, and `resume` all run before activating a routine: ready →
 * active, any proven blocker → saved paused with a stable code + repair command.
 *
 * Structural context readiness is synchronous for the scheduler. Interactive
 * add/edit/doctor/resume additionally call the live variant below, which probes
 * authentication and Codex's native workspace-trust record before activation.
 */
import * as fs from 'fs';
import * as path from 'path';
import * as TOML from 'smol-toml';
import { resolveJobExecutionContext, resolveHostStrategy } from './scheduling/routines.js';
import { evaluateRoutineReadiness } from './routine-context.js';
import { readAuthHealth } from './auth-health.js';
import { machineId } from './machine-id.js';
import { getVersionHomePath, isVersionInstalled, resolveVersion } from './installations/versions.js';
import { probeLocalFleetAuth } from './auth-health.js';
import { resolveHostRunTarget } from './hosts/run-target.js';
import { hostIdentityArgs, sshTargetFor } from './hosts/types.js';
import { probeHost } from './hosts/ready.js';
import { sshExec, shellQuote } from './ssh-exec.js';
import { encodePowershell, powershellQuote, POWERSHELL_PROGRESS_SILENCE } from './hosts/remote-cmd.js';
/**
 * Verdicts that make a FIRE-TIME auth preflight block the run: the last live
 * probe found the account either server-rejected (`revoked`) or with no
 * credential at all (`unconfigured` — the "Please run /login" / "no account
 * signed in" case, which is the most common way a routine's dispatch account
 * goes dead). Everything else fails OPEN: `rate_limited` is still authenticated,
 * `expired` self-heals on the next refresh, `unverified` means signed-in but no
 * live probe endpoint (codex/grok), and `error` is indeterminate — none of those
 * should stop a fire. This is deliberately BROADER than {@link isDeadVerdict}
 * (display-only, `revoked` alone): a signed-out account must block a fire, not
 * just paint a red cell.
 */
function fireBlockingAuthVerdict(verdict) {
    return verdict === 'revoked' || verdict === 'unconfigured';
}
/**
 * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
 * exact (agent, version) a routine has resolved to run, and return an
 * `agent_auth_failed` blocker when that account is provably signed out — so the
 * daemon records a terminal `blocked` run (with the re-login repair) instead of
 * spawning a doomed run that 401s and burns a session (PHNX-3415).
 *
 * Cache-only (no network, no prompt — the daemon refreshes the cache
 * periodically and `fleet ping` writes it), and fails OPEN on a missing or
 * non-blocking verdict so a stale/absent probe never wedges a routine. It is
 * checked AFTER version rotation resolves the account (`launch.chain[0]`), so it
 * judges the identity the run will actually use, never a rotated-past dead pin.
 */
export function fireTimeAuthReadiness(agent, version) {
    const health = readAuthHealth(machineId(), agent, version);
    if (!health || !fireBlockingAuthVerdict(health.verdict))
        return null;
    const who = health.account ? ` (${health.account})` : '';
    return {
        code: 'agent_auth_failed',
        message: `the ${agent}${who} account is signed out — the last auth probe returned '${health.verdict}', so this run would fail authentication`,
        repair: `agents run ${agent}@${version} -- login`,
    };
}
/**
 * Evaluate whether a routine is ready to activate on this box. `probeAgent`
 * defaults to "is a version of the routine's agent resolvable" via
 * {@link resolveVersion}; pass a stub in tests to exercise the availability path
 * without an installed harness.
 */
export function evaluateActivationReadiness(config, deps = {}) {
    const strategy = resolveHostStrategy(config);
    const mode = strategy;
    // Local placement inspects this box's filesystem; a remote/cloud target defers
    // existence (its filesystem is unreachable here) and checks portability only.
    const context = resolveJobExecutionContext(config, {
        mode,
        probe: mode === 'local' ? undefined : null,
    });
    // A pinned version on LOCAL placement must exist here — accepting a routine
    // whose exact pin is absent just moves the failure to fire time (RUSH-2719).
    // Remote host/fleet targets defer the check: their installs are unreachable
    // from this box until the RUSH-2290 execution-context-on-target resolver.
    const pinnedLocally = mode === 'local' ? config.version : undefined;
    const probeAgent = deps.probeAgent ?? ((agent) => pinnedLocally
        ? isVersionInstalled(agent, pinnedLocally)
        : resolveVersion(agent) !== undefined);
    return evaluateRoutineReadiness(context, {
        // Command routines have no agent to install; workflow routines dispatch
        // through `agents run` (claude under the hood) and are checked at run time.
        agentInstalled: config.agent && !config.workflow && !config.command
            ? () => probeAgent(config.agent)
            : undefined,
    }, { agent: config.agent, version: pinnedLocally });
}
/** One-line human summary of a blocked readiness result, with its repair. */
export function formatReadinessBlocker(result) {
    if (result.ready || !result.readiness)
        return 'ready';
    const { code, message, repair } = result.readiness;
    return `${code}: ${message}${repair ? `\n  repair: ${repair}` : ''}`;
}
/** Parse the target HOME sentinel and the project catalog returned by one SSH call. */
export function parseRemoteProjectSnapshot(stdout) {
    const lines = stdout.split(/\r?\n/);
    const homeLine = lines.shift();
    if (!homeLine?.startsWith('__HOME__'))
        return undefined;
    const home = homeLine.slice('__HOME__'.length);
    if (!home)
        return undefined;
    try {
        const projects = JSON.parse(lines.join('\n'));
        if (!Array.isArray(projects))
            return undefined;
        if (!projects.every((entry) => entry && typeof entry === 'object' && typeof entry.name === 'string')) {
            return undefined;
        }
        return { home, projects: projects };
    }
    catch {
        return undefined;
    }
}
/** Build a target-native probe that creates and removes a file in the workspace. */
export function buildRemoteWorkspaceProbe(cwd, windows) {
    if (windows) {
        return `powershell -NoProfile -EncodedCommand ${encodePowershell(`$d=${powershellQuote(cwd)}; if (-not (Test-Path -LiteralPath $d -PathType Container)) { exit 1 }; $p=Join-Path $d ([IO.Path]::GetRandomFileName()); try { New-Item -ItemType File -Path $p -ErrorAction Stop | Out-Null; Remove-Item -LiteralPath $p -Force -ErrorAction Stop; exit 0 } catch { exit 1 }`)}`;
    }
    const pattern = path.posix.join(cwd, '.agents-routine-readiness.XXXXXX');
    return `probe_path=$(mktemp ${shellQuote(pattern)}) && rm -f "$probe_path"`;
}
/** A successful probe must contain the exact requested postcondition, not merely exit zero. */
export function probeOutputHasSentinel(stdout, sentinel) {
    const containsExact = (value) => {
        if (value === sentinel)
            return true;
        if (Array.isArray(value))
            return value.some(containsExact);
        if (value && typeof value === 'object')
            return Object.values(value).some(containsExact);
        return false;
    };
    return stdout.split(/\r?\n/).some((line) => {
        const trimmed = line.trim();
        if (trimmed === sentinel)
            return true;
        try {
            return containsExact(JSON.parse(trimmed));
        }
        catch {
            return false;
        }
    });
}
function codexWorkspaceTrusted(version, cwd) {
    try {
        const configPath = path.join(getVersionHomePath('codex', version), '.codex', 'config.toml');
        const parsed = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
        const projects = parsed.projects;
        return Object.entries(projects ?? {}).some(([root, project]) => {
            if (project.trust_level !== 'trusted')
                return false;
            const relative = path.relative(root, cwd);
            return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
        });
    }
    catch {
        return false;
    }
}
/**
 * Interactive setup/repair readiness. Unlike the scheduler's deterministic
 * structural gate, this completes a real local auth request and reads Codex's
 * native trust record before add/edit/resume can activate the definition.
 */
export async function evaluateActivationReadinessLive(config) {
    const structural = evaluateActivationReadiness(config);
    if (!structural.ready)
        return structural;
    const mode = resolveHostStrategy(config);
    if (mode === 'host' && config.host)
        return evaluateHostActivationReadiness(config);
    if (mode !== 'local' || !config.agent || config.workflow || config.command)
        return structural;
    const version = resolveVersion(config.agent);
    if (!version)
        return structural;
    const context = resolveJobExecutionContext(config, { mode: 'local' });
    let authVerdict;
    const rows = await probeLocalFleetAuth({ agents: [config.agent] });
    const row = rows.find((candidate) => candidate.version === version);
    const accepted = new Set(['live', 'rate_limited', 'unverified']);
    authVerdict = row && accepted.has(row.health.verdict)
        ? { ok: true }
        : { ok: false, reason: row?.health.verdict ?? 'unconfigured' };
    return evaluateRoutineReadiness(context, {
        agentInstalled: () => true,
        ...(config.agent === 'codex' && context.absoluteCwd
            ? { codexTrusted: () => codexWorkspaceTrusted(version, context.absoluteCwd) }
            : {}),
        authOk: () => authVerdict,
    }, { agent: config.agent });
}
/** Resolve and probe the actual SSH target used by a host-placed routine. */
export async function evaluateHostActivationReadiness(config) {
    const host = await resolveHostRunTarget(config.host);
    const target = sshTargetFor(host);
    const identity = hostIdentityArgs(host);
    if (!probeHost(target, host.os, identity).reachable) {
        return evaluateRoutineReadiness(resolveJobExecutionContext(config, { mode: 'host', probe: null }), {
            targetReachable: () => false,
        }, { agent: config.agent });
    }
    const windows = host.os?.toLowerCase().includes('win') ?? false;
    const homeCommand = windows
        ? `powershell -NoProfile -EncodedCommand ${encodePowershell(`${POWERSHELL_PROGRESS_SILENCE}; Write-Output ("__HOME__" + $HOME); & agents projects list --json`)}`
        : `bash -lc ${shellQuote('printf "__HOME__%s\\n" "$HOME"; agents projects list --json')}`;
    const projectResult = sshExec(target, homeCommand, { timeoutMs: 20_000, extraSshArgs: identity });
    if (projectResult.code !== 0) {
        return evaluateRoutineReadiness(resolveJobExecutionContext(config, { mode: 'host', probe: null }), {
            targetReachable: () => false,
        }, { agent: config.agent });
    }
    const snapshot = parseRemoteProjectSnapshot(projectResult.stdout);
    if (!snapshot) {
        return evaluateRoutineReadiness(resolveJobExecutionContext(config, { mode: 'host', probe: null }), {
            targetReachable: () => false,
        }, { agent: config.agent });
    }
    const targetHome = snapshot.home;
    const defs = snapshot.projects;
    const def = config.project ? defs.find((candidate) => candidate.name === config.project) : undefined;
    const projectResolution = config.project
        ? (def ? { defined: true, base: def.defaultPath ?? def.root } : { defined: false })
        : undefined;
    const unprobed = resolveJobExecutionContext(config, { mode: 'host', targetHome, probe: null, projectResolution });
    if (!unprobed.ready || !unprobed.absoluteCwd)
        return { context: unprobed, ready: false, readiness: unprobed.readiness };
    const check = buildRemoteWorkspaceProbe(unprobed.absoluteCwd, windows);
    const fsResult = sshExec(target, check, { timeoutMs: 12_000, extraSshArgs: identity });
    if (fsResult.code !== 0) {
        return evaluateRoutineReadiness({ ...unprobed, ready: false, readiness: {
                code: 'workspace_not_writable',
                message: `the execution directory is missing or not writable on ${host.name}: ${unprobed.resolvedCwd}`,
            } });
    }
    if (config.agent && !config.workflow && !config.command) {
        if (config.agent === 'codex') {
            const args = ['run', 'codex', 'Reply with exactly ROUTINE_READY', '--mode', 'plan', '--timeout', '45s', '--json'];
            const command = windows
                ? `powershell -NoProfile -EncodedCommand ${encodePowershell(`${POWERSHELL_PROGRESS_SILENCE}; Set-Location -LiteralPath ${powershellQuote(unprobed.absoluteCwd)}; & agents ${args.map(powershellQuote).join(' ')}`)}`
                : `cd ${shellQuote(unprobed.absoluteCwd)} && agents ${args.map(shellQuote).join(' ')}`;
            const probe = sshExec(target, command, { timeoutMs: 60_000, extraSshArgs: identity });
            if (probe.code !== 0 || !probeOutputHasSentinel(probe.stdout, 'ROUTINE_READY')) {
                const detail = `${probe.stderr}\n${probe.stdout}`.trim();
                const untrusted = detail.includes('trusted directory') || detail.includes('trusted workspace');
                return evaluateRoutineReadiness(unprobed, {
                    ...(untrusted ? { codexTrusted: () => false } : { authOk: () => ({ ok: false, reason: detail || 'probe failed' }) }),
                }, { agent: config.agent });
            }
        }
        else {
            const pingArgs = ['devices', 'ping', '--local', '--json'];
            const command = windows
                ? `powershell -NoProfile -EncodedCommand ${encodePowershell(`${POWERSHELL_PROGRESS_SILENCE}; & agents ${pingArgs.map(powershellQuote).join(' ')}`)}`
                : `agents ${pingArgs.map(shellQuote).join(' ')}`;
            const probe = sshExec(target, command, { timeoutMs: 30_000, extraSshArgs: identity });
            let verdict = 'error';
            if (probe.code === 0) {
                try {
                    const payload = JSON.parse(probe.stdout);
                    verdict = payload.rows?.find((row) => row.agent === config.agent)?.health.verdict ?? 'unconfigured';
                }
                catch {
                    verdict = 'error';
                }
            }
            if (!new Set(['live', 'rate_limited', 'unverified']).has(verdict)) {
                return evaluateRoutineReadiness(unprobed, {
                    authOk: () => ({ ok: false, reason: verdict }),
                }, { agent: config.agent });
            }
        }
    }
    return { context: unprobed, ready: true };
}
