/**
 * Kernel Execution — service.
 *
 * Concrete, worktree-backed implementation of the step / verify / merge
 * path. Every mutation lives in an isolated `git worktree`; verify hashes
 * the changed set; merge fast-forwards the target branch only when the
 * hashes still match. All three operations append one row to the kernel
 * ledger.
 *
 * Deliberately not a framework:
 *   - no plugin bus
 *   - no DSL for mutations beyond write_file / append_file / delete_file / run_command
 *   - no attempt to replace phantom's SubAgentRuntime (that is PR 3)
 */
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { createHash, randomUUID } from 'node:crypto';
import { execAsync } from '../../utils/exec-async.js';
import { resolveNexusStateDir } from '../../engines/runtime-registry.js';
import { createKernelLedger, getKernelLedger, } from './ledger.js';
export class KernelExecutionServiceImpl {
    repoRoot;
    ledger;
    worktreeRoot;
    verifiers;
    backend;
    constructor(opts) {
        this.repoRoot = opts.repoRoot;
        this.ledger = opts.ledger ?? getKernelLedger();
        this.worktreeRoot =
            opts.worktreeRoot ?? path.join(resolveNexusStateDir(), 'kernel-execution', 'worktrees');
        this.backend = opts.backend;
        this.verifiers = new Map();
        for (const v of opts.verifiers ?? [])
            this.verifiers.set(v.id, v);
        fs.mkdirSync(this.worktreeRoot, { recursive: true });
    }
    async step(input) {
        const warnings = [];
        const worktreeRef = input.worktreeHintRef?.trim() && input.worktreeHintRef.trim().length > 0
            ? input.worktreeHintRef.trim()
            : `nexus/step/${input.stepId}`;
        const worktreePath = path.join(this.worktreeRoot, input.stepId);
        // Refuse to overwrite an existing worktree — stepIds must be unique.
        if (fs.existsSync(worktreePath)) {
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                op: 'step',
                status: 'failed',
                warnings: [`worktree already exists: ${worktreePath}`],
                reasons: ['duplicate_step'],
                meta: input.metadata,
            });
            return {
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                worktreeRef,
                worktreePath,
                baseRef: input.baseRef ?? 'HEAD',
                changedFiles: [],
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
            };
        }
        // Resolve baseRef → concrete sha so we know what we branched from.
        const baseRef = input.baseRef?.trim() && input.baseRef.trim().length > 0 ? input.baseRef.trim() : 'HEAD';
        let baseSha;
        try {
            baseSha = (await gitOut(this.repoRoot, ['rev-parse', '--verify', baseRef])).trim();
        }
        catch (err) {
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                op: 'step',
                status: 'failed',
                warnings: [`base ref not found: ${baseRef}`],
                reasons: ['bad_base_ref'],
                meta: input.metadata,
            });
            return {
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                worktreeRef,
                worktreePath,
                baseRef,
                changedFiles: [],
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
                debug: { error: String(err?.message ?? err) },
            };
        }
        // Create worktree on a fresh branch pointing at baseSha.
        try {
            await git(this.repoRoot, ['worktree', 'add', '-b', worktreeRef, worktreePath, baseSha]);
        }
        catch (err) {
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                op: 'step',
                status: 'failed',
                warnings: [`git worktree add failed: ${String(err?.message ?? err)}`],
                reasons: ['worktree_add_failed'],
                meta: input.metadata,
            });
            return {
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                worktreeRef,
                worktreePath,
                baseRef,
                changedFiles: [],
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
                debug: { error: String(err?.message ?? err) },
            };
        }
        // Apply mutations in order. Any failure aborts and cleans up.
        try {
            for (const m of input.mutations ?? []) {
                await applyMutation(worktreePath, m);
            }
        }
        catch (err) {
            warnings.push(`mutation failed: ${String(err?.message ?? err)}`);
            await safeCleanupWorktree(this.repoRoot, worktreePath);
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                op: 'step',
                status: 'failed',
                worktreeRef,
                warnings,
                reasons: ['mutation_failed'],
                meta: input.metadata,
            });
            return {
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                worktreeRef,
                worktreePath,
                baseRef,
                changedFiles: [],
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
                debug: { error: String(err?.message ?? err) },
            };
        }
        // Stage everything and commit. If there's nothing to stage, we still
        // want a no-op step recorded — the step then has no commitSha.
        await git(worktreePath, ['add', '-A']);
        const hasStaged = (await gitOut(worktreePath, ['diff', '--cached', '--name-only'])).trim().length > 0;
        let commitSha;
        let changedFiles = [];
        if (hasStaged) {
            const msg = `[kernel] step ${input.stepId}: ${input.task}`.slice(0, 500);
            try {
                await git(worktreePath, ['-c', 'commit.gpgsign=false', 'commit', '-m', msg]);
                commitSha = (await gitOut(worktreePath, ['rev-parse', 'HEAD'])).trim();
                const names = await gitOut(worktreePath, ['diff', '--name-only', `${baseSha}..HEAD`]);
                changedFiles = names.split('\n').map((l) => l.trim()).filter(Boolean);
            }
            catch (err) {
                warnings.push(`commit failed: ${String(err?.message ?? err)}`);
            }
        }
        const entry = await this.ledger.append({
            runId: input.runId,
            stepId: input.stepId,
            op: 'step',
            status: 'succeeded',
            worktreeRef,
            changedFiles,
            warnings,
            meta: {
                ...input.metadata,
                worktreePath,
                baseRef,
                baseSha,
                commitSha,
                task: input.task,
                intent: input.intent,
            },
        });
        return {
            runId: input.runId,
            stepId: input.stepId,
            status: 'succeeded',
            worktreeRef,
            worktreePath,
            baseRef,
            changedFiles,
            commitSha,
            warnings,
            ledgerEntryId: entry.id,
            debug: { baseSha },
        };
    }
    async verify(input) {
        const stepEntry = await this.ledger.findStep(input.runId, input.stepId);
        const verifyId = randomUUID();
        if (!stepEntry || stepEntry.status !== 'succeeded') {
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                verifyId,
                op: 'verify',
                status: 'failed',
                warnings: ['no succeeded step found for verify'],
                reasons: ['step_not_found'],
                meta: input.metadata,
            });
            return {
                verifyId,
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                verifierResults: [],
                verifiedFileHashes: {},
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
            };
        }
        const worktreePath = stepEntry.meta?.worktreePath;
        if (!worktreePath || !fs.existsSync(worktreePath)) {
            const entry = await this.ledger.append({
                runId: input.runId,
                stepId: input.stepId,
                verifyId,
                op: 'verify',
                status: 'failed',
                warnings: [`worktree path missing for step ${input.stepId}`],
                reasons: ['worktree_missing'],
                meta: input.metadata,
            });
            return {
                verifyId,
                runId: input.runId,
                stepId: input.stepId,
                status: 'failed',
                verifierResults: [],
                verifiedFileHashes: {},
                warnings: entry.warnings ?? [],
                ledgerEntryId: entry.id,
            };
        }
        const changedFiles = stepEntry.changedFiles ?? [];
        const verifiedFileHashes = await hashFiles(worktreePath, changedFiles);
        const verifierResults = [];
        const warnings = [];
        for (const id of input.verifierIds ?? []) {
            const v = this.verifiers.get(id);
            if (!v) {
                verifierResults.push({ id, status: 'failed', output: 'verifier not registered' });
                warnings.push(`unknown verifier: ${id}`);
                continue;
            }
            try {
                const res = await v.run({ worktreePath, changedFiles, stepId: input.stepId, runId: input.runId });
                verifierResults.push({ id, status: res.status, output: res.output });
            }
            catch (err) {
                verifierResults.push({ id, status: 'failed', output: String(err?.message ?? err) });
            }
        }
        const status = verifierResults.every((r) => r.status === 'passed') ? 'passed' : 'failed';
        const entry = await this.ledger.append({
            runId: input.runId,
            stepId: input.stepId,
            verifyId,
            op: 'verify',
            status,
            worktreeRef: stepEntry.worktreeRef,
            changedFiles,
            verifiedFileHashes,
            verifierResults,
            warnings,
            meta: input.metadata,
        });
        return {
            verifyId,
            runId: input.runId,
            stepId: input.stepId,
            status,
            verifierResults,
            verifiedFileHashes,
            warnings,
            ledgerEntryId: entry.id,
        };
    }
    async merge(input) {
        const mergeId = randomUUID();
        const stepEntry = await this.ledger.findStep(input.runId, input.stepId);
        if (!stepEntry || stepEntry.status !== 'succeeded') {
            return this.rejectMerge({
                mergeId,
                input,
                reason: 'step_not_found',
                warnings: [`no succeeded step found for ${input.stepId}`],
            });
        }
        const worktreePath = stepEntry.meta?.worktreePath;
        const commitSha = stepEntry.meta?.commitSha;
        if (!worktreePath || !commitSha) {
            return this.rejectMerge({
                mergeId,
                input,
                reason: 'step_not_found',
                warnings: ['step recorded but has no worktree path or commit sha'],
            });
        }
        // Pick the verify row to trust: explicit verifyId if given, else latest passed.
        let verifyEntry;
        if (input.verifyId) {
            verifyEntry = await this.ledger.findVerifyById(input.verifyId);
            if (!verifyEntry || verifyEntry.stepId !== input.stepId || verifyEntry.runId !== input.runId) {
                return this.rejectMerge({
                    mergeId,
                    input,
                    reason: 'no_verify',
                    warnings: [`verify ${input.verifyId} not found or does not match step`],
                });
            }
        }
        else {
            verifyEntry = await this.ledger.findLatestPassedVerify(input.runId, input.stepId);
            if (!verifyEntry) {
                return this.rejectMerge({
                    mergeId,
                    input,
                    reason: 'no_verify',
                    warnings: ['no passed verify recorded for step'],
                });
            }
        }
        if (verifyEntry.status !== 'passed') {
            return this.rejectMerge({
                mergeId,
                input,
                reason: 'verify_failed',
                warnings: [`verify ${verifyEntry.verifyId} did not pass`],
            });
        }
        // Stale-verify guard: re-hash the recorded files and ensure they still
        // match the hashes we recorded at verify time. Any drift (incl. a
        // later step that rewrote the tree) must reject the merge.
        const changedFiles = verifyEntry.changedFiles ?? stepEntry.changedFiles ?? [];
        const currentHashes = await hashFiles(worktreePath, changedFiles);
        const prior = verifyEntry.verifiedFileHashes ?? {};
        const drift = [];
        for (const f of changedFiles) {
            if (currentHashes[f] !== prior[f])
                drift.push(f);
        }
        if (drift.length > 0) {
            return this.rejectMerge({
                mergeId,
                input,
                reason: 'stale_verify',
                warnings: [`worktree changed since verify: ${drift.slice(0, 10).join(', ')}`],
            });
        }
        // Fast-forward targetRef. We want "target is ancestor of step commit";
        // anything else is a conflict the kernel will not silently resolve.
        let targetSha = null;
        try {
            targetSha = (await gitOut(this.repoRoot, ['rev-parse', '--verify', `refs/heads/${input.targetRef}`])).trim();
        }
        catch {
            targetSha = null;
        }
        if (targetSha) {
            try {
                await git(this.repoRoot, ['merge-base', '--is-ancestor', targetSha, commitSha]);
            }
            catch {
                return this.rejectMerge({
                    mergeId,
                    input,
                    reason: 'merge_conflict',
                    warnings: [`targetRef ${input.targetRef} is not an ancestor of step commit`],
                });
            }
        }
        try {
            if (targetSha) {
                await git(this.repoRoot, [
                    'update-ref',
                    `refs/heads/${input.targetRef}`,
                    commitSha,
                    targetSha,
                ]);
            }
            else {
                await git(this.repoRoot, ['update-ref', `refs/heads/${input.targetRef}`, commitSha]);
            }
        }
        catch (err) {
            return this.rejectMerge({
                mergeId,
                input,
                reason: 'internal_error',
                warnings: [`update-ref failed: ${String(err?.message ?? err)}`],
            });
        }
        const entry = await this.ledger.append({
            runId: input.runId,
            stepId: input.stepId,
            mergeId,
            op: 'merge',
            status: 'merged',
            worktreeRef: stepEntry.worktreeRef,
            changedFiles: stepEntry.changedFiles,
            reasons: input.reason ? [input.reason] : undefined,
            meta: {
                ...input.metadata,
                mergedSha: commitSha,
                targetRef: input.targetRef,
                verifyId: verifyEntry.verifyId,
            },
        });
        return {
            mergeId,
            runId: input.runId,
            stepId: input.stepId,
            status: 'merged',
            mergedSha: commitSha,
            targetRef: input.targetRef,
            warnings: [],
            ledgerEntryId: entry.id,
            debug: { verifyId: verifyEntry.verifyId },
        };
    }
    /**
     * Execute a task via the configured RuntimeBackend (Phantom/SubAgentRuntime
     * in the default installation). Requires a backend to be set in options.
     * Records one ledger entry — same pattern as step/verify/merge.
     */
    async execute(input) {
        if (!this.backend) {
            throw new Error(`kernel.run.execute: no RuntimeBackend configured. ` +
                `Pass a backend (e.g. PhantomRuntimeBackend) to KernelExecutionServiceOptions.`);
        }
        const prepared = await this.backend.prepareStep(input);
        const result = await this.backend.executeStep({
            ...input,
            candidateFiles: prepared.candidateFiles.length > 0
                ? prepared.candidateFiles
                : input.candidateFiles,
        });
        await this.ledger.append({
            runId: input.runId,
            stepId: input.stepId,
            op: 'step',
            status: result.warnings.some((w) => w.startsWith('phantom execution reached state: failed'))
                ? 'failed'
                : 'succeeded',
            worktreeRef: result.worktreeRef,
            changedFiles: result.changedFiles,
            warnings: result.warnings,
            meta: { ...input.metadata, backend: 'RuntimeBackend', summary: result.summary },
        });
        return result;
    }
    async rejectMerge(args) {
        const entry = await this.ledger.append({
            runId: args.input.runId,
            stepId: args.input.stepId,
            mergeId: args.mergeId,
            op: 'merge',
            status: 'rejected',
            warnings: args.warnings,
            reasons: [args.reason, ...(args.input.reason ? [args.input.reason] : [])],
            meta: { ...args.input.metadata, targetRef: args.input.targetRef },
        });
        return {
            mergeId: args.mergeId,
            runId: args.input.runId,
            stepId: args.input.stepId,
            status: 'rejected',
            targetRef: args.input.targetRef,
            rejectedReason: args.reason,
            warnings: args.warnings,
            ledgerEntryId: entry.id,
        };
    }
}
// ───────────────────────── helpers ─────────────────────────────────────
function assertSafeRelPath(p) {
    if (!p || typeof p !== 'string')
        throw new Error('path is required');
    if (path.isAbsolute(p))
        throw new Error(`path must be relative: ${p}`);
    const norm = path.normalize(p);
    if (norm.startsWith('..') || norm.split(path.sep).includes('..')) {
        throw new Error(`path escapes worktree: ${p}`);
    }
    return norm;
}
async function applyMutation(worktreePath, m) {
    switch (m.kind) {
        case 'write_file': {
            const rel = assertSafeRelPath(m.path);
            const abs = path.join(worktreePath, rel);
            await fs.promises.mkdir(path.dirname(abs), { recursive: true });
            await fs.promises.writeFile(abs, m.content, 'utf8');
            return;
        }
        case 'append_file': {
            const rel = assertSafeRelPath(m.path);
            const abs = path.join(worktreePath, rel);
            await fs.promises.mkdir(path.dirname(abs), { recursive: true });
            await fs.promises.appendFile(abs, m.content, 'utf8');
            return;
        }
        case 'delete_file': {
            const rel = assertSafeRelPath(m.path);
            const abs = path.join(worktreePath, rel);
            await fs.promises.rm(abs, { force: true });
            return;
        }
        case 'run_command': {
            const cwdRel = m.cwd ? assertSafeRelPath(m.cwd) : '.';
            const cwd = path.join(worktreePath, cwdRel);
            await execAsync(m.command, { cwd, env: cleanGitEnv() });
            return;
        }
    }
}
async function hashFiles(worktreePath, relPaths) {
    const out = {};
    for (const rel of relPaths) {
        const abs = path.join(worktreePath, rel);
        try {
            const buf = await fs.promises.readFile(abs);
            out[rel] = createHash('sha256').update(buf).digest('hex');
        }
        catch {
            out[rel] = 'missing';
        }
    }
    return out;
}
async function git(cwd, args) {
    await execAsync(shellJoin(['git', ...args]), { cwd, env: cleanGitEnv() });
}
async function gitOut(cwd, args) {
    const { stdout } = await execAsync(shellJoin(['git', ...args]), { cwd, env: cleanGitEnv() });
    return String(stdout ?? '');
}
async function safeCleanupWorktree(repoRoot, worktreePath) {
    try {
        await git(repoRoot, ['worktree', 'remove', '--force', worktreePath]);
    }
    catch {
        // Best-effort; worktree may not be tracked yet.
    }
    try {
        await fs.promises.rm(worktreePath, { recursive: true, force: true });
    }
    catch {
        // Ignore.
    }
}
/**
 * Quote+join argv for `exec`. We use `exec` (not `execFile`) so we can pass
 * through `-c commit.gpgsign=false` inline; every arg still gets quoted.
 */
function shellJoin(argv) {
    return argv
        .map((a) => {
        if (a === '')
            return "''";
        if (/^[A-Za-z0-9_\-./=:@+,]+$/.test(a))
            return a;
        return `'${a.replace(/'/g, `'\\''`)}'`;
    })
        .join(' ');
}
/**
 * Return a copy of process.env with GIT_* vars stripped. Necessary when
 * running inside a pre-push hook so the tmp/step repo isn't hijacked by
 * GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE leaking from the parent.
 */
function cleanGitEnv() {
    const env = {};
    for (const [k, v] of Object.entries(process.env)) {
        if (v !== undefined && !k.startsWith('GIT_'))
            env[k] = v;
    }
    return env;
}
// ───────────────────── factory ─────────────────────────────────────────
let singleton = null;
export function getKernelExecutionService(opts) {
    if (singleton && !opts)
        return singleton;
    if (opts) {
        singleton = new KernelExecutionServiceImpl(opts);
        return singleton;
    }
    // Default: wire against the repo the process is running inside.
    singleton = new KernelExecutionServiceImpl({ repoRoot: process.cwd() });
    return singleton;
}
export function configureKernelExecutionService(opts) {
    singleton = new KernelExecutionServiceImpl(opts);
    return singleton;
}
export function __resetKernelExecutionServiceForTests() {
    singleton = null;
}
/** Build a fresh, in-memory service against a temp ledger. Test-only sugar. */
export function createTestExecutionService(opts) {
    const ledgerPath = opts.ledgerPath ?? path.join(os.tmpdir(), `nexus-kernel-ledger-${randomUUID()}.jsonl`);
    return new KernelExecutionServiceImpl({
        repoRoot: opts.repoRoot,
        ledger: createKernelLedger(ledgerPath),
        worktreeRoot: opts.worktreeRoot,
        verifiers: opts.verifiers,
    });
}
