/**
 * GET /api/site-tag/confidence?brandId=X&range=30d
 *
 * Computes a 0-100 "Data Confidence" score for the Site Tag by
 * blending six weighted factors:
 *
 *   sessionMatch     30%  Site Tag sessions / GA4 sessions x 100
 *   conversionCapture 25% Site Tag conversions / GA4 conversions x 100
 *   freshness        15%  Time since last event
 *   dedupQuality     10%  1 - duplicate rate
 *   eventCoverage    10%  Diversity of event types captured
 *   pluginDetection  10%  Booking + form plugins detected + tracked
 *
 * Each factor contributes its own 0-100 sub-score; the weighted sum
 * is the overall score. Each factor also carries a human-readable
 * recommendation so the dashboard can surface actionable next steps
 * ("Connect GA4 to improve conversion capture measurement", etc.).
 *
 * Authenticated via getCurrentUser -- the dashboard user fetches it
 * for their own brand. Cached at the response level for 5 minutes
 * so repeat loads don't re-run the 6-query fan-out.
 *
 * Phase 19A.29: session/conversion/event aggregations now read from
 * SiteTagDailyRollup (one query, ~50ms) instead of live table scans.
 * Redis cache (5min TTL) + hard 30s timeout guard against connection
 * pool starvation on high-volume brands (Fairway: was 25s, now <1s).
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/auth";
import { canonicalConversionType, HARD_CONVERSION_TYPES } from "@/lib/conversion-taxonomy";
import { getCredentials } from "@/lib/services/integration-credentials";
import { apiCallAuthed } from "@/lib/integrations/types";
import { getSessionCount, getSiteTagInstalledAt } from "@/lib/services/platform-metrics";
import { getCached, setCached } from "@/lib/cache/redis-cache";
import { countedConversionWhere } from "@/lib/conversions/counted-where";

export const dynamic = "force-dynamic";
export const maxDuration = 30;

const DEBUG_CONFIDENCE = process.env.DEBUG_CONFIDENCE === "1";

type Range = "1d" | "7d" | "28d" | "30d" | "90d";

interface FactorScore {
  id: string;
  label: string;
  score: number; // 0-100
  weight: number;
  detail: string;
  recommendation: string | null;
}

const GA4_API_BASE = "https://analyticsdata.googleapis.com/v1beta";

// GA4 events we ask about when computing conversionCapture. Mirrors
// the taxonomy's gtag: source list without the prefix.
const GA4_CONVERSION_EVENT_NAMES = [
  "make_an_appointment", "book_appointment", "book_appointment2025",
  "schedule_appointment", "appointment_scheduled", "appointment_confirmed",
  "contact_us", "contact_form_submit", "form_submit", "form_submission",
  "generate_lead", "sign_up", "subscribe",
  "phone_call", "click_to_call",
  "purchase", "conversion",
];

function rangeToWindow(range: Range): { days: number; from: Date } {
  const days = range === "1d" ? 1 : range === "7d" ? 7 : range === "28d" ? 28 : range === "90d" ? 90 : 30;
  return { days, from: new Date(Date.now() - days * 86_400_000) };
}

export async function GET(req: Request) {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const { searchParams } = new URL(req.url);
  const brandId = searchParams.get("brandId");
  const range = (searchParams.get("range") ?? "30d") as Range;
  if (!brandId) {
    return NextResponse.json({ error: "brandId required" }, { status: 400 });
  }

  // Access check -- user must be a member of the brand's org.
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (!brand) return NextResponse.json({ error: "Brand not found" }, { status: 404 });
  if (!brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  // Redis cache check: 1s timeout guards against connection hangs that caused
  // the May-28 pool starvation pattern. On miss or error, fall through to compute.
  const cacheKey = `confidence:${brandId}:${range}`;
  try {
    const cached = await Promise.race([
      getCached<object>(cacheKey),
      new Promise<null>((resolve) => setTimeout(() => resolve(null), 1000)),
    ]);
    if (cached) {
      console.info("[confidence] cache hit", { brandId, range });
      return NextResponse.json(cached);
    }
  } catch {
    console.warn("[confidence] cache read failed, computing live");
  }

  // Hard 30s timeout safety net: if computation stalls (e.g. rollup query
  // unexpectedly slow), return a degraded response instead of holding Prisma
  // connections until Vercel kills the function at 60s. This is the final
  // backstop after the rollup fast path and Redis cache.
  const COMPUTATION_TIMEOUT_MS = 30_000;
  const degradedFallback = NextResponse.json(
    {
      score: 0,
      verdict: "Poor",
      factors: [],
      recommendations: [],
      inputs: {},
      partial: true,
      generatedAt: new Date().toISOString(),
    },
    { status: 200 },
  );

  return Promise.race([
    (async (): Promise<NextResponse> => {
      try {
        const { days, from } = rangeToWindow(range);

        const site = await prisma.trackedSite.findUnique({
          where: { brandId }, select: { id: true, lastEventAt: true, createdAt: true },
        });

        // ---- Install-date resolution + overlap window -----------------------
        //
        // Matches the fix applied to /api/admin/conversion-reconciliation +
        // /api/admin/platform-metrics (commit 8c0c100). Comparing a 30-day
        // GA4 session count against a Site Tag that's only been installed
        // for 6 days makes the capture rate look catastrophic when the
        // daily rate is actually healthy. Every downstream factor that
        // compares Site Tag vs GA4 now uses the OVERLAP:
        //   installedAt   = earliest TrackedEvent for this brand's site
        //                   (fallback: TrackedSite.createdAt)
        //   effectiveFrom = max(rangeStart, installedAt)
        //
        // The in-DB Site Tag aggregations (duplicate rate, event coverage,
        // freshness) still use the full `from` window -- they don't need
        // an apples-to-apples cross-source comparison.
        const installedAt = await getSiteTagInstalledAt(brandId).catch(() => null);
        const effectiveFrom = installedAt && installedAt > from ? installedAt : from;
        const effectiveDays = Math.max(
          1,
          Math.ceil((Date.now() - effectiveFrom.getTime()) / 86_400_000),
        );
        const partialOverlap = installedAt != null && installedAt > from;

        // ---- 1. Session match rate vs GA4 -----------------------------------
        // Default: 100% when GA4 is not connected. No evidence of a
        // problem -- the Site Tag is the only measurement system so
        // there's nothing to compare against.
        let sessionMatch: FactorScore = {
          id: "sessionMatch", label: "Session match rate",
          score: 100, weight: 30,
          detail: "N/A -- GA4 not connected. Site Tag is the sole session source.",
          recommendation: "Connect GA4 in Settings -> Integrations to verify session counts against Google's numbers.",
        };

        // ---- 2. Conversion capture rate vs GA4 ------------------------------
        // Same default-100% logic as sessions. No GA4 = no comparison =
        // no penalty.
        let conversionCapture: FactorScore = {
          id: "conversionCapture", label: "Conversion capture rate",
          score: 100, weight: 25,
          detail: "N/A -- GA4 not connected. Site Tag conversions are the sole source.",
          recommendation: "Connect GA4 to reconcile conversion capture against Google's ground-truth counts.",
        };

        let ga4Sessions = 0;
        let ga4HardConversions = 0;

        const ga4Integration = await prisma.brandIntegration.findFirst({
          where: { brandId, integrationId: "ga4", connected: true },
          select: { id: true },
        });

        if (ga4Integration) {
          try {
            const creds = await getCredentials(brandId, "ga4");
            const propertyId = (typeof creds?.propertyId === "string" ? creds.propertyId : null);
            if (creds && propertyId) {
              // GA4 reads the SAME overlap window so the comparison is
              // apples-to-apples. When the Site Tag was installed mid-range,
              // GA4 only sees the post-install days -- the capture-rate
              // denominator shrinks to match the Site Tag's lifetime.
              const startDate = effectiveFrom.toISOString().slice(0, 10);
              const endDate = new Date().toISOString().slice(0, 10);
              const [sessionsRes, conversionsRes] = await Promise.all([
                apiCallAuthed(
                  `${GA4_API_BASE}/properties/${propertyId}:runReport`,
                  {
                    method: "POST",
                    body: {
                      dateRanges: [{ startDate, endDate }],
                      metrics: [{ name: "sessions" }],
                    },
                  },
                  { brandId, integrationId: "ga4" },
                ),
                apiCallAuthed(
                  `${GA4_API_BASE}/properties/${propertyId}:runReport`,
                  {
                    method: "POST",
                    body: {
                      dateRanges: [{ startDate, endDate }],
                      dimensions: [{ name: "eventName" }],
                      metrics: [{ name: "eventCount" }],
                      dimensionFilter: {
                        filter: {
                          fieldName: "eventName",
                          inListFilter: { values: GA4_CONVERSION_EVENT_NAMES },
                        },
                      },
                      limit: "100",
                    },
                  },
                  { brandId, integrationId: "ga4" },
                ),
              ]);
              if (sessionsRes.ok) {
                const data = sessionsRes.data as { rows?: Array<{ metricValues?: Array<{ value: string }> }> };
                ga4Sessions = parseInt(data.rows?.[0]?.metricValues?.[0]?.value ?? "0", 10);
              }
              if (conversionsRes.ok) {
                const data = conversionsRes.data as { rows?: Array<{ dimensionValues?: Array<{ value: string }>; metricValues?: Array<{ value: string }> }> };
                for (const row of data.rows ?? []) {
                  const name = row.dimensionValues?.[0]?.value ?? "";
                  const count = parseInt(row.metricValues?.[0]?.value ?? "0", 10);
                  const canon = canonicalConversionType(`gtag:${name}`) ?? canonicalConversionType(name);
                  if (canon && HARD_CONVERSION_TYPES.includes(canon)) {
                    ga4HardConversions += count;
                  }
                }
              }
            }
          } catch (err) {
            console.warn("[confidence] ga4 query failed", err);
          }
        }

        // ---- Count Site Tag sessions + conversions --------------------------
        // Phase 19A.29: replaced three live table-scan aggregations with a
        // single SiteTagDailyRollup read (~50ms vs 25s on Fairway). Live
        // queries are kept as a fallback for new brands where the cron has
        // not yet populated sufficient rollup rows.
        let siteTagSessions = 0;
        let siteTagHardConversions = 0;
        let duplicateRate = 0;
        let lastEventMinutesAgo: number | null = null;
        let eventTypeCoverageScore = 0;

        // Coverage map: shared by both rollup and live paths.
        const coverageMap: Array<{ type: string; points: number }> = [
          { type: "page_view",         points: 30 },
          { type: "session_start",     points: 10 },
          { type: "form_start",        points: 10 },
          { type: "form_submit",       points: 15 },
          { type: "click_to_call",     points: 15 },
          { type: "booking_confirmed", points: 10 },
          { type: "outbound_click",    points: 10 },
        ];

        if (site) {
          if (site.lastEventAt) {
            lastEventMinutesAgo = Math.floor((Date.now() - site.lastEventAt.getTime()) / 60_000);
          }

          // Rollup fast path: one query over SiteTagDailyRollup replaces three
          // live aggregations. Allow 1-day gap for the current-day row that may
          // not yet be written by the cron (expectedDays - 1 threshold).
          const startOfEffectiveDay = new Date(
            Date.UTC(
              effectiveFrom.getUTCFullYear(),
              effectiveFrom.getUTCMonth(),
              effectiveFrom.getUTCDate(),
            ),
          );
          let rollupRows: Array<{
            date: Date;
            sessions: number;
            conversionsByType: unknown;
            eventCountsByType: unknown;
          }> = [];
          try {
            rollupRows = await prisma.siteTagDailyRollup.findMany({
              where: { brandId, date: { gte: startOfEffectiveDay } },
              select: { date: true, sessions: true, conversionsByType: true, eventCountsByType: true },
              orderBy: { date: "asc" },
            });
          } catch (rollupErr) {
            console.warn("[confidence] rollup read failed, will use live queries", rollupErr instanceof Error ? rollupErr.message : rollupErr);
          }

          const rollupSufficient = rollupRows.length >= Math.max(1, effectiveDays - 1);

          if (rollupSufficient) {
            // Derive sessionCount, hard conversions, and event coverage from rollup.
            // conversionsByType keys are canonical (cron applies canonicalConversionType).
            // duplicateRate cannot be derived from rollup (no session-level rows);
            // the cron already filters isDuplicateOf=null so in-DB dedup is applied,
            // leaving duplicateRate at its initialized 0 (dedupQuality scores 100).
            siteTagSessions = rollupRows.reduce((s, r) => s + r.sessions, 0);

            for (const row of rollupRows) {
              if (!row.conversionsByType) continue;
              for (const [type, count] of Object.entries(row.conversionsByType as Record<string, number>)) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                if (HARD_CONVERSION_TYPES.includes(type as any)) {
                  siteTagHardConversions += count;
                }
              }
            }

            const presentEventTypes = new Set<string>();
            for (const row of rollupRows) {
              if (!row.eventCountsByType) continue;
              for (const [type, count] of Object.entries(row.eventCountsByType as Record<string, number>)) {
                if (count > 0) presentEventTypes.add(type);
              }
            }
            for (const { type, points } of coverageMap) {
              if (presentEventTypes.has(type)) eventTypeCoverageScore += points;
            }
            eventTypeCoverageScore = Math.min(100, eventTypeCoverageScore);

            console.info("[confidence] rollup fast path", {
              brandId, range,
              rollupRows: rollupRows.length,
              siteTagSessions,
              siteTagHardConversions,
            });
          } else {
            // Live fallback: new brand or cron lag. Same queries as before 19A.29.
            console.warn("[confidence] rollup insufficient, falling back to live queries", {
              brandId, rollupRows: rollupRows.length, expectedDays: effectiveDays,
            });

            const [sessionCount, conversionRows, eventTypeRows] = await Promise.all([
              // Session count via shared helper -- matches every other surface.
              getSessionCount(brandId, effectiveFrom, { trackedSiteId: site.id }),
              // SiteConversion rows over the overlap too -- conversionCapture
              // needs the same-window denominator on both sides.
              prisma.siteConversion.findMany({
                where: { ...countedConversionWhere(brandId, { since: effectiveFrom }) },
                select: { sessionId: true, pageUrl: true, conversionType: true },
                take: 100_000,
              }),
              prisma.trackedEvent.groupBy({
                by: ["eventType"],
                where: { trackedSiteId: site.id, timestamp: { gte: from } },
                _count: true,
              }),
            ]);

            siteTagSessions = sessionCount;

            // Deduped hard conversion count: unique (sessionId, pageUrl,
            // canonical) tuples where canonical is in HARD_CONVERSION_TYPES.
            const uniqueKeys = new Set<string>();
            let rawHard = 0;
            for (const r of conversionRows) {
              const canon = canonicalConversionType(r.conversionType);
              if (!canon || !HARD_CONVERSION_TYPES.includes(canon)) continue;
              rawHard++;
              uniqueKeys.add(`${r.sessionId}|${r.pageUrl}|${canon}`);
            }
            siteTagHardConversions = uniqueKeys.size;
            duplicateRate = rawHard > 0 ? (rawHard - uniqueKeys.size) / rawHard : 0;

            const eventTypes = new Set(eventTypeRows.map((r) => r.eventType));
            for (const { type, points } of coverageMap) {
              if (eventTypes.has(type)) eventTypeCoverageScore += points;
            }
            eventTypeCoverageScore = Math.min(100, eventTypeCoverageScore);
          }
        }

        // Shared caveat used by every GA4-vs-SiteTag recommendation when
        // the Site Tag was installed inside the selected range.
        const partialOverlapNote = partialOverlap && installedAt
          ? (() => {
              const installed = installedAt.toISOString().slice(0, 10);
              const fullAt = new Date(installedAt.getTime() + days * 86_400_000)
                .toISOString().slice(0, 10);
              return `Site Tag installed ${installed} (${effectiveDays} day${effectiveDays === 1 ? "" : "s"} ago) -- capture rate based on the overlapping period only. Full ${days}-day comparison available after ${fullAt}.`;
            })()
          : null;

        // ---- Populate session + conversion factor scores --------------------
        if (ga4Integration && DEBUG_CONFIDENCE) {
          console.info("[data-confidence] Session comparison window:", {
            brandId,
            range,
            start: effectiveFrom.toISOString(),
            end: new Date().toISOString(),
            effectiveDays,
            partialOverlap,
            siteTagSessions,
            ga4Sessions,
          });
        }

        if (ga4Integration && ga4Sessions > 0) {
          const rawRate = siteTagSessions / ga4Sessions;
          const ratePct = Math.round(rawRate * 1000) / 10;
          const score = rawRate >= 0.95 ? 100 : Math.max(0, Math.round(rawRate * 100));
          const excessPct = rawRate > 1 ? Math.round((rawRate - 1) * 1000) / 10 : 0;
          const detail = rawRate > 1
            ? `Site Tag: ${siteTagSessions.toLocaleString()} · GA4: ${ga4Sessions.toLocaleString()} (${ratePct}% -- Site Tag captures ${excessPct}% more than GA4) · ${effectiveDays}-day overlap`
            : `Site Tag: ${siteTagSessions.toLocaleString()} · GA4: ${ga4Sessions.toLocaleString()} (${ratePct}%) · ${effectiveDays}-day overlap`;
          sessionMatch = {
            id: "sessionMatch", label: "Session match rate",
            score, weight: 30,
            detail,
            recommendation: rawRate > 1.05
              ? "Site Tag is reporting more sessions than GA4. This is expected -- first-party cookieless tracking captures visitors who block Google Analytics (ad-blockers, ITP, consent rejections). The Site Tag number is the more accurate picture of actual visitors."
              : partialOverlapNote
                ? partialOverlapNote
                : (rawRate < 0.8
                    ? "Significant gap. Check that the Site Tag is installed on every page and the allowed-domains list covers all host variants."
                    : null),
          };
        }

        if (ga4Integration && ga4HardConversions > 0) {
          const rawRate = siteTagHardConversions / ga4HardConversions;
          const ratePct = Math.round(rawRate * 1000) / 10;
          const score = rawRate >= 0.9 ? 100 : Math.max(0, Math.round(rawRate * 100));
          const excessPct = rawRate > 1 ? Math.round((rawRate - 1) * 1000) / 10 : 0;
          const detail = rawRate > 1
            ? `Site Tag: ${siteTagHardConversions.toLocaleString()} · GA4: ${ga4HardConversions.toLocaleString()} (${ratePct}% -- Site Tag captures ${excessPct}% more than GA4) · ${effectiveDays}-day overlap`
            : `Site Tag: ${siteTagHardConversions.toLocaleString()} · GA4: ${ga4HardConversions.toLocaleString()} (${ratePct}%) · ${effectiveDays}-day overlap`;
          conversionCapture = {
            id: "conversionCapture", label: "Conversion capture rate",
            score, weight: 25,
            detail,
            recommendation: rawRate > 1.05
              ? "Site Tag is catching more conversions than GA4. This is expected and a selling point -- cookieless first-party tracking sees conversions GA4 misses (ad-blocker users, cookie-consent rejections, iframe-based widgets GA4 can't observe). Trust the Site Tag number for attribution decisions."
              : partialOverlapNote
                ? partialOverlapNote
                : (rawRate < 0.9
                    ? "Some GA4 conversions aren't landing in the Site Tag. Open /admin/conversion-reconciliation to see the specific event names missing."
                    : null),
          };
        } else if (ga4Integration && ga4HardConversions === 0 && siteTagHardConversions > 0) {
          // Site Tag is catching conversions GA4 isn't configured to see --
          // legitimate case (plugin events, postMessage paths GA4 misses).
          conversionCapture = {
            id: "conversionCapture", label: "Conversion capture rate",
            score: 100, weight: 25,
            detail: `Site Tag catching ${siteTagHardConversions.toLocaleString()} conversions GA4 doesn't track.`,
            recommendation: "GA4 reports 0 hard conversions for this window -- either it's not configured to track conversions yet, or its events are routed through names meSEO doesn't map. The Site Tag is the source of truth here.",
          };
        }

        // ---- Freshness ------------------------------------------------------
        let freshness: FactorScore = {
          id: "freshness", label: "Data freshness",
          score: 0, weight: 15,
          detail: "No events received yet.",
          recommendation: "Install the Site Tag snippet on the brand's public website and visit a page to trigger the first event.",
        };
        if (lastEventMinutesAgo != null) {
          let score = 0;
          if (lastEventMinutesAgo < 1)         score = 100;
          else if (lastEventMinutesAgo < 5)    score = 95;
          else if (lastEventMinutesAgo < 15)   score = 90;
          else if (lastEventMinutesAgo < 30)   score = 80;
          else if (lastEventMinutesAgo < 60)   score = 65;
          else if (lastEventMinutesAgo < 360)  score = 45;
          else if (lastEventMinutesAgo < 1440) score = 25;
          else score = 10;
          const pretty = lastEventMinutesAgo < 1 ? "just now"
            : lastEventMinutesAgo < 60 ? `${lastEventMinutesAgo}m ago`
            : lastEventMinutesAgo < 1440 ? `${Math.floor(lastEventMinutesAgo / 60)}h ago`
            : `${Math.floor(lastEventMinutesAgo / 1440)}d ago`;
          freshness = {
            id: "freshness", label: "Data freshness",
            score, weight: 15,
            detail: `Last event ${pretty}.`,
            recommendation: score < 65
              ? "No recent events in the last hour. Verify the tag snippet is still on the site -- an infrequently-visited page without the snippet can cause stale data."
              : null,
          };
        }

        // ---- Dedup quality --------------------------------------------------
        const dupPct = Math.round(duplicateRate * 1000) / 10;
        const dedupScore = Math.max(0, Math.min(100, Math.round((1 - duplicateRate) * 100)));
        const dedupQuality: FactorScore = {
          id: "dedupQuality", label: "Dedup quality",
          score: dedupScore, weight: 10,
          detail: `${dupPct}% duplicate rate over the window (${dedupScore}% unique).`,
          recommendation: duplicateRate > 0.15
            ? "High duplicate rate -- run POST /api/admin/debug/dedup-conversions to collapse legacy duplicates."
            : null,
        };

        // ---- Event coverage -------------------------------------------------
        const eventCoverage: FactorScore = {
          id: "eventCoverage", label: "Event coverage",
          score: eventTypeCoverageScore, weight: 10,
          detail: `${Math.round(eventTypeCoverageScore)}% of expected event types captured.`,
          recommendation: eventTypeCoverageScore < 70
            ? "Some expected event types aren't firing. Check the Site Tag is running on every page and that booking plugins are correctly embedded."
            : null,
        };

        // ---- Plugin detection -----------------------------------------------
        // Based on plugin_detected + form_iframe_present TrackedEvents in
        // the last 30 days.
        let pluginScore = 70;
        let pluginDetail = "No booking or form plugins detected.";
        let pluginRec: string | null = null;
        if (site) {
          const [pluginRows, iframePresent] = await Promise.all([
            prisma.trackedEvent.findFirst({
              where: {
                trackedSiteId: site.id,
                eventType: "plugin_detected",
                timestamp: { gte: from },
              },
              select: { metadata: true },
            }),
            prisma.trackedEvent.findFirst({
              where: {
                trackedSiteId: site.id,
                eventType: "form_iframe_present",
                timestamp: { gte: from },
              },
              select: { id: true },
            }),
          ]);
          if (pluginRows) {
            const meta = (pluginRows.metadata ?? {}) as Record<string, unknown>;
            // Handle new consolidated payload { plugins: [...] } and legacy flat shape.
            let pluginName = "plugin";
            if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
              const first = meta.plugins[0] as Record<string, unknown>;
              if (typeof first.plugin === "string") pluginName = first.plugin;
            } else if (typeof meta.plugin === "string") {
              pluginName = meta.plugin;
            }
            pluginScore = 100;
            pluginDetail = `${pluginName} detected and being tracked.`;
          } else if (iframePresent) {
            pluginScore = 50;
            pluginDetail = "Form iframe detected but no trackable plugin -- likely a cross-origin widget we can only observe.";
            pluginRec = "Consider enabling an integration we can see into directly (WPForms / Gravity Forms / Contact Form 7) for full tracking.";
          }
        }
        const pluginDetection: FactorScore = {
          id: "pluginDetection", label: "Plugin detection",
          score: pluginScore, weight: 10,
          detail: pluginDetail,
          recommendation: pluginRec,
        };

        // ---- Weighted total -------------------------------------------------
        const factors: FactorScore[] = [
          sessionMatch, conversionCapture, freshness,
          dedupQuality, eventCoverage, pluginDetection,
        ];
        const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
        const weightedScore = factors.reduce((sum, f) => sum + f.score * f.weight, 0) / totalWeight;
        const score = Math.round(weightedScore);

        const recommendations = factors
          .filter((f) => f.recommendation)
          .map((f) => ({ factor: f.id, label: f.label, message: f.recommendation as string }));

        const verdict = score >= 80 ? "Healthy"
          : score >= 60 ? "Degraded"
          : "Poor";

        if (DEBUG_CONFIDENCE) console.info("[confidence]", { brandId, range, score, factors: factors.map((f) => [f.id, f.score]) });

        const responseData = {
          score,
          verdict,
          factors,
          recommendations,
          scope: {
            brandId, range, days,
            effectiveFromISO: effectiveFrom.toISOString(),
            effectiveDays,
            installedAt: installedAt?.toISOString() ?? null,
            partialOverlap,
          },
          inputs: {
            siteTagSessions,
            ga4Sessions,
            siteTagHardConversions,
            ga4HardConversions,
            duplicateRate,
            lastEventMinutesAgo,
            eventTypeCoverageScore,
          },
          generatedAt: new Date().toISOString(),
        };

        // Fire-and-forget cache write: 5min TTL. The race guard prevents a slow
        // Redis write from delaying the response (mirrors the read-side guard).
        void Promise.race([
          setCached(cacheKey, responseData, 300),
          new Promise<void>((_, reject) => setTimeout(() => reject(new Error("redis write timeout")), 1000)),
        ]).catch((e) => console.warn("[confidence] cache write failed", e instanceof Error ? e.message : e));

        return NextResponse.json(responseData, {
          headers: { "Cache-Control": "private, max-age=300, stale-while-revalidate=600" },
        });
      } catch (err) {
        console.error("[confidence] Error:", err instanceof Error ? err.message : String(err));
        return NextResponse.json(
          { error: "Failed to compute confidence score", score: 0, factors: [], inputs: {} },
          { status: 200 },
        );
      }
    })(),
    new Promise<NextResponse>((resolve) =>
      setTimeout(() => {
        console.warn("[confidence] hard timeout 30s reached, returning degraded response", { brandId, range });
        resolve(degradedFallback);
      }, COMPUTATION_TIMEOUT_MS),
    ),
  ]);
}
