/*
 * 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 { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { ensureDir } from './file-writer.js';
import { computeBenchmark, listEstimates } from './workflow-benchmark.js';
export function sprintPath(cwd) {
    return join(cwd, '.setup-agents', 'state', 'sprints.jsonl');
}
export function lessonsPath(cwd) {
    return join(cwd, '.setup-agents', 'state', 'lessons.jsonl');
}
function appendRecord(file, record) {
    ensureDir(join(file, '..'));
    if (!existsSync(file))
        writeFileSync(file, '');
    appendFileSync(file, `${JSON.stringify(record)}\n`);
}
function readJsonl(file) {
    if (!existsSync(file))
        return [];
    return readFileSync(file, 'utf8')
        .trim()
        .split('\n')
        .filter(Boolean)
        .map((line) => JSON.parse(line));
}
export function startSprint(cwd, opts) {
    const record = {
        id: `sprint-start-${Date.now()}-${randomBytes(3).toString('hex')}`,
        sprintId: opts.sprintId,
        event: 'start',
        startDate: opts.startDate,
        endDate: opts.endDate,
        capacityDays: opts.capacityDays,
        createdAt: new Date().toISOString(),
    };
    appendRecord(sprintPath(cwd), record);
    return record;
}
export function closeSprint(cwd, opts) {
    const estimates = listEstimates(cwd);
    // Identify sprint window from the start event
    const startEvents = readJsonl(sprintPath(cwd)).filter((r) => r.sprintId === opts.sprintId && r.event === 'start');
    const startEvent = startEvents.at(-1);
    // Filter estimates that belong to this sprint
    // All estimates declared between sprint start and close are considered in-sprint
    const sprintEstimates = startEvent?.startDate
        ? estimates.filter((e) => {
            const declaredMs = new Date(e.declaredAt).getTime();
            const startMs = new Date(startEvent.startDate).getTime();
            // end is inclusive: treat endDate as end-of-day by adding 1 day
            const endMs = startEvent.endDate ? new Date(startEvent.endDate).getTime() + 24 * 60 * 60 * 1000 : Infinity;
            return declaredMs >= startMs && declaredMs < endMs;
        })
        : estimates;
    // actualPoints = sum of aiUnguidedEstimateDays (closest proxy for committed story points
    // when no explicit done-run data exists at this level of the service)
    // Prefer soloEstimateDays as "committed baseline" and aiUnguidedEstimateDays as actual proxy
    const committedPoints = opts.committedPoints;
    const actualPoints = sprintEstimates.reduce((sum, e) => sum + e.aiUnguidedEstimateDays, 0);
    const velocityPct = committedPoints > 0 ? Math.round(((actualPoints - committedPoints) / committedPoints) * 100) : 0;
    // Averages across all estimates in the sprint
    const withSolo = sprintEstimates.filter((e) => e.soloEstimateDays > 0);
    const avgVsSoloPct = withSolo.length > 0
        ? Math.round(withSolo.reduce((sum, e) => sum + ((e.aiUnguidedEstimateDays - e.soloEstimateDays) / e.soloEstimateDays) * 100, 0) / withSolo.length)
        : null;
    const withAiGuided = sprintEstimates.filter((e) => e.aiUnguidedEstimateDays > 0 && e.aiGuidedDays != null && e.aiGuidedDays > 0);
    const avgVsAiUnguidedPct = withAiGuided.length > 0
        ? Math.round(withAiGuided.reduce((sum, e) => sum + ((e.aiGuidedDays - e.aiUnguidedEstimateDays) / e.aiUnguidedEstimateDays) * 100, 0) / withAiGuided.length)
        : null;
    const record = {
        id: `sprint-close-${Date.now()}-${randomBytes(3).toString('hex')}`,
        sprintId: opts.sprintId,
        event: 'close',
        committedPoints,
        actualPoints,
        velocityPct,
        avgVsSoloPct,
        avgVsAiUnguidedPct,
        createdAt: new Date().toISOString(),
    };
    appendRecord(sprintPath(cwd), record);
    return record;
}
export function velocityTrend(cwd, limit) {
    const closeEvents = readJsonl(sprintPath(cwd)).filter((r) => r.event === 'close');
    const byId = new Map();
    for (const r of closeEvents)
        byId.set(r.sprintId, r);
    return [...byId.values()].slice(-limit);
}
export function checkAndRecordBiasLessons(cwd) {
    const estimates = listEstimates(cwd);
    const lessons = [];
    // Group estimates by declaredBy
    const byRole = new Map();
    for (const e of estimates) {
        const group = byRole.get(e.declaredBy) ?? [];
        group.push(e);
        byRole.set(e.declaredBy, group);
    }
    const existingLessons = readJsonl(lessonsPath(cwd));
    for (const [role, roleEstimates] of byRole) {
        if (roleEstimates.length < 3)
            continue;
        // biasPct = vsSoloPct: positive → actual > solo (underestimated), negative → actual < solo (overestimated)
        const biasResults = roleEstimates.flatMap((e) => {
            try {
                const b = computeBenchmark(cwd, e.storyId);
                if (b.vsSoloPct === null)
                    return [];
                return [{ storyId: e.storyId, biasPct: b.vsSoloPct }];
            }
            catch {
                return [];
            }
        });
        // over = team beat estimate (actuals < baseline, biasPct < -30)
        // under = team missed estimate (actuals > baseline, biasPct > +30)
        const overBias = biasResults.filter((b) => b.biasPct < -30);
        const underBias = biasResults.filter((b) => b.biasPct > 30);
        const checkBiasGroup = (group, direction) => {
            if (group.length < 3)
                return;
            const avgBias = Math.round(group.reduce((sum, b) => sum + Math.abs(b.biasPct), 0) / group.length);
            const storySample = group.map((b) => b.storyId);
            const sprintId = `auto-${role}-${direction}`;
            // Avoid duplicates: skip if a lesson for this role+direction+same stories already exists
            const alreadyRecorded = existingLessons.some((l) => l.role === role && l.biasDirection === direction && l.biasAvgPct === avgBias);
            if (alreadyRecorded)
                return;
            const lesson = {
                id: `lesson-${Date.now()}-${randomBytes(3).toString('hex')}`,
                sprintId,
                role,
                biasDirection: direction,
                biasAvgPct: avgBias,
                storySample,
                createdAt: new Date().toISOString(),
            };
            appendRecord(lessonsPath(cwd), lesson);
            lessons.push(lesson);
        };
        checkBiasGroup(overBias, 'over');
        checkBiasGroup(underBias, 'under');
    }
    return lessons;
}
//# sourceMappingURL=workflow-velocity.js.map