/**
 * Vendor-attributed SLA service (Prisma-bound orchestration).
 *
 * Glues the pure SLO math (slo-math.ts) + layer attribution
 * (error-class-layer.ts) to the data: for a monitor or a whole vendor over a
 * rolling window, count up/down/maintenance samples (preferring HeartbeatHourly
 * rollups, falling back to raw Heartbeat for recency), build the status stream
 * for episodes/MTTR, and attribute downtime to vendor layers via errorClass.
 *
 * Maintenance minutes are excluded from the SLI denominator (see slo-math).
 *
 * See docs/superpowers/plans/2026-06-02-vendor-sla.md.
 */
import { prisma } from '@/lib/prisma';
import {
    computeSlo,
    countOutageEpisodes,
    meanTimeToRecovery,
    estimateServiceCredit,
    THIRTY_DAYS_MINUTES,
    type Slo,
    type StatusSample,
    type StatusCounts,
} from './slo-math';
import {
    attributeDowntimeByLayer,
    type LayerAttribution,
    type ErrorClassCount,
} from './error-class-layer';

export interface MonitorSloResult {
    monitorId: number;
    name: string;
    slo: Slo;
    breaches: number;
    mttrMinutes: number;
    attribution: LayerAttribution;
}

export interface VendorSlaSummary {
    vendorId: number;
    name: string;
    promisedUptime: number;
    windowDays: number;
    measuredUptime: number;
    attainment: number;
    meetingSla: boolean;
    errorBudgetRemainingPercent: number;
    breaches: number;
    mttrMinutes: number;
    creditPercentEstimate: number | null;
    attribution: LayerAttribution;
    monitors: MonitorSloResult[];
}

/**
 * Count up/down/maintenance samples for a monitor over [since, now].
 *
 * Strategy: HeartbeatHourly rollups carry successCount/failureCount per hour.
 * Raw Heartbeat is retained only ~7 days, so for windows longer than that the
 * rollups are the only complete source. We sum rollup success/failure as the
 * up/down sample counts. Maintenance is read separately from raw heartbeats
 * within raw retention (rollups don't bucket maintenance), which is a
 * conservative lower bound — acceptable because maintenance only ever shrinks
 * the denominator in our favour and the rollup window already excludes
 * maintenance from successCount/failureCount via the engine's status marking.
 */
async function countSamplesForMonitor(monitorId: number, since: Date, now: Date): Promise<StatusCounts> {
    const rollups = await prisma.heartbeatHourly.aggregate({
        where: { monitorId, timestamp: { gte: since, lte: now } },
        _sum: { successCount: true, failureCount: true },
    });

    const up = rollups._sum.successCount ?? 0;
    const down = rollups._sum.failureCount ?? 0;

    const maintenance = await prisma.heartbeat.count({
        where: { monitorId, createdAt: { gte: since, lte: now }, status: 'maintenance' },
    });

    return { up, down, maintenance };
}

/**
 * Build an ordered status stream for episodes/MTTR. Uses raw Heartbeat (recent
 * detail) — episodes/MTTR are most meaningful for recent outages, and raw rows
 * carry the per-sample timestamp the pure functions need.
 */
async function statusStreamForMonitor(monitorId: number, since: Date, now: Date): Promise<StatusSample[]> {
    const rows = await prisma.heartbeat.findMany({
        where: { monitorId, createdAt: { gte: since, lte: now } },
        select: { createdAt: true, status: true },
        orderBy: { createdAt: 'asc' },
        take: 20000, // bound the pull; recent detail only
    });
    return rows.map((r) => ({ at: r.createdAt, status: r.status as StatusSample['status'] }));
}

/** Per-errorClass down-sample counts for a monitor, for layer attribution. */
async function errorClassCountsForMonitor(monitorId: number, since: Date, now: Date): Promise<ErrorClassCount[]> {
    const grouped = await prisma.heartbeat.groupBy({
        by: ['errorClass'],
        where: { monitorId, createdAt: { gte: since, lte: now }, status: 'down' },
        _count: { _all: true },
    });
    return grouped.map((g) => ({ errorClass: g.errorClass, count: g._count._all }));
}

export async function computeMonitorSlo(
    monitorId: number,
    targetPercent: number,
    windowMinutes: number = THIRTY_DAYS_MINUTES,
    now: Date = new Date(),
): Promise<MonitorSloResult> {
    const since = new Date(now.getTime() - windowMinutes * 60000);
    const monitor = await prisma.monitor.findUnique({
        where: { id: monitorId },
        select: { id: true, name: true },
    });
    if (!monitor) throw new Error(`Monitor ${monitorId} not found`);

    const [samples, stream, ecCounts] = await Promise.all([
        countSamplesForMonitor(monitorId, since, now),
        statusStreamForMonitor(monitorId, since, now),
        errorClassCountsForMonitor(monitorId, since, now),
    ]);

    const slo = computeSlo({ samples, targetPercent, windowMinutes });

    return {
        monitorId,
        name: monitor.name,
        slo,
        breaches: countOutageEpisodes(stream),
        mttrMinutes: meanTimeToRecovery(stream),
        attribution: attributeDowntimeByLayer(ecCounts),
    };
}

/** Merge a list of StatusCounts into one aggregate. */
function mergeCounts(list: StatusCounts[]): StatusCounts {
    return list.reduce(
        (acc, c) => ({ up: acc.up + c.up, down: acc.down + c.down, maintenance: acc.maintenance + c.maintenance }),
        { up: 0, down: 0, maintenance: 0 },
    );
}

export async function computeVendorSla(
    vendorId: number,
    windowMinutes: number = THIRTY_DAYS_MINUTES,
    now: Date = new Date(),
): Promise<VendorSlaSummary> {
    const vendor = await prisma.vendor.findUnique({
        where: { id: vendorId },
        select: { id: true, name: true, promisedUptime: true, creditTerms: true },
    });
    if (!vendor) throw new Error(`Vendor ${vendorId} not found`);

    const monitors = await prisma.monitor.findMany({
        where: { vendorId, deletedAt: null },
        select: { id: true, name: true },
        orderBy: { name: 'asc' },
    });

    const target = vendor.promisedUptime;
    const monitorResults: MonitorSloResult[] = [];
    for (const m of monitors) {
        monitorResults.push(await computeMonitorSlo(m.id, target, windowMinutes, now));
    }

    // Vendor-level rollup: aggregate the per-monitor sample counts and
    // attribution into a single vendor SLO so the headline number reflects the
    // whole vendor surface, not an average of averages.
    const aggSamples = mergeCounts(monitorResults.map((r) => r.slo.samples));
    const vendorSlo = computeSlo({ samples: aggSamples, targetPercent: target, windowMinutes });

    const aggAttribution = attributeDowntimeByLayer(
        monitorResults.flatMap((r) => layerToCounts(r.attribution)),
    );

    const breaches = monitorResults.reduce((s, r) => s + r.breaches, 0);
    const mttrValues = monitorResults.map((r) => r.mttrMinutes).filter((v) => v > 0);
    const mttrMinutes = mttrValues.length === 0
        ? 0
        : Math.round((mttrValues.reduce((a, b) => a + b, 0) / mttrValues.length) * 100) / 100;

    const creditPercentEstimate = estimateServiceCredit(vendor.creditTerms, vendorSlo.sli, target);

    return {
        vendorId: vendor.id,
        name: vendor.name,
        promisedUptime: target,
        windowDays: Math.round(windowMinutes / (24 * 60)),
        measuredUptime: vendorSlo.sli,
        attainment: vendorSlo.attainment,
        meetingSla: vendorSlo.meetingSla,
        errorBudgetRemainingPercent: vendorSlo.errorBudget.remainingPercent,
        breaches,
        mttrMinutes,
        creditPercentEstimate,
        attribution: aggAttribution,
        monitors: monitorResults,
    };
}

// Turn a per-monitor layer attribution back into ErrorClassCount-like entries
// so the vendor aggregate re-derives percentages over the combined total.
// We emit one synthetic entry per layer using a representative errorClass.
const LAYER_REPRESENTATIVE_ERRORCLASS: Record<string, string> = {
    ISP: 'TIMEOUT',
    HOST: 'TCP_REJECT',
    SSL: 'TLS_INVALID',
    DNS: 'DNS_FAIL',
    CDN: 'UNKNOWN', // no errorClass maps to CDN today; stays unattributed
    APP: 'HTTP_5XX',
};

function layerToCounts(attr: LayerAttribution): ErrorClassCount[] {
    const out: ErrorClassCount[] = [];
    for (const [layer, count] of Object.entries(attr.byLayer)) {
        if (count > 0) out.push({ errorClass: LAYER_REPRESENTATIVE_ERRORCLASS[layer] ?? 'UNKNOWN', count });
    }
    if (attr.unattributed > 0) out.push({ errorClass: 'UNKNOWN', count: attr.unattributed });
    return out;
}
