// ---------------------------------------------------------------------------
// Deviation calculation
// ---------------------------------------------------------------------------
/**
 * Compute deviation percentage: positive = over estimate, negative = under estimate.
 * Returns 0 when estimatedHours is 0 to avoid division by zero.
 */
export function computeDeviationPct(estimatedHours, actualHours) {
    if (estimatedHours === 0) {
        return 0;
    }
    return ((actualHours - estimatedHours) / estimatedHours) * 100;
}
// ---------------------------------------------------------------------------
// Median helper
// ---------------------------------------------------------------------------
function median(values) {
    if (values.length === 0) {
        return 0;
    }
    const sorted = [...values].sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    if (sorted.length % 2 === 0) {
        const lo = sorted.at(mid - 1) ?? 0;
        const hi = sorted.at(mid) ?? 0;
        return (lo + hi) / 2;
    }
    return sorted.at(mid) ?? 0;
}
// ---------------------------------------------------------------------------
// Scope breakdown
// ---------------------------------------------------------------------------
function buildScopeBreakdown(records) {
    const grouped = new Map();
    for (const r of records) {
        const existing = grouped.get(r.specScope) ?? [];
        existing.push(r.deviationPct);
        grouped.set(r.specScope, existing);
    }
    const result = [];
    for (const [scope, deviations] of grouped) {
        const avg = deviations.reduce((s, v) => s + v, 0) / deviations.length;
        // If consistently over by more than 20%, suggest increasing multiplier
        const suggestedMultiplier = avg > 20 ? 1 + avg / 100 : 1;
        result.push({
            scope,
            count: deviations.length,
            avgDeviationPct: Math.round(avg * 10) / 10,
            suggestedMultiplier: Math.round(suggestedMultiplier * 100) / 100,
        });
    }
    return result.sort((a, b) => a.scope.localeCompare(b.scope));
}
// ---------------------------------------------------------------------------
// Type breakdown
// ---------------------------------------------------------------------------
function buildTypeBreakdown(records) {
    const grouped = new Map();
    for (const r of records) {
        const existing = grouped.get(r.specType) ?? [];
        existing.push(r.deviationPct);
        grouped.set(r.specType, existing);
    }
    const result = [];
    for (const [type, deviations] of grouped) {
        const avg = deviations.reduce((s, v) => s + v, 0) / deviations.length;
        result.push({
            type,
            count: deviations.length,
            avgDeviationPct: Math.round(avg * 10) / 10,
        });
    }
    return result.sort((a, b) => a.type.localeCompare(b.type));
}
// ---------------------------------------------------------------------------
// Report generation
// ---------------------------------------------------------------------------
export function generateAccuracyReport(projectId, records) {
    const deviations = records.map((r) => r.deviationPct);
    const avgDeviationPct = records.length > 0 ? deviations.reduce((s, v) => s + v, 0) / records.length : 0;
    const outliers = records
        .filter((r) => Math.abs(r.deviationPct) > 50)
        .map((r) => ({ specId: r.specId, deviationPct: r.deviationPct }));
    const chronological = [...records].sort((a, b) => a.recordedAt.localeCompare(b.recordedAt));
    const trend = chronological.map((r) => ({
        specId: r.specId,
        recordedAt: r.recordedAt,
        deviationPct: r.deviationPct,
    }));
    return {
        projectId,
        generatedAt: new Date().toISOString(),
        totalRecords: records.length,
        perSpec: chronological,
        aggregate: {
            avgDeviationPct: Math.round(avgDeviationPct * 10) / 10,
            medianDeviationPct: Math.round(median(deviations) * 10) / 10,
            outlierCount: outliers.length,
            outliers,
        },
        byScope: buildScopeBreakdown(records),
        byType: buildTypeBreakdown(records),
        trend,
    };
}
// ---------------------------------------------------------------------------
// Calibration factor computation
// ---------------------------------------------------------------------------
/**
 * Compute calibration factors from historical actuals.
 * Only creates a multiplier when sample size >= minSamples.
 * A positive avg deviation means we historically underestimate → multiplier > 1.
 */
export function computeCalibrationFactors(projectId, records, minSamples) {
    const scopeGroups = new Map();
    const typeGroups = new Map();
    for (const r of records) {
        const scopeList = scopeGroups.get(r.specScope) ?? [];
        scopeList.push(r.deviationPct);
        scopeGroups.set(r.specScope, scopeList);
        const typeList = typeGroups.get(r.specType) ?? [];
        typeList.push(r.deviationPct);
        typeGroups.set(r.specType, typeList);
    }
    const scopeMultipliers = {};
    const typeMultipliers = {};
    const sampleSize = {};
    for (const [scope, deviations] of scopeGroups) {
        sampleSize[`scope:${scope}`] = deviations.length;
        if (deviations.length >= minSamples) {
            const avg = deviations.reduce((s, v) => s + v, 0) / deviations.length;
            if (Math.abs(avg) > 20) {
                scopeMultipliers[scope] = Math.round((1 + avg / 100) * 100) / 100;
            }
        }
    }
    for (const [type, deviations] of typeGroups) {
        sampleSize[`type:${type}`] = deviations.length;
        if (deviations.length >= minSamples) {
            const avg = deviations.reduce((s, v) => s + v, 0) / deviations.length;
            if (Math.abs(avg) > 20) {
                typeMultipliers[type] = Math.round((1 + avg / 100) * 100) / 100;
            }
        }
    }
    return {
        projectId,
        updatedAt: new Date().toISOString(),
        scopeMultipliers,
        typeMultipliers,
        sampleSize,
    };
}
// ---------------------------------------------------------------------------
// Apply calibration to an estimation
// ---------------------------------------------------------------------------
/**
 * Apply stored calibration factors to a base estimation hours value.
 * Returns the adjusted hours (or the original if no factor found).
 */
export function applyCalibration(baseHours, scope, type, factors) {
    if (!factors) {
        return baseHours;
    }
    let adjusted = baseHours;
    const scopeFactor = factors.scopeMultipliers[scope];
    if (scopeFactor !== undefined) {
        adjusted *= scopeFactor;
    }
    const typeFactor = factors.typeMultipliers[type];
    if (typeFactor !== undefined) {
        adjusted *= typeFactor;
    }
    return Math.round(adjusted * 10) / 10;
}
//# sourceMappingURL=estimation-calibrator.js.map