/**
 * Executes parallel workflow steps concurrently and aggregates results.
 *
 * When onStream is provided, uses ParallelLogger to prefix each
 * sub-step output with `[name]` for readable interleaved display.
 */
import { executeAgent } from '../../../agents/agent-usecases.js';
import { ParallelLogger } from './parallel-logger.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.js';
import { incrementStepIteration } from './state-manager.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { buildSessionKey } from '../session-key.js';
import { runWithPhaseSpan } from '../observability/workflowSpans.js';
import { buildPhaseExecutionId } from '../../../shared/utils/phaseExecutionId.js';
import { sanitizeSensitiveText } from '../../../shared/utils/sensitiveText.js';
const log = createLogger('parallel-runner');
/**
 * Simple semaphore for controlling concurrency.
 * Limits the number of concurrent async operations.
 * Same implementation as ArpeggioRunner's Semaphore.
 */
class Semaphore {
    maxConcurrency;
    running = 0;
    waiting = [];
    constructor(maxConcurrency) {
        this.maxConcurrency = maxConcurrency;
    }
    async acquire() {
        if (this.running < this.maxConcurrency) {
            this.running++;
            return;
        }
        return new Promise((resolve) => {
            this.waiting.push(resolve);
        });
    }
    release() {
        if (this.waiting.length > 0) {
            const next = this.waiting.shift();
            next();
        }
        else {
            this.running--;
        }
    }
}
export class ParallelRunner {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    /**
     * Run a parallel step: execute all sub-steps concurrently, then aggregate results.
     * The aggregated output becomes the parent step response for rules evaluation.
     */
    async runParallelStep(step, state, task, maxSteps, updatePersonaSession, runtime) {
        if (!step.parallel) {
            throw new Error(`Step "${step.name}" has no parallel sub-steps`);
        }
        const subSteps = step.parallel;
        const stepIteration = incrementStepIteration(state, step.name);
        log.debug('Running parallel step', {
            step: step.name,
            subSteps: subSteps.map(s => s.name),
            stepIteration,
        });
        // Create parallel logger for prefixed output (only when streaming is enabled)
        const parallelLogger = this.deps.engineOptions.onStream
            ? new ParallelLogger(this.buildParallelLoggerOptions(step.name, stepIteration, subSteps.map((s) => s.name), state.iteration, maxSteps))
            : undefined;
        const parentPm = runtime
            ? this.deps.optionsBuilder.resolveStepProviderModel(step, runtime)
            : this.deps.optionsBuilder.resolveStepProviderModel(step);
        const parentRuleCtx = {
            state,
            cwd: this.deps.getCwd(),
            provider: parentPm.provider,
            resolvedProvider: parentPm.provider,
            resolvedModel: parentPm.model,
            interactive: this.deps.getInteractive(),
            detectRuleIndex: this.deps.detectRuleIndex,
            structuredCaller: this.deps.structuredCaller,
        };
        // Create semaphore for concurrency control (if configured)
        const semaphore = step.concurrency != null
            ? new Semaphore(step.concurrency)
            : undefined;
        if (semaphore) {
            log.debug('Concurrency limit enabled', { step: step.name, concurrency: step.concurrency });
        }
        // Run all sub-steps concurrently (failures are captured, not thrown)
        // When semaphore is set, at most `concurrency` sub-steps execute simultaneously.
        const settled = await Promise.allSettled(subSteps.map(async (subStep, index) => {
            if (semaphore) {
                await semaphore.acquire();
            }
            try {
                const subIteration = incrementStepIteration(state, subStep.name);
                const subInstruction = this.deps.stepExecutor.buildInstruction(subStep, subIteration, state, task, maxSteps, runtime?.fallback);
                const parentIteration = state.iteration;
                const subPm = runtime
                    ? this.deps.optionsBuilder.resolveStepProviderModel(subStep, runtime)
                    : this.deps.optionsBuilder.resolveStepProviderModel(subStep);
                const subRuleCtx = {
                    ...parentRuleCtx,
                    provider: subPm.provider,
                    resolvedProvider: subPm.provider,
                    resolvedModel: subPm.model,
                };
                // Session key uses buildSessionKey (persona:provider) — same as normal steps.
                // This ensures sessions are shared across steps with the same persona+provider,
                // while different providers (e.g., claude-eye vs codex-eye) get separate sessions.
                const subSessionKey = buildSessionKey(subStep, runtime?.providerInfo?.provider);
                // Phase 1: main execution (Write excluded if sub-step has report)
                const baseOptions = this.deps.optionsBuilder.buildAgentOptions(subStep, runtime);
                let didEmitPhaseStart = false;
                let resolvedPromptParts;
                const phaseExecutionId = buildPhaseExecutionId({
                    step: subStep.name,
                    iteration: parentIteration,
                    phase: 1,
                    sequence: 1,
                });
                // Override onStream with parallel logger's prefixed handler (immutable)
                const agentOptions = parallelLogger
                    ? { ...baseOptions, onStream: parallelLogger.createStreamHandler(subStep.name, index) }
                    : { ...baseOptions };
                agentOptions.onPromptResolved = (promptParts) => {
                    resolvedPromptParts = promptParts;
                    this.deps.onPhaseStart?.(subStep, 1, 'execute', subInstruction, promptParts, phaseExecutionId, parentIteration);
                    didEmitPhaseStart = true;
                };
                const subResponse = await runWithPhaseSpan({
                    enabled: this.deps.observabilityEnabled,
                    runId: this.deps.observabilityRunId,
                    workflowName: this.deps.getWorkflowName(),
                    step: subStep,
                    iteration: parentIteration,
                    phase: 1,
                    phaseName: 'execute',
                    instruction: subInstruction,
                    phaseExecutionId,
                    workflowStack: this.deps.getCurrentWorkflowStack?.(),
                    sanitizeText: this.deps.sanitizeObservabilityText,
                    providerInfo: subPm,
                    getPromptParts: () => resolvedPromptParts,
                }, () => executeAgent(subStep.persona, subInstruction, agentOptions), (result) => ({
                    status: result.status,
                    content: result.content,
                    error: result.error,
                }));
                if (!didEmitPhaseStart) {
                    throw new Error(`Missing prompt parts for phase start: ${subStep.name}:1`);
                }
                updatePersonaSession(subSessionKey, subResponse.sessionId);
                this.deps.onPhaseComplete?.(subStep, 1, 'execute', subResponse.content, subResponse.status, subResponse.error, phaseExecutionId, parentIteration);
                if (subResponse.status === 'error' || subResponse.status === 'blocked' || subResponse.status === 'rate_limited') {
                    state.stepOutputs.set(subStep.name, subResponse);
                    return { subStep, response: subResponse, instruction: subInstruction, providerInfo: subPm };
                }
                // Phase 2/3 context resolves the same runtime-aware session key as Phase 1.
                const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, subResponse.content, updatePersonaSession, this.deps.onPhaseStart, this.deps.onPhaseComplete, this.deps.onJudgeStage, parentIteration, runtime);
                // Phase 2: report output for sub-step
                if (subStep.outputContracts && subStep.outputContracts.length > 0) {
                    const reportResult = await runReportPhase(subStep, subIteration, phaseCtx);
                    if (reportResult && 'blocked' in reportResult) {
                        const blockedResponse = {
                            ...subResponse,
                            status: 'blocked',
                            content: reportResult.response.content,
                        };
                        state.stepOutputs.set(subStep.name, blockedResponse);
                        return { subStep, response: blockedResponse, instruction: subInstruction, providerInfo: subPm };
                    }
                    if (reportResult && 'rateLimited' in reportResult) {
                        const rateLimitedResponse = {
                            ...reportResult.response,
                            persona: subStep.name,
                        };
                        state.stepOutputs.set(subStep.name, rateLimitedResponse);
                        return { subStep, response: rateLimitedResponse, instruction: subInstruction, providerInfo: subPm };
                    }
                }
                // Phase 3: status judgment for sub-step
                let subPhase3;
                try {
                    subPhase3 = needsStatusJudgmentPhase(subStep)
                        ? await runStatusJudgmentPhase(subStep, phaseCtx)
                        : undefined;
                }
                catch (error) {
                    log.info('Phase 3 status judgment failed for sub-step, falling back to phase1 rule evaluation', {
                        step: subStep.name,
                        error: getErrorMessage(error),
                    });
                }
                let finalResponse;
                if (subPhase3) {
                    finalResponse = { ...subResponse, matchedRuleIndex: subPhase3.ruleIndex, matchedRuleMethod: subPhase3.method };
                }
                else {
                    const match = await detectMatchedRule(subStep, subResponse.content, '', subRuleCtx);
                    finalResponse = match
                        ? { ...subResponse, matchedRuleIndex: match.index, matchedRuleMethod: match.method }
                        : subResponse;
                }
                const qualityGateResult = await this.deps.runQualityGates({
                    qualityGates: subStep.qualityGates,
                    projectRoot: this.deps.getCwd(),
                    step: subStep,
                });
                if (!qualityGateResult.ok) {
                    state.stepOutputs.set(subStep.name, qualityGateResult.response);
                    return {
                        subStep,
                        response: qualityGateResult.response,
                        instruction: subInstruction,
                        providerInfo: subPm,
                        qualityGateFailure: true,
                    };
                }
                state.stepOutputs.set(subStep.name, finalResponse);
                this.deps.stepExecutor.emitStepReports(subStep);
                return { subStep, response: finalResponse, instruction: subInstruction, providerInfo: subPm };
            }
            finally {
                if (semaphore) {
                    semaphore.release();
                }
            }
        }));
        // Map settled results: fulfilled → as-is, rejected → error AgentResponse
        const subResults = settled.map((result, index) => {
            if (result.status === 'fulfilled') {
                return result.value;
            }
            const failedStep = subSteps[index];
            const errorMsg = getErrorMessage(result.reason);
            log.error('Sub-step failed', { step: failedStep.name, error: sanitizeSensitiveText(errorMsg) });
            const errorResponse = {
                persona: failedStep.name,
                status: 'error',
                content: '',
                timestamp: new Date(),
                error: errorMsg,
            };
            state.stepOutputs.set(failedStep.name, errorResponse);
            return { subStep: failedStep, response: errorResponse, instruction: '', providerInfo: undefined };
        });
        const terminalResults = this.collectTerminalResults(subResults);
        const rateLimitedResult = terminalResults.find((r) => r.response.status === 'rate_limited');
        if (rateLimitedResult) {
            return this.createTerminalParentResult({
                step,
                state,
                stepIteration,
                subResults,
                terminalResults,
                status: 'rate_limited',
                providerInfo: rateLimitedResult.providerInfo ?? parentPm,
            });
        }
        const errorResults = terminalResults.filter((r) => r.response.status === 'error');
        if (errorResults.length > 0) {
            return this.createTerminalParentResult({
                step,
                state,
                stepIteration,
                subResults,
                terminalResults,
                status: 'error',
                providerInfo: errorResults[0]?.providerInfo ?? parentPm,
            });
        }
        const blockedResults = terminalResults.filter((r) => r.response.status === 'blocked');
        if (blockedResults.length > 0) {
            return this.createTerminalParentResult({
                step,
                state,
                stepIteration,
                subResults,
                terminalResults: blockedResults,
                status: 'blocked',
                providerInfo: blockedResults[0]?.providerInfo ?? parentPm,
            });
        }
        const qualityGateFailure = subResults.find((r) => ('qualityGateFailure' in r && r.qualityGateFailure === true));
        if (qualityGateFailure) {
            const failureResponse = {
                persona: step.name,
                status: 'done',
                content: [
                    `Parallel sub-step quality gate failed: ${qualityGateFailure.subStep.name}`,
                    '',
                    qualityGateFailure.response.content,
                ].join('\n'),
                timestamp: new Date(),
            };
            return {
                response: failureResponse,
                instruction: qualityGateFailure.instruction,
                providerInfo: qualityGateFailure.providerInfo ?? parentPm,
                qualityGateFailure: {
                    response: failureResponse,
                    stepIteration,
                },
            };
        }
        // Print completion summary
        if (parallelLogger) {
            parallelLogger.printSummary(step.name, subResults.map((r) => ({
                name: r.subStep.name,
                condition: r.response.matchedRuleIndex != null && r.subStep.rules
                    ? r.subStep.rules[r.response.matchedRuleIndex]?.condition
                    : undefined,
            })));
        }
        // Aggregate sub-step outputs into the parent step response
        const aggregatedContent = subResults
            .map((r) => `## ${r.subStep.name}\n${r.response.content}`)
            .join('\n\n---\n\n');
        const aggregatedInstruction = subResults
            .map((r) => r.instruction)
            .join('\n\n');
        // Parent step uses aggregate conditions, so tagContent is empty
        const match = await detectMatchedRule(step, aggregatedContent, '', parentRuleCtx);
        const aggregatedResponse = {
            persona: step.name,
            status: 'done',
            content: aggregatedContent,
            timestamp: new Date(),
            ...(match && { matchedRuleIndex: match.index, matchedRuleMethod: match.method }),
        };
        state.stepOutputs.set(step.name, aggregatedResponse);
        state.lastOutput = aggregatedResponse;
        this.deps.stepExecutor.persistPreviousResponseSnapshot(state, step.name, stepIteration, aggregatedResponse.content);
        this.deps.stepExecutor.emitStepReports(step);
        return { response: aggregatedResponse, instruction: aggregatedInstruction, providerInfo: parentPm };
    }
    buildParallelLoggerOptions(stepName, stepIteration, subStepNames, iteration, maxSteps) {
        const options = {
            subStepNames,
            parentOnStream: this.deps.engineOptions.onStream,
            progressInfo: {
                iteration,
                maxSteps,
            },
        };
        if (this.deps.engineOptions.taskPrefix != null && this.deps.engineOptions.taskColorIndex != null) {
            return {
                ...options,
                taskLabel: this.deps.engineOptions.taskPrefix,
                taskColorIndex: this.deps.engineOptions.taskColorIndex,
                parentStepName: stepName,
                stepIteration,
            };
        }
        return options;
    }
    createTerminalParentResult(options) {
        const content = this.buildTerminalDiagnostic(options.step, options.terminalResults, options.status);
        const failureCategory = this.firstFailureCategory(options.terminalResults);
        const response = {
            persona: options.step.name,
            status: options.status,
            content,
            timestamp: new Date(),
            ...(options.status === 'error' || options.status === 'rate_limited' ? { error: content } : {}),
            ...(failureCategory && { failureCategory }),
            ...this.firstRateLimitMetadata(options.terminalResults),
        };
        options.state.stepOutputs.set(options.step.name, response);
        options.state.lastOutput = response;
        if (options.status === 'blocked') {
            this.deps.stepExecutor.persistPreviousResponseSnapshot(options.state, options.step.name, options.stepIteration, response.content);
        }
        return {
            response,
            instruction: options.subResults.map((result) => result.instruction).join('\n\n'),
            providerInfo: options.providerInfo,
            consumedStepIterations: [
                options.step.name,
                ...options.subResults.map((result) => result.subStep.name),
            ],
        };
    }
    collectTerminalResults(results) {
        return results.filter((result) => (result.response.status === 'error'
            || result.response.status === 'blocked'
            || result.response.status === 'rate_limited'));
    }
    buildTerminalDiagnostic(step, terminalResults, status) {
        const detailLines = terminalResults.map((result) => {
            const failureCategory = result.response.failureCategory ?? 'none';
            const detail = sanitizeSensitiveText(result.response.error ?? result.response.content);
            const lines = [
                `- sub-step: ${result.subStep.name}`,
                `  status: ${result.response.status}`,
                `  failureCategory: ${failureCategory}`,
            ];
            if (result.response.rateLimitInfo) {
                lines.push(`  rateLimitInfo: provider=${result.response.rateLimitInfo.provider}, source=${result.response.rateLimitInfo.source}`);
            }
            lines.push(`  detail: ${detail}`);
            return lines.join('\n');
        });
        return [
            `Parallel step "${step.name}" returned ${status} because one or more sub-steps ended in a non-rule terminal status.`,
            'Aggregate rules were not evaluated as a normal review result because terminal sub-step statuses',
            'do not represent matched aggregate conditions such as all("approved") or any("needs_fix").',
            '',
            'Sub-step diagnostics:',
            ...detailLines,
        ].join('\n');
    }
    firstFailureCategory(results) {
        return results.find((result) => result.response.failureCategory)?.response.failureCategory;
    }
    firstRateLimitMetadata(results) {
        const rateLimitedResult = results.find((result) => result.response.status === 'rate_limited');
        if (!rateLimitedResult) {
            return {};
        }
        return {
            ...(rateLimitedResult.response.errorKind && { errorKind: rateLimitedResult.response.errorKind }),
            ...(rateLimitedResult.response.rateLimitInfo && { rateLimitInfo: rateLimitedResult.response.rateLimitInfo }),
        };
    }
}
//# sourceMappingURL=ParallelRunner.js.map