/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { randomBytes } from 'node:crypto';
import { join } from 'node:path';
import { appendJsonlFile, readJsonlFile } from './file-writer.js';
import { listRecords } from './orchestration-records.js';
import { taskStatePath } from './task-state.js';
import { listWorkflowRuns } from './workflow-engine.js';
const SIZING_ORDER = ['xs', 's', 'm', 'l', 'xl'];
export const ESTIMATES_FILE = 'estimates.jsonl';
export function estimatesPath(cwd) {
    return join(cwd, '.setup-agents', 'state', ESTIMATES_FILE);
}
export function recordEstimate(cwd, input) {
    const base = {
        id: `estimate-${Date.now()}-${randomBytes(3).toString('hex')}`,
        storyId: input.storyId,
        sizingLabel: input.sizingLabel,
        soloEstimateDays: input.soloEstimateDays,
        aiUnguidedEstimateDays: input.aiUnguidedEstimateDays,
        confidence: input.confidence,
        declaredBy: input.declaredBy,
        declaredAt: input.declaredAt ?? new Date().toISOString(),
    };
    const record = input.aiGuidedDays !== undefined ? { ...base, aiGuidedDays: input.aiGuidedDays } : base;
    appendJsonlFile(estimatesPath(cwd), record);
    return record;
}
function latestEstimatesByStory(cwd) {
    const latest = new Map();
    for (const r of readJsonlFile(estimatesPath(cwd)))
        latest.set(r.storyId, r);
    return latest;
}
export function readEstimate(cwd, storyId) {
    return latestEstimatesByStory(cwd).get(storyId);
}
export function listEstimates(cwd) {
    return [...latestEstimatesByStory(cwd).values()];
}
function readTaskEventLog(cwd) {
    return readJsonlFile(taskStatePath(cwd));
}
function taskLifecycleDays(cwd, taskId) {
    const events = readTaskEventLog(cwd).filter((e) => e.id === taskId);
    const claimEvent = events.find((e) => e.event === 'claimed');
    const doneEvent = events.findLast((e) => e.event === 'done');
    if (!claimEvent || !doneEvent)
        return null;
    const diffMs = new Date(doneEvent.updatedAt).getTime() - new Date(claimEvent.updatedAt).getTime();
    return Math.round((diffMs / (1000 * 60 * 60 * 24)) * 100) / 100;
}
function deriveActualDays(phaseActual, taskActual) {
    if (phaseActual !== null && taskActual !== null) {
        // Both exist — prefer whichever is larger (task may span out-of-band work)
        return taskActual > phaseActual
            ? { actualDays: taskActual, actualSource: 'task-lifecycle' }
            : { actualDays: phaseActual, actualSource: 'phase-timeline' };
    }
    if (phaseActual !== null)
        return { actualDays: phaseActual, actualSource: 'phase-timeline' };
    if (taskActual !== null)
        return { actualDays: taskActual, actualSource: 'task-lifecycle' };
    return { actualDays: null, actualSource: null };
}
export function computeBenchmark(cwd, storyId) {
    const estimate = readEstimate(cwd, storyId);
    if (!estimate)
        throw new Error(`No estimate found for story: ${storyId}`);
    const runs = listWorkflowRuns(cwd);
    const completedRun = runs.find((r) => r.storyId === storyId && r.status === 'done');
    const evidenceRecords = listRecords(cwd, 'evidence');
    const evidenceCount = evidenceRecords.filter((e) => e.taskIds.includes(storyId)).length;
    // Phase-timeline actual (primary source)
    let phaseActual = null;
    let qaIterations = 0;
    if (completedRun) {
        const firstPhase = completedRun.phases[0];
        const releasePhase = completedRun.phases.find((p) => p.phase === 'release');
        const startMs = firstPhase?.startedAt ? new Date(firstPhase.startedAt).getTime() : null;
        const endMs = releasePhase?.completedAt ? new Date(releasePhase.completedAt).getTime() : null;
        if (startMs !== null && endMs !== null) {
            phaseActual = Math.round(((endMs - startMs) / (1000 * 60 * 60 * 24)) * 100) / 100;
        }
        qaIterations = completedRun.qaIterations;
    }
    // Task-lifecycle fallback
    const taskActual = taskLifecycleDays(cwd, storyId);
    const { actualDays, actualSource } = deriveActualDays(phaseActual, taskActual);
    const aiGuidedDays = estimate.aiGuidedDays ?? null;
    if (actualDays === null && !completedRun) {
        return {
            storyId,
            sizingLabel: estimate.sizingLabel,
            soloEstimateDays: estimate.soloEstimateDays,
            aiUnguidedEstimateDays: estimate.aiUnguidedEstimateDays,
            aiGuidedDays,
            actualDays: null,
            actualSource: null,
            vsSoloPct: null,
            vsAiUnguidedPct: null,
            vsAiGuidedPct: null,
            qaIterations: 0,
            evidenceCount,
            status: 'pending',
        };
    }
    const vsSoloPct = actualDays !== null
        ? Math.round(((actualDays - estimate.soloEstimateDays) / estimate.soloEstimateDays) * 100)
        : null;
    const vsAiUnguidedPct = actualDays !== null
        ? Math.round(((actualDays - estimate.aiUnguidedEstimateDays) / estimate.aiUnguidedEstimateDays) * 100)
        : null;
    const vsAiGuidedPct = actualDays !== null && aiGuidedDays !== null
        ? Math.round(((actualDays - aiGuidedDays) / aiGuidedDays) * 100)
        : null;
    return {
        storyId,
        sizingLabel: estimate.sizingLabel,
        soloEstimateDays: estimate.soloEstimateDays,
        aiUnguidedEstimateDays: estimate.aiUnguidedEstimateDays,
        aiGuidedDays,
        actualDays,
        actualSource,
        vsSoloPct,
        vsAiUnguidedPct,
        vsAiGuidedPct,
        qaIterations,
        evidenceCount,
        status: 'complete',
    };
}
function median(values) {
    if (values.length === 0)
        return null;
    const sorted = [...values].sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
export function historicalEstimateStats(cwd) {
    const estimates = listEstimates(cwd);
    const completed = estimates.flatMap((est) => {
        try {
            const r = computeBenchmark(cwd, est.storyId);
            return r.actualDays !== null ? [r] : [];
        }
        catch {
            return [];
        }
    });
    const count = completed.length;
    if (count < 2)
        return { count, medianSoloDays: null, medianAiGuidedDays: null, calibrationNote: null };
    const medianSoloDays = median(completed.map((s) => s.soloEstimateDays));
    const guidedValues = completed.filter((s) => s.aiGuidedDays !== null).map((s) => s.aiGuidedDays);
    const medianAiGuidedDays = median(guidedValues);
    const deviations = completed.filter((s) => s.vsSoloPct !== null).map((s) => s.vsSoloPct);
    const avgDeviation = deviations.length > 0 ? Math.round(deviations.reduce((a, b) => a + b, 0) / deviations.length) : null;
    let calibrationNote = null;
    if (avgDeviation !== null && Math.abs(avgDeviation) > 30) {
        calibrationNote =
            avgDeviation > 0
                ? `Historical estimates ran ${avgDeviation}% over actual — consider increasing your baseline.`
                : `Historical estimates ran ${Math.abs(avgDeviation)}% under actual — consider decreasing your baseline.`;
    }
    return { count, medianSoloDays, medianAiGuidedDays, calibrationNote };
}
export function summarizeBenchmark(cwd) {
    const estimates = listEstimates(cwd);
    const stories = [];
    for (const est of estimates) {
        try {
            stories.push(computeBenchmark(cwd, est.storyId));
        }
        catch {
            // skip stories where computation fails
        }
    }
    stories.sort((a, b) => SIZING_ORDER.indexOf(a.sizingLabel) - SIZING_ORDER.indexOf(b.sizingLabel));
    const withActuals = stories.filter((s) => s.actualDays !== null);
    const totalWithActuals = withActuals.length;
    const avgVsSoloPct = totalWithActuals > 0
        ? Math.round(withActuals.reduce((sum, s) => sum + (s.vsSoloPct ?? 0), 0) / totalWithActuals)
        : null;
    const avgVsAiUnguidedPct = totalWithActuals > 0
        ? Math.round(withActuals.reduce((sum, s) => sum + (s.vsAiUnguidedPct ?? 0), 0) / totalWithActuals)
        : null;
    const withGuided = withActuals.filter((s) => s.vsAiGuidedPct !== null);
    const avgVsAiGuidedPct = withGuided.length > 0
        ? Math.round(withGuided.reduce((sum, s) => sum + (s.vsAiGuidedPct ?? 0), 0) / withGuided.length)
        : null;
    return { stories, totalWithActuals, avgVsSoloPct, avgVsAiUnguidedPct, avgVsAiGuidedPct };
}
//# sourceMappingURL=workflow-benchmark.js.map