import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { AgentMessageBus } from './message-bus.js';
import {} from './completion-report.js';
import { buildGateFailureTask, buildFixTask, buildReviewTask, parseEngineerCompletionReport, parseReviewerCompletionReport, } from './wrfc-reporting.js';
import { WrfcWorkmap } from './wrfc-workmap.js';
import { AgentWorktree } from './worktree.js';
import { completePlanItemsForAgent } from './wrfc-plan-sync.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
import { emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, } from './wrfc-config.js';
import { completeWrfcOrchestrationNode, createWrfcWorkflowContext, emitWrfcAutoCommitted, emitWrfcCascadeAbort, emitWrfcChainCreated, emitWrfcChainPassed, emitWrfcGraphCreated, emitWrfcStateChanged, failWrfcOrchestrationNode, startWrfcOrchestrationNode, } from './wrfc-runtime-events.js';
import { runWrfcGateChecks } from './wrfc-gate-runtime.js';
export { extractScoreFromText, extractPassedFromText, extractIssuesFromText } from './wrfc-reporting.js';
const VALID_TRANSITIONS = {
    pending: ['engineering'],
    engineering: ['reviewing', 'failed'],
    reviewing: ['fixing', 'awaiting_gates', 'failed'],
    fixing: ['reviewing', 'failed'],
    awaiting_gates: ['gating', 'failed'],
    gating: ['passed', 'failed', 'committing'],
    committing: ['passed', 'failed'],
};
const MAX_ACTIVE_CHAINS = 6;
const CHAIN_CLEANUP_DELAY_MS = 60_000;
export class WrfcController {
    chains = new Map();
    chainQueue = [];
    unsubscribers = [];
    activeChainCount = 0;
    pendingParentChainIds = new Map();
    sessionId;
    workmap;
    projectRoot;
    runtimeBus;
    messageBus;
    planManager = null;
    agentManager;
    configManager;
    createWorktree;
    constructor(runtimeBus, messageBus, deps) {
        this.runtimeBus = runtimeBus;
        this.messageBus = messageBus;
        this.agentManager = deps.agentManager;
        this.configManager = deps.configManager;
        this.projectRoot = deps.projectRoot;
        this.createWorktree = deps.createWorktree ?? (() => new AgentWorktree(this.projectRoot));
        this.sessionId = crypto.randomUUID().slice(0, 8);
        this.workmap = new WrfcWorkmap(this.projectRoot, this.sessionId, { surfaceRoot: deps.surfaceRoot });
        this.setupListeners();
    }
    createChain(engineerRecord) {
        logger.info('WrfcController.createChain: called', {
            agentId: engineerRecord.id,
            task: engineerRecord.task.slice(0, 60),
            activeChainCount: this.activeChainCount,
        });
        const chain = this.createBaseChain(engineerRecord);
        if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
            this.chainQueue.push({ record: engineerRecord, queuedAt: Date.now() });
            logger.debug('WrfcController.createChain: at cap, queued', {
                chainId: chain.id,
                agentId: engineerRecord.id,
                activeCount: this.activeChainCount,
                queueLength: this.chainQueue.length,
            });
            emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
            return chain;
        }
        this.startEngineeringChain(chain, true);
        logger.debug('WrfcController.createChain', { chainId: chain.id, agentId: engineerRecord.id });
        return chain;
    }
    getSessionId() { return this.sessionId; }
    getWorkmap() { return this.workmap; }
    setPlanManager(planManager) {
        this.planManager = planManager;
    }
    setRuntimeBus(runtimeBus) {
        for (const unsub of this.unsubscribers)
            unsub();
        this.unsubscribers = [];
        this.runtimeBus = runtimeBus;
        this.setupListeners();
    }
    getChain(chainId) { return this.chains.get(chainId) ?? null; }
    listChains() { return Array.from(this.chains.values()); }
    dispose() {
        for (const unsub of this.unsubscribers)
            unsub();
        this.unsubscribers = [];
    }
    transition(chain, to) {
        const allowed = VALID_TRANSITIONS[chain.state];
        if (!allowed || !allowed.includes(to)) {
            logger.error('WrfcController: illegal state transition', {
                chainId: chain.id,
                from: chain.state,
                to,
            });
            throw new Error(`Illegal WRFC transition: ${chain.state} -> ${to} for chain ${chain.id}`);
        }
        const from = chain.state;
        chain.state = to;
        emitWrfcStateChanged(this.runtimeBus, this.sessionId, chain.id, from, to);
        logger.debug('WrfcController.transition', { chainId: chain.id, from, to });
    }
    setupListeners() {
        const unsubComplete = this.runtimeBus.on('AGENT_COMPLETED', ({ payload }) => {
            this.onAgentComplete(payload.agentId).catch((error) => {
                logger.error('WrfcController.onAgentComplete unhandled error', {
                    agentId: payload.agentId,
                    error: summarizeError(error),
                });
            });
        });
        const unsubError = this.runtimeBus.on('AGENT_FAILED', ({ payload }) => {
            this.onAgentFailed(payload.agentId, payload.error);
        });
        this.unsubscribers.push(unsubComplete, unsubError);
    }
    async onAgentComplete(agentId) {
        const chain = this.findChainByAgentId(agentId);
        if (!chain)
            return;
        const record = this.agentManager.getStatus(agentId);
        const rawOutput = record?.fullOutput ?? '';
        logger.debug('WrfcController.onAgentComplete', {
            chainId: chain.id,
            agentId,
            state: chain.state,
            outputLength: rawOutput.length,
        });
        if (chain.state === 'pending') {
            chain.bufferedCompletion = { agentId, fullOutput: rawOutput };
            logger.debug('WrfcController.onAgentComplete: chain pending, buffering completion', {
                chainId: chain.id,
                agentId,
            });
            return;
        }
        if (chain.state === 'engineering' || chain.state === 'fixing') {
            const report = parseEngineerCompletionReport(rawOutput, record?.template);
            this.handleEngineerCompletion(chain, agentId, report);
        }
        else if (chain.state === 'reviewing') {
            const review = parseReviewerCompletionReport(chain.id, rawOutput, getWrfcScoreThreshold(this.configManager));
            chain.reviewerReport = review;
            chain.reviewCycles += 1;
            await this.processReview(chain, review);
        }
        if (this.planManager) {
            completePlanItemsForAgent(agentId, this.planManager);
        }
        if (chain.state === 'gating' || chain.state === 'passed' || chain.state === 'committing') {
            return;
        }
        await this.checkAndRunGatesForAll();
    }
    onAgentFailed(agentId, errorMessage) {
        const chain = this.findChainByAgentId(agentId);
        if (!chain)
            return;
        this.failChain(chain, errorMessage ?? `Agent ${agentId} failed`);
    }
    startReview(chain, report) {
        this.transition(chain, 'reviewing');
        const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', buildReviewTask(chain.id, report, getWrfcScoreThreshold(this.configManager)), true);
        chain.reviewerAgentId = reviewerRecord.id;
        chain.allAgentIds.push(reviewerRecord.id);
        reviewerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: reviewerRecord.id,
            role: 'reviewer',
            wrfcId: chain.id,
        });
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `review:${chain.reviewCycles + 1}`, 'reviewer', 'Reviewer assessment', reviewerRecord.id);
        logger.debug('WrfcController.startReview', {
            chainId: chain.id,
            reviewerAgentId: reviewerRecord.id,
        });
    }
    async processReview(chain, review) {
        const threshold = getWrfcScoreThreshold(this.configManager);
        this.completeCurrentNode(chain, `Score ${review.score}/10${review.passed ? ' passed' : ' needs fixes'}`);
        emitWorkflowReviewCompleted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
            chainId: chain.id,
            score: review.score,
            passed: review.passed,
        });
        this.workmap.append({
            ts: new Date().toISOString(),
            wrfcId: chain.id,
            event: 'review_complete',
            agentId: chain.reviewerAgentId,
            score: review.score,
            passed: review.score >= threshold,
            issues: review.issues?.slice(0, 10).map((issue) => ({
                severity: issue.severity,
                description: issue.description,
                file: issue.file,
            })),
        });
        logger.debug('WrfcController.processReview', {
            chainId: chain.id,
            score: review.score,
            threshold,
            fixAttempts: chain.fixAttempts,
        });
        chain.reviewScores.push(review.score);
        if (review.score >= threshold) {
            this.transition(chain, 'awaiting_gates');
            await this.checkAndRunGatesForAll();
            return;
        }
        const scores = chain.reviewScores;
        if (scores.length >= 3) {
            const initial = scores[0];
            const lastTwo = scores.slice(-2);
            if (lastTwo[0] < initial && lastTwo[1] < initial) {
                emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Score regression warning: initial ${initial}/10, last two ${lastTwo[0]}/10, ${lastTwo[1]}/10 — both below initial. Fix quality may be degrading.`);
            }
        }
        const maxFixAttempts = getWrfcMaxFixAttempts(this.configManager);
        if (chain.fixAttempts >= maxFixAttempts) {
            this.failChain(chain, `Score ${review.score}/10 below threshold ${threshold}/10 after ${chain.fixAttempts} fix attempt${chain.fixAttempts !== 1 ? 's' : ''} — below threshold`);
            return;
        }
        this.startFix(chain, review);
    }
    startFix(chain, review) {
        chain.fixAttempts += 1;
        this.transition(chain, 'fixing');
        const maxAttempts = getWrfcMaxFixAttempts(this.configManager);
        emitWorkflowFixAttempted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
            chainId: chain.id,
            attempt: chain.fixAttempts,
            maxAttempts,
        });
        const fixerRecord = this.spawnWrfcAgent(chain, 'engineer', buildFixTask(chain.id, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts), true);
        chain.fixerAgentId = fixerRecord.id;
        chain.allAgentIds.push(fixerRecord.id);
        fixerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: fixerRecord.id,
            role: 'fixer',
            wrfcId: chain.id,
        });
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `fix:${chain.fixAttempts}`, 'fixer', `Fix attempt ${chain.fixAttempts}`, fixerRecord.id);
        this.workmap.append({
            ts: new Date().toISOString(),
            wrfcId: chain.id,
            event: 'fix_started',
            agentId: fixerRecord.id,
            attempt: chain.fixAttempts,
        });
        logger.debug('WrfcController.startFix', {
            chainId: chain.id,
            fixerAgentId: fixerRecord.id,
            attempt: chain.fixAttempts,
        });
    }
    async runGates(chain) {
        this.transition(chain, 'gating');
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `gate:${chain.reviewCycles}:${chain.fixAttempts}`, 'verifier', 'Quality gates');
        return runWrfcGateChecks({
            configManager: this.configManager,
            projectRoot: this.projectRoot,
            runtimeBus: this.runtimeBus,
            sessionId: this.sessionId,
            chainId: chain.id,
            onResult: (results) => {
                chain.gateResults = results.slice();
            },
        });
    }
    async processGateResults(chain, results) {
        if (!chain.currentNodeId?.includes(':gate:')) {
            chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `gate:${chain.reviewCycles}:${chain.fixAttempts}`, 'verifier', 'Quality gates');
        }
        const allPassed = results.length === 0 || results.every((result) => result.passed);
        const autoCommit = getWrfcAutoCommit(this.configManager);
        for (const result of results) {
            this.workmap.append({
                ts: new Date().toISOString(),
                wrfcId: chain.id,
                event: 'gate_result',
                gate: result.gate,
                passed: result.passed,
                gateOutput: result.output.slice(0, 200),
            });
        }
        this.completeCurrentNode(chain, allPassed
            ? 'All quality gates passed'
            : `${results.filter((result) => !result.passed).length} quality gate(s) failed`);
        if (allPassed) {
            this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_passed' });
            chain.gatesPassed = true;
            if (autoCommit) {
                await this.autoCommit(chain);
            }
            else {
                this.completeChainAsPassed(chain);
            }
            return;
        }
        const failedGates = results.filter((result) => !result.passed);
        const fingerprint = failedGates.map((result) => `${result.gate}:${result.output.slice(0, 200)}`).join('|');
        const maxGateRetries = getWrfcMaxFixAttempts(this.configManager);
        chain.gateFailureFingerprint = fingerprint;
        this.completeChainAsPassed(chain);
        if (chain.gateRetryDepth >= maxGateRetries) {
            logger.error('WrfcController.processGateResults: gate retry limit reached, manual intervention required', {
                chainId: chain.id,
                gateRetryDepth: chain.gateRetryDepth,
                maxGateRetries,
            });
            emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.gateRetryDepth}/${maxGateRetries}). Manual intervention required.`);
            return;
        }
        const followUpTask = buildGateFailureTask(chain.id, chain.task, failedGates);
        const followUpRecord = this.spawnWrfcAgent(chain, 'engineer', followUpTask, false);
        const followUpChain = this.findChainByAgentId(followUpRecord.id);
        if (followUpChain) {
            followUpChain.parentChainId = chain.id;
        }
        else {
            this.pendingParentChainIds.set(followUpRecord.id, chain.id);
        }
        logger.debug('WrfcController.processGateResults: gate failure — spawned follow-up agent', {
            parentChainId: chain.id,
            followUpAgentId: followUpRecord.id,
        });
    }
    scheduleChainCleanup(chain) {
        setTimeout(() => {
            if (chain.state === 'passed' || chain.state === 'failed') {
                this.chains.delete(chain.id);
            }
        }, CHAIN_CLEANUP_DELAY_MS);
    }
    async checkAndRunGatesForAll() {
        const allChains = Array.from(this.chains.values()).filter((chain) => chain.state !== 'passed' && chain.state !== 'failed');
        const activeWorkChains = allChains.filter((chain) => (chain.state === 'pending'
            || chain.state === 'engineering'
            || chain.state === 'reviewing'
            || chain.state === 'fixing'));
        if (activeWorkChains.length > 0) {
            logger.debug('WrfcController.checkAndRunGatesForAll: waiting for active chains', {
                activeWork: activeWorkChains.length,
                awaitingGates: allChains.filter((chain) => chain.state === 'awaiting_gates').length,
            });
            return;
        }
        const readyChains = allChains.filter((chain) => chain.state === 'awaiting_gates');
        if (readyChains.length === 0)
            return;
        logger.debug('WrfcController.checkAndRunGatesForAll: all chains ready, running gates', {
            readyCount: readyChains.length,
        });
        const gateRunner = readyChains[0];
        const results = await this.runGates(gateRunner);
        for (const chain of readyChains) {
            if (chain.id !== gateRunner.id) {
                this.transition(chain, 'gating');
                chain.gateResults = results;
            }
            await this.processGateResults(chain, results);
        }
    }
    async autoCommit(chain) {
        this.transition(chain, 'committing');
        const agentId = chain.allAgentIds.length > 0
            ? chain.allAgentIds[chain.allAgentIds.length - 1]
            : (chain.fixerAgentId ?? chain.engineerAgentId);
        if (!agentId) {
            this.failChain(chain, 'autoCommit: no agent ID found on chain');
            return;
        }
        if (!existsSync(join(this.projectRoot, '.git'))) {
            logger.debug('WrfcController.autoCommit: not a git repo, skipping commit', { chainId: chain.id });
            this.completeChainAsPassed(chain);
            return;
        }
        const worktree = this.createWorktree();
        try {
            const merged = await worktree.merge(agentId);
            emitWrfcAutoCommitted(this.runtimeBus, this.sessionId, chain.id);
            this.completeChainAsPassed(chain);
            logger.debug('WrfcController.autoCommit: success', { chainId: chain.id, agentId, merged });
        }
        catch (error) {
            const reason = summarizeError(error);
            logger.error('WrfcController.autoCommit: failed', { chainId: chain.id, error: reason });
            this.failChain(chain, `autoCommit failed: ${reason}`);
        }
        finally {
            for (const id of chain.allAgentIds) {
                worktree.cleanup(id).catch((error) => {
                    logger.debug('WrfcController.autoCommit: cleanup error (non-fatal)', {
                        agentId: id,
                        error: summarizeError(error),
                    });
                });
            }
        }
    }
    failChain(chain, reason) {
        if (chain.state === 'pending') {
            this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.engineerAgentId);
        }
        const wasActive = chain.state !== 'passed' && chain.state !== 'failed' && chain.state !== 'pending';
        this.failCurrentNode(chain, reason);
        try {
            this.transition(chain, 'failed');
        }
        catch {
            chain.state = 'failed';
        }
        if (wasActive) {
            this.activeChainCount = Math.max(0, this.activeChainCount - 1);
        }
        chain.error = reason;
        chain.completedAt = Date.now();
        this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
        emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
        logger.error('WrfcController.failChain', { chainId: chain.id, reason });
        this.scheduleChainCleanup(chain);
        this.safeDequeueNext();
    }
    async dequeueNext() {
        if (this.chainQueue.length === 0 || this.activeChainCount >= MAX_ACTIVE_CHAINS)
            return;
        const queued = this.chainQueue.shift();
        const chain = this.chains.get(queued.record.wrfcId ?? '');
        if (!chain) {
            logger.warn('WrfcController.dequeueNext: queued chain not found, discarding', {
                agentId: queued.record.id,
            });
            return;
        }
        logger.debug('WrfcController.dequeueNext: starting queued chain', {
            chainId: chain.id,
            agentId: queued.record.id,
            waitedMs: Date.now() - queued.queuedAt,
        });
        this.startEngineeringChain(chain, false);
        if (!chain.bufferedCompletion)
            return;
        const buffered = chain.bufferedCompletion;
        chain.bufferedCompletion = undefined;
        await this.onAgentComplete(buffered.agentId);
    }
    findChainByAgentId(agentId) {
        for (const chain of this.chains.values()) {
            if (chain.allAgentIds.includes(agentId))
                return chain;
        }
        return null;
    }
    generateWrfcId() { return `wrfc-${crypto.randomUUID().slice(0, 8)}`; }
    completeCurrentNode(chain, summary) {
        if (!chain.currentNodeId)
            return;
        completeWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, summary);
        chain.currentNodeId = undefined;
    }
    failCurrentNode(chain, error) {
        if (!chain.currentNodeId)
            return;
        failWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, error);
        chain.currentNodeId = undefined;
    }
    createBaseChain(engineerRecord) {
        const chain = {
            id: this.generateWrfcId(),
            state: 'pending',
            task: engineerRecord.task,
            engineerAgentId: engineerRecord.id,
            allAgentIds: [engineerRecord.id],
            fixAttempts: 0,
            reviewCycles: 0,
            gateRetryDepth: 0,
            reviewScores: [],
            createdAt: Date.now(),
        };
        this.chains.set(chain.id, chain);
        emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${engineerRecord.task}`);
        engineerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: engineerRecord.id,
            template: engineerRecord.template,
            wrfcId: chain.id,
        });
        this.attachPendingParentChain(chain, engineerRecord.id);
        return chain;
    }
    attachPendingParentChain(chain, agentId) {
        const pendingParentId = this.pendingParentChainIds.get(agentId);
        if (!pendingParentId)
            return;
        chain.parentChainId = pendingParentId;
        const parent = this.chains.get(pendingParentId);
        if (parent) {
            chain.gateRetryDepth = parent.gateRetryDepth + (parent.gateFailureFingerprint ? 1 : 0);
        }
        this.pendingParentChainIds.delete(agentId);
    }
    startEngineeringChain(chain, emitCreated) {
        this.activeChainCount += 1;
        this.transition(chain, 'engineering');
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `engineer:${chain.fixAttempts}`, 'engineer', 'Engineer implementation', chain.engineerAgentId);
        if (emitCreated) {
            emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
        }
    }
    handleEngineerCompletion(chain, agentId, report) {
        this.completeCurrentNode(chain, report.summary);
        if (chain.state === 'engineering') {
            chain.engineerReport = report;
            this.workmap.append({
                ts: new Date().toISOString(),
                wrfcId: chain.id,
                event: 'engineer_complete',
                agentId,
                task: chain.task,
            });
        }
        this.startReview(chain, report);
    }
    spawnWrfcAgent(chain, template, task, dangerouslyDisableWrfc) {
        const sourceAgent = [chain.fixerAgentId, chain.engineerAgentId]
            .filter((value) => typeof value === 'string')
            .map((agentId) => this.agentManager.getStatus(agentId))
            .find((record) => record != null) ?? null;
        return this.agentManager.spawn({
            mode: 'spawn',
            task,
            template,
            ...(sourceAgent?.model ? { model: sourceAgent.model } : {}),
            ...(sourceAgent?.provider ? { provider: sourceAgent.provider } : {}),
            ...(sourceAgent?.fallbackModels?.length ? { fallbackModels: [...sourceAgent.fallbackModels] } : {}),
            ...(sourceAgent?.routing ? { routing: sourceAgent.routing } : {}),
            ...(sourceAgent?.reasoningEffort ? { reasoningEffort: sourceAgent.reasoningEffort } : {}),
            ...(dangerouslyDisableWrfc ? { dangerously_disable_wrfc: true } : {}),
        });
    }
    completeChainAsPassed(chain) {
        this.activeChainCount = Math.max(0, this.activeChainCount - 1);
        this.transition(chain, 'passed');
        chain.completedAt = Date.now();
        emitWrfcChainPassed(this.runtimeBus, this.sessionId, chain.id);
        this.scheduleChainCleanup(chain);
        this.safeDequeueNext();
    }
    safeDequeueNext() {
        this.dequeueNext().catch((error) => {
            logger.error('WrfcController.dequeueNext unhandled error', { error: summarizeError(error) });
        });
    }
}
