/**
 * Brand Health Detector
 * ─────────────────────
 * Core detection logic for the hourly brand-health cron.
 * Computes expected vs actual event rates per brand and drives
 * the state machine: degraded → down (alert) → resolved.
 *
 * Shared between /api/cron/brand-health (scheduled) and
 * /api/admin/brand-health/check-now (manual trigger).
 */

import { prisma } from "@/lib/prisma";
import { sendDegradedAlert, sendResolvedAlert } from "./brand-health-email";

// ─── Thresholds (locked per spec) ────────────────────────────────────────────
const DROP_THRESHOLD = 0.90;        // alert when actualRate < 10% of expected
const RECOVER_THRESHOLD = 0.50;     // resolve when actualRate returns to >50% of expected
const MIN_BASELINE_RATE = 100;      // events/hr — skip low-traffic brands
const MIN_HISTORY_DAYS = 7;        // brands need 7 days of data
const CONSECUTIVE_HOURS_NEEDED = 2; // hours degraded before firing email

// ─── Types ───────────────────────────────────────────────────────────────────

export interface BrandHealthResult {
  brandId: string;
  brandName: string;
  siteId: string | null;
  skipped: boolean;
  skipReason?: string;
  expectedRate: number;
  actualRate: number;
  dropPct: number;
  action: "none" | "opened" | "escalated_to_down" | "updated" | "resolved";
  alertId?: string;
  consecutiveHoursDegraded?: number;
}

// ─── Main detector ───────────────────────────────────────────────────────────

export async function runBrandHealthCheck(): Promise<BrandHealthResult[]> {
  const brands = await prisma.brand.findMany({
    where: { status: { not: "deleted" }, trackedSite: { isNot: null }, isDemo: false },
    select: {
      id: true,
      name: true,
      domain: true,
      trackedSite: { select: { id: true, siteId: true } },
    },
  });

  const results: BrandHealthResult[] = [];

  for (const brand of brands) {
    const result = await checkBrand(brand);
    results.push(result);
    console.log("[brand-health] check", JSON.stringify(result));
  }

  return results;
}

// ─── Per-brand check ─────────────────────────────────────────────────────────

async function checkBrand(brand: {
  id: string;
  name: string;
  domain: string;
  trackedSite: { id: string; siteId: string } | null;
}): Promise<BrandHealthResult> {
  const base = {
    brandId: brand.id,
    brandName: brand.name,
    siteId: brand.trackedSite?.siteId ?? null,
    expectedRate: 0,
    actualRate: 0,
    dropPct: 0,
  };

  if (!brand.trackedSite) {
    return { ...base, skipped: true, skipReason: "no_site", action: "none" };
  }

  const siteDbId = brand.trackedSite.id;

  // ── a) Require ≥7 days history ──────────────────────────────────────────
  const firstEvent = await prisma.trackedEvent.findFirst({
    where: { trackedSiteId: siteDbId },
    orderBy: { timestamp: "asc" },
    select: { timestamp: true },
  }).catch(() => null);

  if (!firstEvent) {
    return { ...base, skipped: true, skipReason: "no_events", action: "none" };
  }

  const historyDays = (Date.now() - firstEvent.timestamp.getTime()) / 86_400_000;
  if (historyDays < MIN_HISTORY_DAYS) {
    return { ...base, skipped: true, skipReason: "insufficient_history", action: "none" };
  }

  // ── b) Compute baseline for this hour-of-day ────────────────────────────
  // Pull events from the past 7 days in the same UTC hour-of-day,
  // one full hour each day, then divide by 7.
  const now = new Date();
  const currentHour = now.getUTCHours();
  const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

  // Build 7 1-hour windows (one per day, same hour) across the past week
  let baselineCount = 0;
  for (let dayOffset = 1; dayOffset <= 7; dayOffset++) {
    const windowEnd = new Date(now.getTime() - dayOffset * 24 * 60 * 60 * 1000);
    windowEnd.setUTCMinutes(0, 0, 0);
    windowEnd.setUTCHours(currentHour);
    const windowStart = new Date(windowEnd.getTime() - 60 * 60 * 1000);
    const count = await prisma.trackedEvent.count({
      where: {
        trackedSiteId: siteDbId,
        timestamp: { gte: windowStart, lt: windowEnd },
        synthetic: false,
      },
    });
    baselineCount += count;
  }
  const expectedRate = baselineCount / 7;

  if (expectedRate < MIN_BASELINE_RATE) {
    return { ...base, expectedRate, skipped: true, skipReason: "low_traffic", action: "none" };
  }

  // ── c) Actual events in last 60 min ─────────────────────────────────────
  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
  const actualRate = await prisma.trackedEvent.count({
    where: {
      trackedSiteId: siteDbId,
      timestamp: { gte: oneHourAgo },
      synthetic: false,
    },
  });

  // ── d) Drop % ────────────────────────────────────────────────────────────
  const dropPct = expectedRate > 0 ? (expectedRate - actualRate) / expectedRate : 0;

  const resultBase = { ...base, expectedRate, actualRate, dropPct };

  // ── e) State machine ─────────────────────────────────────────────────────
  const openAlert = await prisma.brandHealthAlert.findFirst({
    where: { brandId: brand.id, status: { not: "resolved" } },
    orderBy: { createdAt: "desc" },
  }).catch(() => null);

  // CASE 5: No open alert + healthy
  if (!openAlert && dropPct < DROP_THRESHOLD) {
    return { ...resultBase, skipped: false, action: "none" };
  }

  // CASE 1: No open alert + newly degraded
  if (!openAlert && dropPct >= DROP_THRESHOLD) {
    const alert = await prisma.brandHealthAlert.create({
      data: {
        brandId: brand.id,
        status: "degraded",
        expectedRate,
        actualRate,
        dropPct,
        consecutiveHoursDegraded: 1,
        firstDetectedAt: now,
        lastCheckedAt: now,
      },
    });
    return { ...resultBase, skipped: false, action: "opened", alertId: alert.id, consecutiveHoursDegraded: 1 };
  }

  if (!openAlert) return { ...resultBase, skipped: false, action: "none" };

  // CASE 3: Open alert + recovered
  if (dropPct < RECOVER_THRESHOLD) {
    await prisma.brandHealthAlert.update({
      where: { id: openAlert.id },
      data: { status: "resolved", resolvedAt: now, lastCheckedAt: now, actualRate, dropPct },
    });
    if (openAlert.alertSentAt) {
      try {
        await sendResolvedAlert(brand, openAlert);
      } catch (err) {
        console.error("[brand-health] resolved email failed", { brandId: brand.id, err: err instanceof Error ? err.message : err });
      }
    }
    return { ...resultBase, skipped: false, action: "resolved", alertId: openAlert.id };
  }

  // Still degraded (dropPct >= RECOVER_THRESHOLD) — distinguish escalation from update
  const newConsecutive = openAlert.consecutiveHoursDegraded + 1;

  // CASE 2: Degraded + now hitting the 2-hour threshold → escalate to "down"
  if (openAlert.status === "degraded" && newConsecutive >= CONSECUTIVE_HOURS_NEEDED) {
    const updated = await prisma.brandHealthAlert.update({
      where: { id: openAlert.id },
      data: {
        status: "down",
        consecutiveHoursDegraded: newConsecutive,
        alertSentAt: now,
        lastCheckedAt: now,
        actualRate,
        dropPct,
      },
    });
    try {
      await sendDegradedAlert(brand, updated);
    } catch (err) {
      console.error("[brand-health] degraded email failed", { brandId: brand.id, err: err instanceof Error ? err.message : err });
    }
    return { ...resultBase, skipped: false, action: "escalated_to_down", alertId: openAlert.id, consecutiveHoursDegraded: newConsecutive };
  }

  // CASE 4: Already "down" or "degraded" + still degraded → update counters only
  await prisma.brandHealthAlert.update({
    where: { id: openAlert.id },
    data: {
      consecutiveHoursDegraded: newConsecutive,
      lastCheckedAt: now,
      actualRate,
      dropPct,
    },
  });
  return { ...resultBase, skipped: false, action: "updated", alertId: openAlert.id, consecutiveHoursDegraded: newConsecutive };
}
