/**
 * Phase Timing Context
 *
 * Module-level singleton that provides phase timing recording
 * to executeNode() without changing its public API.
 *
 * The worker calls setPhaseTimingContext() once after DI init.
 * Node helpers call recordPhaseStart/End() during graph execution.
 * Errors are swallowed so timing never blocks graph execution.
 */
import { randomUUID } from 'node:crypto';
let contextRunId;
let contextRepository;
let lastTimingId = null;
/**
 * Get the last phase timing ID. Used by the worker to record approval wait start.
 */
export function getLastTimingId() {
    return lastTimingId;
}
/**
 * Set the phase timing context. Called once by the worker after DI init.
 */
export function setPhaseTimingContext(runId, repository) {
    contextRunId = runId;
    contextRepository = repository;
}
/**
 * Clear the phase timing context. Useful for testing.
 */
export function clearPhaseTimingContext() {
    contextRunId = undefined;
    contextRepository = undefined;
    lastTimingId = null;
}
/**
 * Compute the iteration-aware phase name.
 *
 * Queries existing PhaseTiming rows for the current run, counts how many
 * share the same base phase name, and appends `:N` for iterations > 1.
 *
 * Examples:
 *   - First "requirements" execution  → "requirements"
 *   - Second "requirements" execution → "requirements:2"
 *   - Third "requirements" execution  → "requirements:3"
 */
async function resolveIterationPhase(phase, runId, repository) {
    try {
        const existing = await repository.findByRunId(runId);
        // Count rows whose phase is the base name or matches the base:N pattern
        const count = existing.filter((t) => t.phase === phase || t.phase.startsWith(`${phase}:`)).length;
        return count === 0 ? phase : `${phase}:${count + 1}`;
    }
    catch {
        // On error, fall back to bare phase name
        return phase;
    }
}
/**
 * Record the start of a phase. Returns the timing record ID (for later update)
 * or null if context is not set or save fails.
 *
 * Automatically appends an iteration suffix (`:2`, `:3`, …) when the same
 * phase has already been recorded for this run (e.g. after a rejection loop).
 */
export async function recordPhaseStart(phase, metadata) {
    if (!contextRunId || !contextRepository)
        return null;
    const id = randomUUID();
    const now = new Date();
    try {
        const iterationPhase = await resolveIterationPhase(phase, contextRunId, contextRepository);
        await contextRepository.save({
            id,
            agentRunId: contextRunId,
            phase: iterationPhase,
            startedAt: now,
            createdAt: now,
            updatedAt: now,
            ...(metadata?.prompt != null && { prompt: metadata.prompt }),
            ...(metadata?.modelId != null && { modelId: metadata.modelId }),
            ...(metadata?.agentType != null && { agentType: metadata.agentType }),
        });
        lastTimingId = id;
        return id;
    }
    catch {
        lastTimingId = null;
        return null;
    }
}
/**
 * Record the end of a phase. Updates the timing record with completedAt, durationMs,
 * and optional execution metadata (token usage, exit code, error).
 * No-op if timingId is null (phase start was skipped or failed).
 */
export async function recordPhaseEnd(timingId, durationMs, metadata) {
    if (!timingId || !contextRepository)
        return;
    try {
        await contextRepository.update(timingId, {
            completedAt: new Date(),
            durationMs: BigInt(durationMs),
            ...(metadata?.inputTokens != null && { inputTokens: BigInt(metadata.inputTokens) }),
            ...(metadata?.outputTokens != null && { outputTokens: BigInt(metadata.outputTokens) }),
            ...(metadata?.cacheCreationInputTokens != null && {
                cacheCreationInputTokens: BigInt(metadata.cacheCreationInputTokens),
            }),
            ...(metadata?.cacheReadInputTokens != null && {
                cacheReadInputTokens: BigInt(metadata.cacheReadInputTokens),
            }),
            ...(metadata?.costUsd != null && { costUsd: metadata.costUsd }),
            ...(metadata?.numTurns != null && { numTurns: metadata.numTurns }),
            ...(metadata?.durationApiMs != null && { durationApiMs: BigInt(metadata.durationApiMs) }),
            ...(metadata?.exitCode != null && { exitCode: metadata.exitCode }),
            ...(metadata?.errorMessage != null && { errorMessage: metadata.errorMessage }),
            ...(metadata?.prompt != null && { prompt: metadata.prompt }),
        });
    }
    catch {
        // Swallow — timing update failure is non-fatal
    }
}
/**
 * Record an instant lifecycle event (e.g., run:started, run:stopped, run:crashed).
 * These are phase timing records with zero duration, used to mark events in the timeline.
 *
 * Can be called with explicit runId and repository (for use outside the worker context,
 * e.g., from use cases like StopAgentRunUseCase or ResumeFeatureUseCase).
 */
export async function recordLifecycleEvent(phase, explicitRunId, explicitRepo) {
    const runId = explicitRunId ?? contextRunId;
    const repo = explicitRepo ?? contextRepository;
    if (!runId || !repo)
        return;
    const now = new Date();
    try {
        await repo.save({
            id: randomUUID(),
            agentRunId: runId,
            phase,
            startedAt: now,
            completedAt: now,
            durationMs: BigInt(0),
            createdAt: now,
            updatedAt: now,
        });
    }
    catch {
        // Swallow — lifecycle event recording is non-fatal
    }
}
/**
 * Update a phase timing record with the prompt text.
 * Useful when the prompt is built after recordPhaseStart (e.g. merge node).
 * No-op if timingId is null or context is not set.
 */
export async function updatePhasePrompt(timingId, prompt) {
    if (!timingId || !contextRepository)
        return;
    try {
        await contextRepository.update(timingId, { prompt });
    }
    catch {
        // Swallow — prompt update failure is non-fatal
    }
}
/**
 * Record the start of an approval wait. Sets waitingApprovalAt on the timing record.
 * No-op if timingId is null or context is not set.
 */
export async function recordApprovalWaitStart(timingId) {
    if (!timingId || !contextRepository)
        return;
    try {
        await contextRepository.updateApprovalWait(timingId, {
            waitingApprovalAt: new Date(),
        });
    }
    catch {
        // Swallow — approval wait timing failure is non-fatal
    }
}
