/**
 * Pure SLO / error-budget math for vendor-attributed SLA tracking.
 *
 * No Prisma, no I/O — given pre-aggregated samples + a target, compute the
 * Service Level Indicator (uptime %), attainment vs target, and the SRE
 * error budget. Also derives outage-episode count + MTTR from an ordered
 * status stream, and a best-effort service-credit estimate from free-text
 * contract terms.
 *
 * Maintenance samples are excluded from the SLI denominator (matches
 * report-builder.ts and the MaintenanceWindow doc comment: planned downtime
 * is not counted against the SLA).
 *
 * See docs/superpowers/plans/2026-06-02-vendor-sla.md.
 */

/** 30-day rolling window in minutes — the default SLO window. */
export const THIRTY_DAYS_MINUTES = 30 * 24 * 60; // 43200

export interface StatusCounts {
    up: number;
    down: number;
    /** Excluded from the SLI denominator. */
    maintenance: number;
}

export interface ErrorBudgetInput {
    targetPercent: number;
    windowMinutes: number;
    downtimeMinutes: number;
}

export interface ErrorBudget {
    /** Total allowed downtime in the window: (1 - target/100) * windowMinutes. */
    budgetMinutes: number;
    /** Downtime actually consumed. */
    consumedMinutes: number;
    /** budget - consumed; negative when over budget. */
    remainingMinutes: number;
    /** remaining / budget * 100. 100 when nothing consumed; can go negative. */
    remainingPercent: number;
    /** True once consumed exceeds budget. */
    breached: boolean;
}

export interface SloInput {
    samples: StatusCounts;
    targetPercent: number;
    windowMinutes: number;
}

export interface Slo {
    sli: number;
    targetPercent: number;
    /** sli - target, in percentage points. >= 0 means meeting the SLA. */
    attainment: number;
    meetingSla: boolean;
    errorBudget: ErrorBudget;
    downtimeMinutes: number;
    /** The non-maintenance sample counts the SLI was computed from. */
    samples: StatusCounts;
}

export type SampleStatus = 'up' | 'down' | 'maintenance' | 'pending';

export interface StatusSample {
    at: Date;
    status: SampleStatus;
}

function round2(n: number): number {
    return Math.round(n * 100) / 100;
}

/**
 * Service Level Indicator: uptime % over non-maintenance samples.
 * Returns 100 when there is nothing to measure.
 */
export function computeSli(counts: StatusCounts): number {
    const denom = counts.up + counts.down;
    if (denom === 0) return 100;
    return round2((counts.up / denom) * 100);
}

export function computeErrorBudget(input: ErrorBudgetInput): ErrorBudget {
    const budgetMinutes = (1 - input.targetPercent / 100) * input.windowMinutes;
    const consumedMinutes = input.downtimeMinutes;
    const remainingMinutes = budgetMinutes - consumedMinutes;

    let remainingPercent: number;
    if (budgetMinutes <= 0) {
        // Target 100% → zero budget. Any downtime is a breach; otherwise full.
        remainingPercent = consumedMinutes > 0 ? 0 : 100;
    } else {
        remainingPercent = (remainingMinutes / budgetMinutes) * 100;
    }

    return {
        budgetMinutes,
        consumedMinutes,
        remainingMinutes,
        remainingPercent,
        breached: consumedMinutes > budgetMinutes,
    };
}

export function computeSlo(input: SloInput): Slo {
    const sli = computeSli(input.samples);
    const denom = input.samples.up + input.samples.down;
    const downRatio = denom === 0 ? 0 : input.samples.down / denom;
    const downtimeMinutes = downRatio * input.windowMinutes;

    const errorBudget = computeErrorBudget({
        targetPercent: input.targetPercent,
        windowMinutes: input.windowMinutes,
        downtimeMinutes,
    });

    return {
        sli,
        targetPercent: input.targetPercent,
        attainment: round2(sli - input.targetPercent),
        meetingSla: sli >= input.targetPercent,
        errorBudget,
        downtimeMinutes,
        samples: input.samples,
    };
}

/**
 * Number of distinct outage episodes (a maximal run of 'down' samples) in an
 * ordered status stream. Maintenance/pending samples don't open or close an
 * episode — they're skipped, so a maintenance blip mid-outage doesn't split it.
 * An outage that's still open at the end of the stream counts as one episode.
 */
export function countOutageEpisodes(stream: StatusSample[]): number {
    let episodes = 0;
    let inOutage = false;
    for (const s of stream) {
        if (s.status === 'down') {
            if (!inOutage) {
                episodes += 1;
                inOutage = true;
            }
        } else if (s.status === 'up') {
            inOutage = false;
        }
        // 'maintenance' / 'pending' — neither open nor close an episode.
    }
    return episodes;
}

/**
 * Mean time to recovery (minutes): average duration of *recovered* outages
 * (first 'down' sample → first subsequent 'up' sample). A still-open outage at
 * the stream's end is excluded from the mean. Returns 0 when nothing recovered.
 */
export function meanTimeToRecovery(stream: StatusSample[]): number {
    const durations: number[] = [];
    let outageStart: Date | null = null;
    for (const s of stream) {
        if (s.status === 'down') {
            if (outageStart === null) outageStart = s.at;
        } else if (s.status === 'up') {
            if (outageStart !== null) {
                durations.push((s.at.getTime() - outageStart.getTime()) / 60000);
                outageStart = null;
            }
        }
    }
    if (durations.length === 0) return 0;
    const sum = durations.reduce((a, b) => a + b, 0);
    return round2(sum / durations.length);
}

/**
 * Best-effort service-credit estimate as a percent of monthly spend.
 *
 * Recognised free-text patterns (case-insensitive); anything else → null so the
 * UI shows "—". This is deliberately NOT a general contract DSL (see open
 * questions in the design doc).
 *
 *   "<p>% credit if uptime < <threshold>%"  → p% when measuredSli < threshold, else 0
 *   "<p>% per <step>% below target"         → floor((target - sli)/step) * p%, min 0
 */
export function estimateServiceCredit(
    creditTerms: string | null | undefined,
    measuredSli: number,
    targetPercent: number,
): number | null {
    if (!creditTerms || !creditTerms.trim()) return null;
    const terms = creditTerms.trim();

    const threshold = terms.match(/(\d+(?:\.\d+)?)\s*%\s*credit\s*if\s*uptime\s*<\s*(\d+(?:\.\d+)?)\s*%/i);
    if (threshold) {
        const pct = parseFloat(threshold[1]);
        const thr = parseFloat(threshold[2]);
        return measuredSli < thr ? pct : 0;
    }

    const tiered = terms.match(/(\d+(?:\.\d+)?)\s*%\s*per\s*(\d+(?:\.\d+)?)\s*%\s*below\s*target/i);
    if (tiered) {
        const pct = parseFloat(tiered[1]);
        const step = parseFloat(tiered[2]);
        if (step <= 0) return null;
        const below = targetPercent - measuredSli;
        if (below <= 0) return 0;
        const tiers = Math.floor(below / step);
        return round2(tiers * pct);
    }

    return null;
}
