/**
 * Git-worktree lifecycle helpers — ArtifactRecorder, WorktreeSession,
 * MergeMemoryAdapter, MultiTierConsensusPolicy.
 */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ByzantineConsensus } from '../../engines/byzantine-consensus.js';
import { WorktreeDoctorError, doctorGitWorktrees, summarizeWorktreeHealth, toWorktreeRemediation, } from '../../engines/worktree-health.js';
import { MergeOracle } from '../merge-oracle.js';
import { runCommand, quote, sanitizeFileName, relativeTo } from './worker.js';
import { execAsync as exec } from '../../utils/exec-async.js';
// ─── ArtifactRecorder ─────────────────────────────────────────────────────────
/**
 * Lightweight per-run artifact writer. Maintains a flat index of paths
 * written so callers can recover the full run artifact layout.
 */
export class ArtifactRecorder {
    runId;
    runDir;
    index = {};
    constructor(runId, baseDir) {
        this.runId = runId;
        const root = baseDir ?? path.join(os.tmpdir(), 'nexus-prime-runs');
        this.runDir = path.join(root, runId);
        fs.mkdirSync(this.runDir, { recursive: true });
    }
    workerDir(workerId) {
        const dir = path.join(this.runDir, 'workers', workerId);
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    }
    writeJson(relativePath, value) {
        const target = path.join(this.runDir, relativePath);
        fs.mkdirSync(path.dirname(target), { recursive: true });
        fs.writeFileSync(target, JSON.stringify(value, null, 2), 'utf-8');
        this.index[relativePath] = target;
        return target;
    }
    writeText(relativePath, value) {
        const target = path.join(this.runDir, relativePath);
        fs.mkdirSync(path.dirname(target), { recursive: true });
        fs.writeFileSync(target, value, 'utf-8');
        this.index[relativePath] = target;
        return target;
    }
}
// ─── WorktreeSession ──────────────────────────────────────────────────────────
/**
 * Manages the lifecycle of a single git worktree for one worker.
 * Handles creation, command execution, file mutations, diff capture,
 * patch application, and cleanup.
 */
export class WorktreeSession {
    repoRoot;
    workerId;
    role;
    recorder;
    reportHealth;
    worktreeDir;
    constructor(repoRoot, workerId, role, recorder, reportHealth) {
        this.repoRoot = repoRoot;
        this.workerId = workerId;
        this.role = role;
        this.recorder = recorder;
        this.reportHealth = reportHealth;
        this.worktreeDir = path.join(os.tmpdir(), 'nexus-prime-worktrees', `${role}-${workerId}`);
    }
    async create() {
        fs.mkdirSync(path.dirname(this.worktreeDir), { recursive: true });
        const health = await doctorGitWorktrees(this.repoRoot);
        this.reportHealth?.(health);
        try {
            await exec(`git worktree add --detach ${quote(this.worktreeDir)}`, {
                cwd: this.repoRoot,
                maxBuffer: 1024 * 1024 * 20,
            });
            await this.linkWorkspaceDependencies();
        }
        catch (error) {
            const retryHealth = await doctorGitWorktrees(this.repoRoot);
            this.reportHealth?.(retryHealth);
            throw new WorktreeDoctorError(`Unable to prepare ${this.role} worktree. ${summarizeWorktreeHealth(retryHealth)}`, retryHealth, toWorktreeRemediation(error, this.worktreeDir));
        }
    }
    async run(command, allowFailure = false, timeoutMs) {
        return runCommand(this.worktreeDir, command, allowFailure, timeoutMs);
    }
    async applyBindings(bindings, timeoutMs) {
        const modified = new Set();
        const commandRecords = [];
        for (const binding of bindings) {
            switch (binding.type) {
                case 'write_file': {
                    const filePath = this.resolve(binding.path || '');
                    await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
                    await fs.promises.writeFile(filePath, binding.content ?? '', 'utf-8');
                    modified.add(relativeTo(this.worktreeDir, filePath));
                    break;
                }
                case 'append_file': {
                    const filePath = this.resolve(binding.path || '');
                    await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
                    await fs.promises.appendFile(filePath, binding.content ?? '', 'utf-8');
                    modified.add(relativeTo(this.worktreeDir, filePath));
                    break;
                }
                case 'replace_text': {
                    const filePath = this.resolve(binding.path || '');
                    const existing = await fs.promises.readFile(filePath, 'utf-8').catch(() => '');
                    const updated = existing.replace(binding.search ?? '', binding.replace ?? '');
                    await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
                    await fs.promises.writeFile(filePath, updated, 'utf-8');
                    modified.add(relativeTo(this.worktreeDir, filePath));
                    break;
                }
                case 'run_command': {
                    const record = await this.run(binding.command || '', false, timeoutMs);
                    commandRecords.push(record);
                    this.recorder.writeJson(path.join('workers', this.workerId, `${sanitizeFileName(record.command)}.json`), record);
                    break;
                }
            }
        }
        return { modifiedFiles: [...modified], commandRecords };
    }
    async captureDiff() {
        try {
            await exec('git add -A', { cwd: this.worktreeDir, maxBuffer: 1024 * 1024 * 20 });
            try {
                await exec('git reset HEAD -- .agent', { cwd: this.worktreeDir, maxBuffer: 1024 * 1024 * 20 });
            }
            catch {
                // Runtime skill overlays stay outside repo patches.
            }
            try {
                await exec('git reset HEAD -- node_modules', { cwd: this.worktreeDir, maxBuffer: 1024 * 1024 * 20 });
            }
            catch {
                // Shared dependency symlink stays outside repo patches.
            }
            const { stdout } = await exec('git diff --binary --cached HEAD', {
                cwd: this.worktreeDir,
                maxBuffer: 1024 * 1024 * 20,
            });
            return stdout;
        }
        catch {
            return '';
        }
    }
    async applyPatchContent(diff) {
        if (!diff.trim())
            return;
        const patchPath = path.join(this.recorder.workerDir(this.workerId), `${this.role}.patch`);
        await fs.promises.writeFile(patchPath, diff, 'utf-8');
        await exec(`git apply --whitespace=nowarn ${quote(patchPath)}`, {
            cwd: this.worktreeDir,
            maxBuffer: 1024 * 1024 * 20,
        });
    }
    async cleanup() {
        try {
            await exec(`git worktree remove ${quote(this.worktreeDir)} --force`, {
                cwd: this.repoRoot,
                maxBuffer: 1024 * 1024 * 20,
            });
        }
        catch {
            const health = await doctorGitWorktrees(this.repoRoot);
            this.reportHealth?.(health);
        }
    }
    resolve(target) {
        if (path.isAbsolute(target))
            return target;
        return path.join(this.worktreeDir, target);
    }
    async linkWorkspaceDependencies() {
        const source = path.join(this.repoRoot, 'node_modules');
        const target = path.join(this.worktreeDir, 'node_modules');
        try {
            const stat = await fs.promises.lstat(source);
            if (!stat.isDirectory() && !stat.isSymbolicLink())
                return;
            await fs.promises.lstat(target).then(() => undefined).catch(async (error) => {
                if (error?.code !== 'ENOENT')
                    throw error;
                await fs.promises.symlink(source, target, 'dir');
            });
        }
        catch {
            // Worktrees remain valid for repos that do not use node_modules or have not installed deps.
        }
    }
}
// ─── MergeMemoryAdapter ───────────────────────────────────────────────────────
/** Thin adapter that wraps a MemoryBackend for use inside MergeOracle. */
export class MergeMemoryAdapter {
    backend;
    constructor(backend) {
        this.backend = backend;
    }
    async store(content, priority, tags) {
        return Promise.resolve(this.backend.store(content, priority, tags));
    }
}
// ─── MultiTierConsensusPolicy ─────────────────────────────────────────────────
/**
 * Two-level Byzantine consensus: run-level (per-execution) and global
 * (cross-execution promotion). Uses MergeOracle for the final merge decision.
 */
export class MultiTierConsensusPolicy {
    descriptor = {
        kind: 'multi-tier-byzantine',
        mode: 'default',
    };
    localOracle;
    runConsensus = new ByzantineConsensus();
    globalConsensus = new ByzantineConsensus();
    constructor(memory) {
        this.localOracle = new MergeOracle(new MergeMemoryAdapter(memory));
    }
    registerAgents(agentIds) {
        agentIds.forEach(id => {
            this.runConsensus.registerAgent(id);
            this.globalConsensus.registerAgent(id);
        });
    }
    async merge(results) {
        return this.localOracle.merge(results);
    }
    approveRunLevelChange(agentIds, delta = [1, 1, 1]) {
        if (agentIds.length === 0)
            return false;
        this.registerAgents(agentIds);
        const result = this.runConsensus.autoConsensus(agentIds[0], 0, delta);
        return result.accepted;
    }
    approveGlobalPromotion(agentIds, delta = [1, 1, 1]) {
        if (agentIds.length === 0)
            return false;
        agentIds.forEach(id => this.globalConsensus.registerAgent(id));
        const result = this.globalConsensus.autoConsensus(agentIds[0], 1, delta);
        return result.accepted;
    }
    shadowStats() {
        return {
            runLevel: this.runConsensus.getStats(),
            global: this.globalConsensus.getStats(),
        };
    }
}
