/**
 * Executes a single workflow step through the 3-phase model.
 *
 * Phase 1: Main agent execution (with tools)
 * Phase 2: Report output (Write-only, optional)
 * Phase 3: Status judgment (no tools, optional)
 */
import { existsSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { executeAgent } from '../../../agents/agent-usecases.js';
import { InstructionBuilder } from '../instruction/InstructionBuilder.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.js';
import { buildSessionKey } from '../session-key.js';
import { incrementStepIteration, getPreviousOutput } from './state-manager.js';
import { createLogger, getErrorMessage, slugify } from '../../../shared/utils/index.js';
import { waitForStepDelay } from './step-delay.js';
import { parseLastJsonBlock } from '../../../agents/structured-caller/shared.js';
import { assertProviderResolvedForCapabilitySensitiveOptions, } from './engine-provider-options.js';
import { validateStructuredOutputAgainstSchema } from './structured-output-schema-validator.js';
import { providerSupportsStructuredOutput } from '../../../infra/providers/provider-capabilities.js';
import { resolveReportHandles } from '../instruction/report-handles.js';
import { AGENT_FAILURE_CATEGORIES } from '../../../shared/types/agent-failure.js';
import { buildPhaseExecutionId } from '../../../shared/utils/phaseExecutionId.js';
import { runWithPhaseSpan } from '../observability/workflowSpans.js';
const log = createLogger('step-executor');
export class StepExecutor {
    deps;
    structuredOutputNormalizers;
    constructor(deps) {
        this.deps = deps;
        this.structuredOutputNormalizers = deps.structuredOutputNormalizers;
    }
    static buildTimestamp() {
        return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
    }
    static buildSnapshotFileName(stepName, stepIteration, timestamp) {
        const safeStepName = slugify(stepName) || 'step';
        return `${safeStepName}.${stepIteration}.${timestamp}.md`;
    }
    writeSnapshot(content, directoryRel, filename) {
        const absPath = join(this.deps.getCwd(), directoryRel, filename);
        writeFileSync(absPath, content, 'utf-8');
        return `${directoryRel}/${filename}`;
    }
    writeFacetSnapshot(facet, stepName, stepIteration, contents) {
        if (!contents || contents.length === 0)
            return undefined;
        const merged = contents.join('\n\n---\n\n');
        const timestamp = StepExecutor.buildTimestamp();
        const runPaths = this.deps.getRunPaths();
        const directoryRel = facet === 'knowledge'
            ? runPaths.contextKnowledgeRel
            : runPaths.contextPolicyRel;
        const sourcePath = this.writeSnapshot(merged, directoryRel, StepExecutor.buildSnapshotFileName(stepName, stepIteration, timestamp));
        return { content: [merged], sourcePath };
    }
    ensurePreviousResponseSnapshot(state, stepName, stepIteration) {
        if (!state.lastOutput || state.previousResponseSourcePath)
            return;
        const timestamp = StepExecutor.buildTimestamp();
        const runPaths = this.deps.getRunPaths();
        const fileName = StepExecutor.buildSnapshotFileName(stepName, stepIteration, timestamp);
        const sourcePath = this.writeSnapshot(state.lastOutput.content, runPaths.contextPreviousResponsesRel, fileName);
        this.writeSnapshot(state.lastOutput.content, runPaths.contextPreviousResponsesRel, 'latest.md');
        state.previousResponseSourcePath = sourcePath;
    }
    persistPreviousResponseSnapshot(state, stepName, stepIteration, content) {
        const timestamp = StepExecutor.buildTimestamp();
        const runPaths = this.deps.getRunPaths();
        const fileName = StepExecutor.buildSnapshotFileName(stepName, stepIteration, timestamp);
        const sourcePath = this.writeSnapshot(content, runPaths.contextPreviousResponsesRel, fileName);
        this.writeSnapshot(content, runPaths.contextPreviousResponsesRel, 'latest.md');
        state.previousResponseSourcePath = sourcePath;
    }
    buildPhase1Instruction(instruction, step, runtime) {
        const provider = this.deps.optionsBuilder.resolveStepProviderModel(step, runtime).provider;
        assertProviderResolvedForCapabilitySensitiveOptions(provider, {
            stepName: step.name,
            usesStructuredOutput: step.structuredOutput !== undefined,
        });
        const supportsStructuredOutput = providerSupportsStructuredOutput(provider);
        if (!step.structuredOutput || supportsStructuredOutput !== false) {
            return instruction;
        }
        return [
            instruction,
            '',
            'Return exactly one fenced JSON block that matches this JSON schema:',
            '```json',
            JSON.stringify(step.structuredOutput.schema, null, 2),
            '```',
            'Do not include any text before or after the JSON block.',
        ].join('\n');
    }
    normalizeStructuredOutput(step, response, runtime) {
        if (!step.structuredOutput) {
            return response;
        }
        const provider = this.deps.optionsBuilder.resolveStepProviderModel(step, runtime).provider;
        assertProviderResolvedForCapabilitySensitiveOptions(provider, {
            stepName: step.name,
            usesStructuredOutput: true,
        });
        const supportsStructuredOutput = providerSupportsStructuredOutput(provider);
        if (response.status !== 'done') {
            const detail = response.error ?? response.content;
            const failureReason = this.resolveStructuredOutputFailureReason(response);
            const fallback = this.buildStructuredOutputFailureFallback(step, response, failureReason, detail);
            if (fallback) {
                return fallback;
            }
            this.logStructuredOutputFailure(step, failureReason, detail);
            return response;
        }
        try {
            let structuredOutput = response.structuredOutput;
            if (structuredOutput === undefined) {
                if (supportsStructuredOutput !== false) {
                    throw new Error('Structured output response is missing');
                }
                const parsed = parseLastJsonBlock(response.content);
                if (typeof parsed !== 'object' || parsed == null || Array.isArray(parsed)) {
                    throw new Error('Structured output JSON must be an object');
                }
                structuredOutput = parsed;
            }
            validateStructuredOutputAgainstSchema(structuredOutput, step.structuredOutput.schema);
            structuredOutput = this.structuredOutputNormalizers.normalize(structuredOutput, {
                step,
                language: this.deps.getLanguage(),
            });
            if (structuredOutput === response.structuredOutput) {
                return response;
            }
            return {
                ...response,
                structuredOutput,
            };
        }
        catch (error) {
            const detail = getErrorMessage(error);
            const fallback = this.buildStructuredOutputFailureFallback(step, response, supportsStructuredOutput !== false && response.structuredOutput === undefined ? 'missing' : 'schema_error', detail);
            if (fallback) {
                return fallback;
            }
            this.logStructuredOutputFailure(step, supportsStructuredOutput !== false && response.structuredOutput === undefined ? 'missing' : 'schema_error', detail);
            throw new Error(`Step "${step.name}" requires structured_output for provider "${provider}": ${detail}`);
        }
    }
    buildStructuredOutputFailureFallback(step, response, failureReason, detail) {
        const structuredOutputConfig = step.structuredOutput;
        if (structuredOutputConfig === undefined) {
            return undefined;
        }
        return this.structuredOutputNormalizers.buildFailureFallback({
            step,
            response,
            failureReason,
            detail,
            language: this.deps.getLanguage(),
            validate: (value) => validateStructuredOutputAgainstSchema(value, structuredOutputConfig.schema),
        });
    }
    resolveStructuredOutputFailureReason(response) {
        if (response.failureCategory === AGENT_FAILURE_CATEGORIES.STREAM_IDLE_TIMEOUT
            || response.failureCategory === AGENT_FAILURE_CATEGORIES.PART_TIMEOUT) {
            return 'timeout';
        }
        if (response.status === 'error') {
            return 'provider_error';
        }
        return response.structuredOutput === undefined ? 'missing' : 'schema_error';
    }
    logStructuredOutputFailure(step, failureReason, detail) {
        log.info('Structured output failed', {
            step: step.name,
            used_structured_output: false,
            structured_output_failure_reason: failureReason,
            error: detail,
        });
    }
    /** Build Phase 1 instruction from template */
    buildInstruction(step, stepIteration, state, task, maxSteps, fallbackContext) {
        this.ensurePreviousResponseSnapshot(state, step.name, stepIteration);
        const policySnapshot = this.writeFacetSnapshot('policy', step.name, stepIteration, step.policyContents);
        const knowledgeSnapshot = this.writeFacetSnapshot('knowledge', step.name, stepIteration, step.knowledgeContents);
        const workflowSteps = this.deps.getWorkflowSteps();
        const workflowDefinitionSteps = this.deps.getWorkflowDefinitionSteps();
        const reportDir = join(this.deps.getCwd(), this.deps.getReportDir());
        const reportHandles = resolveReportHandles({
            step,
            reportDir,
            workflowSteps: workflowDefinitionSteps,
        });
        const instruction = new InstructionBuilder(step, {
            task,
            iteration: state.iteration,
            maxSteps,
            stepIteration,
            cwd: this.deps.getCwd(),
            projectCwd: this.deps.getProjectCwd(),
            userInputs: state.userInputs,
            previousOutput: getPreviousOutput(state),
            reportDir,
            currentReport: reportHandles.currentReport,
            previousReport: reportHandles.previousReport,
            reportHistory: reportHandles.reportHistory,
            peerReports: reportHandles.peerReports,
            language: this.deps.getLanguage(),
            interactive: this.deps.getInteractive(),
            workflowSteps,
            currentStepIndex: workflowSteps.findIndex(s => s.name === step.name),
            workflowName: this.deps.getWorkflowName(),
            workflowDescription: this.deps.getWorkflowDescription(),
            retryNote: this.deps.getRetryNote(),
            policyContents: policySnapshot?.content ?? step.policyContents,
            policySourcePath: policySnapshot?.sourcePath,
            knowledgeContents: knowledgeSnapshot?.content ?? step.knowledgeContents,
            knowledgeSourcePath: knowledgeSnapshot?.sourcePath,
            previousResponseSourcePath: state.previousResponseSourcePath,
            fallbackContext: fallbackContext ?? state.pendingFallback,
            workflowState: state,
        }).build();
        if (fallbackContext === undefined) {
            state.pendingFallback = undefined;
        }
        return instruction;
    }
    /**
     * Apply shared post-execution phases (Phase 2/3 + fallback rule evaluation).
     *
     * This method is intentionally reusable by non-normal step runners
     * (e.g., team_leader) so rule/report behavior stays consistent.
     */
    async applyPostExecutionPhases(step, state, stepIteration, response, updatePersonaSession, runtime) {
        let nextResponse = response;
        if (nextResponse.status === 'error' || nextResponse.status === 'blocked' || nextResponse.status === 'rate_limited') {
            return nextResponse;
        }
        const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, nextResponse.content, updatePersonaSession, this.deps.onPhaseStart, this.deps.onPhaseComplete, this.deps.onJudgeStage, state.iteration, runtime);
        // Phase 2: report output (resume same session, Write only)
        // Report generation is only valid after a completed Phase 1 response.
        if (nextResponse.status === 'done' && step.outputContracts && step.outputContracts.length > 0) {
            const reportResult = await runReportPhase(step, stepIteration, phaseCtx);
            if (reportResult && 'blocked' in reportResult) {
                nextResponse = { ...nextResponse, status: 'blocked', content: reportResult.response.content };
                return nextResponse;
            }
            if (reportResult && 'rateLimited' in reportResult) {
                return {
                    ...reportResult.response,
                    persona: step.name,
                };
            }
        }
        if (nextResponse.structuredOutput) {
            state.structuredOutputs.set(step.name, nextResponse.structuredOutput);
        }
        // Phase 3: status judgment (new session, no tools, determines matched rule)
        let phase3Result;
        try {
            phase3Result = needsStatusJudgmentPhase(step)
                ? await runStatusJudgmentPhase(step, phaseCtx)
                : undefined;
        }
        catch (error) {
            log.info('Phase 3 status judgment failed, falling back to phase1 rule evaluation', {
                step: step.name,
                error: getErrorMessage(error),
            });
        }
        if (phase3Result) {
            log.debug('Rule matched (Phase 3)', {
                step: step.name,
                ruleIndex: phase3Result.ruleIndex,
                method: phase3Result.method,
            });
            nextResponse = {
                ...nextResponse,
                matchedRuleIndex: phase3Result.ruleIndex,
                matchedRuleMethod: phase3Result.method,
            };
            return nextResponse;
        }
        // No Phase 3 — use rule evaluator with Phase 1 content
        const stepProviderModel = this.deps.optionsBuilder.resolveStepProviderModel(step, runtime);
        const match = await detectMatchedRule(step, nextResponse.content, '', {
            state,
            cwd: this.deps.getCwd(),
            provider: stepProviderModel.provider,
            resolvedProvider: stepProviderModel.provider,
            resolvedModel: stepProviderModel.model,
            interactive: this.deps.getInteractive(),
            detectRuleIndex: this.deps.detectRuleIndex,
            structuredCaller: this.deps.structuredCaller,
        });
        if (match) {
            log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method });
            nextResponse = {
                ...nextResponse,
                matchedRuleIndex: match.index,
                matchedRuleMethod: match.method,
            };
        }
        return nextResponse;
    }
    /**
     * Execute a normal (non-parallel) step through all 3 phases.
     *
     * Returns the final response (with matchedRuleIndex if a rule matched)
     * and the instruction used for Phase 1.
     */
    async runNormalStep(step, state, task, maxSteps, updatePersonaSession, prebuiltInstruction, runtime) {
        await waitForStepDelay(step);
        const stepIteration = prebuiltInstruction
            ? state.stepIterations.get(step.name) ?? 1
            : incrementStepIteration(state, step.name);
        const instruction = prebuiltInstruction ?? this.buildInstruction(step, stepIteration, state, task, maxSteps);
        const phase1Instruction = this.buildPhase1Instruction(instruction, step, runtime);
        const sessionKey = buildSessionKey(step, runtime?.providerInfo?.provider);
        log.debug('Running step', {
            step: step.name,
            persona: step.persona ?? '(none)',
            stepIteration,
            iteration: state.iteration,
            sessionId: state.personaSessions.get(sessionKey) ?? 'new',
        });
        // Phase 1: main execution (Write excluded if step has report)
        let didEmitPhaseStart = false;
        let resolvedPromptParts;
        const phaseExecutionId = buildPhaseExecutionId({
            step: step.name,
            iteration: state.iteration,
            phase: 1,
            sequence: 1,
        });
        const providerInfo = this.deps.optionsBuilder.resolveStepProviderModel(step, runtime);
        const baseAgentOptions = this.deps.optionsBuilder.buildAgentOptions(step, runtime);
        const agentOptions = {
            ...baseAgentOptions,
            onPromptResolved: (promptParts) => {
                resolvedPromptParts = promptParts;
                this.deps.onPhaseStart?.(step, 1, 'execute', phase1Instruction, promptParts, phaseExecutionId, state.iteration);
                didEmitPhaseStart = true;
            },
        };
        let response = await runWithPhaseSpan({
            enabled: this.deps.observabilityEnabled?.() === true,
            runId: this.deps.getObservabilityRunId?.(),
            workflowName: this.deps.getWorkflowName(),
            step,
            iteration: state.iteration,
            phase: 1,
            phaseName: 'execute',
            instruction: phase1Instruction,
            phaseExecutionId,
            workflowStack: this.deps.getCurrentWorkflowStack?.(),
            sanitizeText: this.deps.sanitizeObservabilityText,
            providerInfo,
            getPromptParts: () => resolvedPromptParts,
        }, () => executeAgent(step.persona, phase1Instruction, agentOptions), (result) => ({
            status: result.status,
            content: result.content,
            error: result.error,
        }));
        response = this.normalizeStructuredOutput(step, response, runtime);
        if (!didEmitPhaseStart) {
            throw new Error(`Missing prompt parts for phase start: ${step.name}:1`);
        }
        updatePersonaSession(sessionKey, response.sessionId);
        this.deps.onPhaseComplete?.(step, 1, 'execute', response.content, response.status, response.error, phaseExecutionId, state.iteration);
        // Provider failures should abort immediately.
        if (response.status === 'error' || response.status === 'rate_limited') {
            state.stepOutputs.set(step.name, response);
            state.lastOutput = response;
            return { response, instruction: phase1Instruction, providerInfo };
        }
        // Blocked responses should be handled by WorkflowEngine's blocked flow.
        // Persist snapshot so re-execution receives the latest blocked context.
        if (response.status === 'blocked') {
            state.stepOutputs.set(step.name, response);
            state.lastOutput = response;
            this.persistPreviousResponseSnapshot(state, step.name, stepIteration, response.content);
            return { response, instruction: phase1Instruction, providerInfo };
        }
        response = await this.applyPostExecutionPhases(step, state, stepIteration, response, updatePersonaSession, runtime);
        state.stepOutputs.set(step.name, response);
        state.lastOutput = response;
        if (response.status === 'rate_limited') {
            return { response, instruction: phase1Instruction, providerInfo };
        }
        this.persistPreviousResponseSnapshot(state, step.name, stepIteration, response.content);
        this.emitStepReports(step);
        return { response, instruction: phase1Instruction, providerInfo };
    }
    /** Collect step:report events for each report file that exists */
    emitStepReports(step) {
        if (!step.outputContracts || step.outputContracts.length === 0)
            return;
        const baseDir = join(this.deps.getCwd(), this.deps.getReportDir());
        for (const entry of step.outputContracts) {
            const fileName = entry.name;
            this.checkReportFile(step, baseDir, fileName);
        }
    }
    // Collects report file paths that exist (used by WorkflowEngine to emit events)
    reportFiles = [];
    /** Check if report file exists and collect for emission */
    checkReportFile(step, baseDir, fileName) {
        const filePath = join(baseDir, fileName);
        if (existsSync(filePath)) {
            this.reportFiles.push({ step, filePath, fileName });
        }
    }
    /** Drain collected report files (called by engine after step execution) */
    drainReportFiles() {
        const files = this.reportFiles;
        this.reportFiles = [];
        return files;
    }
}
//# sourceMappingURL=StepExecutor.js.map