/**
 * ADR-PIPELINE-066: Unit Economics Loader
 *
 * Reads `.agentics/runs/<run-id>/unit-economics.json` (or a caller-supplied
 * directory) and returns a Zod-validated UnitEconomics record. Falls back to
 * null when the file is absent or invalid — callers decide whether to use the
 * heuristic fallback or throw.
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { UnitEconomicsSchema, } from '../pipeline/types.js';
import { getSectorEntry } from './domain-unit-registry.js';
const MANIFEST_NAME = 'unit-economics.json';
/**
 * Load the unit-economics manifest for a run.
 *
 * Search order:
 *   1. `<runDir>/unit-economics.json`
 *   2. `<runDir>/artifacts/unit-economics.json`
 *
 * Returns {manifest: null} for any missing/invalid case. This function never
 * throws — the 3-tier precedence in renderFinancialAnalysis relies on a
 * permissive contract.
 */
export function loadUnitEconomics(runDir) {
    if (!runDir) {
        return { manifest: null, inspectedPath: null, reason: 'missing' };
    }
    const candidates = [
        path.join(runDir, MANIFEST_NAME),
        path.join(runDir, 'artifacts', MANIFEST_NAME),
    ];
    let inspectedPath = null;
    for (const candidate of candidates) {
        inspectedPath = candidate;
        if (!fs.existsSync(candidate))
            continue;
        let raw;
        try {
            raw = fs.readFileSync(candidate, 'utf-8');
        }
        catch (err) {
            return {
                manifest: null,
                inspectedPath: candidate,
                reason: 'invalid-json',
                error: err instanceof Error ? err.message : String(err),
            };
        }
        let parsed;
        try {
            parsed = JSON.parse(raw);
        }
        catch (err) {
            return {
                manifest: null,
                inspectedPath: candidate,
                reason: 'invalid-json',
                error: err instanceof Error ? err.message : String(err),
            };
        }
        const result = UnitEconomicsSchema.safeParse(parsed);
        if (!result.success) {
            return {
                manifest: null,
                inspectedPath: candidate,
                reason: 'schema-rejected',
                error: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
            };
        }
        return { manifest: result.data, inspectedPath: candidate, reason: 'loaded' };
    }
    return { manifest: null, inspectedPath, reason: 'missing' };
}
function formatMoney(n) {
    if (!Number.isFinite(n) || n <= 0)
        return '$0';
    if (n >= 1_000_000)
        return `$${(n / 1_000_000).toFixed(1)}M`;
    if (n >= 1_000)
        return `$${(n / 1_000).toFixed(0)}K`;
    return `$${Math.round(n)}`;
}
function computeScopeRatio(manifest) {
    const entry = getSectorEntry(manifest.sector);
    const keys = entry?.scopeKeys ?? ['units', 'rooms', 'sqft', 'vehicles', 'beds', 'agents', 'properties'];
    for (const key of keys) {
        const measured = manifest.measured_scope[key];
        const enterprise = manifest.enterprise_scope[key];
        if (typeof measured === 'number' && typeof enterprise === 'number' && measured > 0) {
            return enterprise / measured;
        }
    }
    // Fall back to the saved ratio implied by the two savings numbers.
    if (manifest.annual_measured_savings_usd > 0) {
        return manifest.annual_extrapolated_savings_usd / manifest.annual_measured_savings_usd;
    }
    return 1;
}
/**
 * Derive the FinancialData fields the executive renderer needs from a
 * unit-economics manifest. Uses the manifest's own numbers — no heuristic
 * substitution.
 */
export function buildFinancialModelFromUnitEconomics(manifest) {
    const measured = manifest.annual_measured_savings_usd;
    const enterprise = manifest.annual_extrapolated_savings_usd;
    const ratio = computeScopeRatio(manifest);
    const entry = getSectorEntry(manifest.sector);
    const unitLabel = manifest.domain_unit.label;
    const formula = entry?.savingsFormula ?? `${unitLabel} × unit economics`;
    // Implementation cost rule-of-thumb: 40% of year-1 enterprise savings, floor $500K.
    const implCost = Math.max(500_000, Math.round(enterprise * 0.4));
    const roiPct = enterprise > 0 ? Math.round((enterprise / implCost) * 100) : 0;
    const paybackMonths = enterprise > 0
        ? Math.max(3, Math.min(36, Math.round((implCost / enterprise) * 12)))
        : 24;
    const npv5yr = Math.round(enterprise * 3.5 - implCost);
    const methodology = `Bottom-up unit economics (${formula}); measured pilot $${formatMoney(measured)}/yr scales ${ratio.toFixed(1)}× to enterprise $${formatMoney(enterprise)}/yr via ${manifest.extrapolation_method}.`;
    return {
        budget: `${formatMoney(implCost)} – ${formatMoney(implCost * 1.4)} (${methodology})`,
        roi: `${roiPct}% projected (${formatMoney(enterprise)} annual savings ÷ ${formatMoney(implCost)} investment)`,
        npv: `${formatMoney(npv5yr)} 5-year NPV (10% discount rate, ${formatMoney(enterprise)}/yr enterprise savings)`,
        payback: `${paybackMonths} months (break-even when cumulative savings exceed investment)`,
        revenue: `${formatMoney(enterprise)} annual enterprise savings — ${formatMoney(measured)} measured on pilot scope, extrapolated via ${manifest.extrapolation_method}`,
        costSavings: `Pilot (${describeScope(manifest.measured_scope)}): ${formatMoney(measured)}/yr · Enterprise (${describeScope(manifest.enterprise_scope)}): ${formatMoney(enterprise)}/yr`,
        methodology,
        scopeRatio: ratio,
    };
}
function describeScope(scope) {
    const parts = [];
    const record = scope;
    for (const key of ['properties', 'rooms', 'sqft', 'vehicles', 'beds', 'agents', 'units']) {
        const v = record[key];
        if (typeof v === 'number' && v > 0)
            parts.push(`${v.toLocaleString()} ${key}`);
    }
    if (parts.length === 0)
        return 'pilot scope';
    return parts.join(', ');
}
// ============================================================================
// Scope-ratio consistency check — ADR-PIPELINE-066 §5
// ============================================================================
export class UnitEconomicsConsistencyError extends Error {
    code;
    constructor(message, code = 'ECLI-SYN-066') {
        super(`${code}: ${message}`);
        this.code = code;
        this.name = 'UnitEconomicsConsistencyError';
    }
}
/**
 * Verify that numbers derived from the manifest still match the manifest
 * within tolerance. Used right before we emit a document so drift between
 * the manifest and the printed prose is caught.
 */
export function verifyManifestConsistency(manifest, printed, tolerancePct = 1) {
    const violations = [];
    const tol = tolerancePct / 100;
    if (typeof printed.measuredSavingsUsd === 'number') {
        const delta = Math.abs(printed.measuredSavingsUsd - manifest.annual_measured_savings_usd);
        const max = Math.max(1, manifest.annual_measured_savings_usd * tol);
        if (delta > max) {
            violations.push(`printed measured savings $${printed.measuredSavingsUsd.toLocaleString()} differs from manifest $${manifest.annual_measured_savings_usd.toLocaleString()} by more than ±${tolerancePct}%`);
        }
    }
    if (typeof printed.enterpriseSavingsUsd === 'number') {
        const delta = Math.abs(printed.enterpriseSavingsUsd - manifest.annual_extrapolated_savings_usd);
        const max = Math.max(1, manifest.annual_extrapolated_savings_usd * tol);
        if (delta > max) {
            violations.push(`printed enterprise savings $${printed.enterpriseSavingsUsd.toLocaleString()} differs from manifest $${manifest.annual_extrapolated_savings_usd.toLocaleString()} by more than ±${tolerancePct}%`);
        }
    }
    if (typeof printed.measuredSavingsUsd === 'number' &&
        typeof printed.enterpriseSavingsUsd === 'number' &&
        printed.measuredSavingsUsd > 0) {
        const ratio = printed.enterpriseSavingsUsd / printed.measuredSavingsUsd;
        const manifestRatio = computeScopeRatio(manifest);
        const ratioDelta = Math.abs(ratio - manifestRatio);
        const ratioMax = Math.max(0.01, manifestRatio * 0.05);
        if (ratioDelta > ratioMax) {
            violations.push(`pilot→enterprise ratio ${ratio.toFixed(2)}× differs from manifest scope ratio ${manifestRatio.toFixed(2)}× by more than ±5%`);
        }
    }
    return { passed: violations.length === 0, violations };
}
/**
 * Entrypoint used by the renderer: run the check, then either throw (strict
 * mode) or return the violations for a soft warning.
 */
export function enforceManifestConsistency(manifest, printed) {
    const result = verifyManifestConsistency(manifest, printed);
    if (!result.passed && process.env['AGENTICS_STRICT_UNIT_ECONOMICS'] === 'true') {
        throw new UnitEconomicsConsistencyError(result.violations.join('; '));
    }
    return result;
}
//# sourceMappingURL=unit-economics-loader.js.map