/**
 * Signal Center — Behavioral Aggregation Service
 * ─────────────────────────────────────────────────
 * Queries the Prisma database to produce a SignalCenter object from Site Tag
 * behavioral data. All queries are scoped to a brandId + trailing window.
 *
 * Internal module — no third-party vendor names in user-facing output.
 */

import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import type { SignalCenter, SignalCenterFrictionEvent } from "../types";

// ─── Helpers ──────────────────────────────────────────────────────────────────

function safeDivide(numerator: number, denominator: number): number {
  if (denominator === 0) return 0;
  return numerator / denominator;
}

function percent(part: number, total: number): number {
  return total === 0 ? 0 : Math.round((part / total) * 10000) / 100;
}

function dropoff(prior: number, current: number): number {
  if (prior === 0) return 0;
  return Math.round(((prior - current) / prior) * 10000) / 100;
}

function median(values: number[]): number {
  if (values.length === 0) return 0;
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 0
    ? (sorted[mid - 1] + sorted[mid]) / 2
    : sorted[mid];
}

function withQueryTimeout<T>(query: Promise<T>, label: string, ms = 10000): Promise<T> {
  return Promise.race([
    query,
    new Promise<T>((_, reject) =>
      setTimeout(
        () => reject(new Error("[signal-center] query timeout: " + label + " exceeded " + ms + "ms")),
        ms,
      )
    ),
  ]);
}

// ─── Sub-section A: Friction Events ──────────────────────────────────────────

const FRICTION_SEVERITY: Record<string, SignalCenterFrictionEvent["severity"]> = {
  rage_click: "critical",
  dead_click: "high",
  form_error: "high",
  quick_bounce: "medium",
  excessive_back: "medium",
  exit_intent: "medium",
};

const FRICTION_DISPLAY_NAME: Record<string, string> = {
  rage_click: "Rage Clicks",
  dead_click: "Dead Clicks",
  form_error: "Form Errors",
  quick_bounce: "Quick Bounces (< 5s)",
  excessive_back: "Excessive Back Navigation",
  exit_intent: "Exit Intent Triggers",
};

async function aggregateFrictionEvents(
  brandId: string,
  trackedSiteId: string,
  since: Date,
): Promise<SignalCenterFrictionEvent[]> {
  try {
    const siteEventTypes = ["rage_click", "dead_click", "exit_intent"];

    // Group SiteEvent by eventType + pageUrl
    const siteEventGroups = await prisma.siteEvent.groupBy({
      by: ["eventType", "pageUrl"],
      where: {
        brandId,
        timestamp: { gte: since },
        eventType: { in: siteEventTypes },
      },
      _count: { id: true },
      orderBy: { _count: { id: "desc" } },
    });

    // Roll up totals per eventType
    const eventTypeTotals: Record<string, number> = {};
    const eventTypePages: Record<string, Array<{ url: string; count: number }>> = {};

    for (const row of siteEventGroups) {
      const et = row.eventType;
      const cnt = row._count.id;
      eventTypeTotals[et] = (eventTypeTotals[et] ?? 0) + cnt;
      if (!eventTypePages[et]) eventTypePages[et] = [];
      eventTypePages[et].push({ url: row.pageUrl, count: cnt });
    }

    // Keep only top 5 pages per event type (already sorted desc from groupBy)
    for (const et of Object.keys(eventTypePages)) {
      eventTypePages[et] = eventTypePages[et].slice(0, 5);
    }

    // quick_bounce: sessions < 5s with pageCount = 1
    type CountRow = [{ count: number }];
    const quickBounceResult = await prisma.$queryRaw<CountRow>(Prisma.sql`
      SELECT COUNT(*)::int as count FROM "TrackedSession"
      WHERE "trackedSiteId" = ${trackedSiteId} AND "startedAt" >= ${since}
      AND "pageCount" = 1 AND EXTRACT(EPOCH FROM ("lastSeenAt" - "startedAt")) < 5
    `);
    const quickBounceCount = quickBounceResult[0]?.count ?? 0;

    // Build results list
    const results: SignalCenterFrictionEvent[] = [];

    for (const et of siteEventTypes) {
      const count = eventTypeTotals[et] ?? 0;
      if (count === 0) continue;
      // Map exit_intent to its display name but keep internal event type notation
      const frictionType = et as SignalCenterFrictionEvent["eventType"] | "exit_intent";
      // exit_intent is not in the SignalCenterFrictionEvent eventType union — emit it
      // as a raw entry using a cast. The spec says include it as a signal, and the
      // severity/display maps handle it; we cast to satisfy the type.
      results.push({
        eventType: frictionType as SignalCenterFrictionEvent["eventType"],
        displayName: FRICTION_DISPLAY_NAME[et] ?? et,
        count,
        severity: FRICTION_SEVERITY[et] ?? "low",
        topAffectedPages: eventTypePages[et] ?? [],
      });
    }

    if (quickBounceCount > 0) {
      results.push({
        eventType: "quick_bounce",
        displayName: FRICTION_DISPLAY_NAME["quick_bounce"],
        count: quickBounceCount,
        severity: FRICTION_SEVERITY["quick_bounce"] as SignalCenterFrictionEvent["severity"],
        topAffectedPages: [],
      });
    }

    return results.sort((a, b) => b.count - a.count).slice(0, 10);
  } catch (err) {
    console.warn("[signal-center] aggregateFrictionEvents failed:", err instanceof Error ? err.message : err);
    return [];
  }
}

// ─── Sub-section B: Conversion Funnel ────────────────────────────────────────

async function aggregateConversionFunnel(
  brandId: string,
  trackedSiteId: string,
  since: Date,
  totalSessions: number,
): Promise<SignalCenter["conversionFunnel"]> {
  try {
    type CountRow = [{ count: number }];

    // Engagement: sessions with pageCount >= 2 OR duration >= 30s
    const engagementResult = await prisma.$queryRaw<CountRow>(Prisma.sql`
      SELECT COUNT(*)::int as count FROM "TrackedSession"
      WHERE "trackedSiteId" = ${trackedSiteId} AND "startedAt" >= ${since}
      AND ("pageCount" >= 2 OR EXTRACT(EPOCH FROM ("lastSeenAt" - "startedAt")) >= 30)
    `);
    const engagementSessions = engagementResult[0]?.count ?? 0;

    // Intent: distinct sessionIds from any unduplicated, valid SiteConversion
    // intentionally unfiltered: audit report; needs broader conversion history
    const intentData = await prisma.siteConversion.findMany({
      where: {
        brandId,
        timestamp: { gte: since },
        isDuplicateOf: null,
        invalidatedReason: null,
      },
      select: { sessionId: true, pageUrl: true },
      distinct: ["sessionId"],
    });
    const intentSessions = intentData.length;

    // Conversion: distinct sessionIds from completed conversions
    // intentionally unfiltered: audit report; needs broader conversion history
    const conversionData = await prisma.siteConversion.findMany({
      where: {
        brandId,
        timestamp: { gte: since },
        isDuplicateOf: null,
        invalidatedReason: null,
        conversionType: { in: ["form_submit", "phone_call", "booking", "calendly"] },
      },
      select: { sessionId: true },
      distinct: ["sessionId"],
    });
    const conversionSessions = conversionData.length;

    // Compute percents (relative to totalSessions)
    const landingPct = 100;
    const engagementPct = percent(engagementSessions, totalSessions);
    const intentPct = percent(intentSessions, totalSessions);
    const conversionPct = percent(conversionSessions, totalSessions);

    // Dropoff from prior stage
    const stages = [
      { name: "landing", sessions: totalSessions },
      { name: "engagement", sessions: engagementSessions },
      { name: "intent", sessions: intentSessions },
      { name: "conversion", sessions: conversionSessions },
    ];

    let biggestDropoffPct = -Infinity;
    let biggestDropoff = { from: "landing", to: "engagement", percent: 0 };

    for (let i = 1; i < stages.length; i++) {
      const prior = stages[i - 1];
      const current = stages[i];
      const drop = dropoff(prior.sessions, current.sessions);
      if (drop > biggestDropoffPct) {
        biggestDropoffPct = drop;
        biggestDropoff = { from: prior.name, to: current.name, percent: drop };
      }
    }

    // Top drop-off pages: pages with high entry sessions but pageCount = 1
    type EntryPageRow = { entryPage: string; count: number };
    const dropoffPageRows = await prisma.$queryRaw<EntryPageRow[]>(Prisma.sql`
      SELECT "entryPage", COUNT(*)::int as count FROM "TrackedSession"
      WHERE "trackedSiteId" = ${trackedSiteId} AND "startedAt" >= ${since}
      AND "pageCount" = 1
      GROUP BY "entryPage"
      ORDER BY count DESC
      LIMIT 5
    `);

    const topDropoffPages = dropoffPageRows.map((r) => ({
      url: r.entryPage,
      stage: "landing",
      sessionsLost: r.count,
    }));

    return {
      landing: {
        sessions: totalSessions,
        percent: landingPct,
        dropoffFromPrior: 0,
      },
      engagement: {
        sessions: engagementSessions,
        percent: engagementPct,
        dropoffFromPrior: dropoff(totalSessions, engagementSessions),
      },
      intent: {
        sessions: intentSessions,
        percent: intentPct,
        dropoffFromPrior: dropoff(engagementSessions, intentSessions),
      },
      conversion: {
        sessions: conversionSessions,
        percent: conversionPct,
        dropoffFromPrior: dropoff(intentSessions, conversionSessions),
      },
      biggestDropoff,
      topDropoffPages,
    };
  } catch (err) {
    console.warn("[signal-center] aggregateConversionFunnel failed:", err instanceof Error ? err.message : err);
    return {
      landing: { sessions: totalSessions, percent: 100, dropoffFromPrior: 0 },
      engagement: { sessions: 0, percent: 0, dropoffFromPrior: 0 },
      intent: { sessions: 0, percent: 0, dropoffFromPrior: 0 },
      conversion: { sessions: 0, percent: 0, dropoffFromPrior: 0 },
      biggestDropoff: { from: "landing", to: "engagement", percent: 0 },
      topDropoffPages: [],
    };
  }
}

// ─── Sub-section C: Form Abandonment ─────────────────────────────────────────

async function aggregateFormAbandonment(
  brandId: string,
  since: Date,
): Promise<SignalCenter["formAbandonment"]> {
  try {
    // intentionally unfiltered: audit report; needs broader conversion history
    const completedForms = await prisma.siteConversion.findMany({
      where: {
        brandId,
        timestamp: { gte: since },
        isDuplicateOf: null,
        invalidatedReason: null,
        conversionType: "form_submit",
      },
      select: { formFields: true },
    });

    const totalCompletes = completedForms.length;
    if (totalCompletes < 3) return null;

    // Estimate starts: completions / 0.3 (30% completion rate heuristic)
    const totalStarts = Math.round(totalCompletes / 0.3);
    const abandonRate = Math.round(((totalStarts - totalCompletes) / totalStarts) * 10000) / 100;

    // Aggregate field names from completed form submissions
    const fieldCounts: Record<string, number> = {};
    for (const conv of completedForms) {
      const fields = conv.formFields;
      if (!Array.isArray(fields)) continue;
      for (const field of fields as Array<{ name?: string; type?: string }>) {
        const name = field?.name;
        if (typeof name === "string" && name.length > 0) {
          fieldCounts[name] = (fieldCounts[name] ?? 0) + 1;
        }
      }
    }

    function getFieldRecommendation(fieldName: string): string {
      const lower = fieldName.toLowerCase();
      if (/phone|mobile|tel/.test(lower)) {
        return "Consider making optional or removing — phone fields have highest drop-off rates";
      }
      if (/email/.test(lower)) {
        return "Pre-populate where possible using site login context";
      }
      if (/address|street|zip|postal/.test(lower)) {
        return "Consider auto-complete or breaking into smaller fields";
      }
      return "Validate inline to reduce submission errors";
    }

    // Build topAbandonedFields from field frequency data
    // Since we only have completed form data, higher-frequency fields are inferred
    // as potential friction points across all form interactions.
    const sortedFields = Object.entries(fieldCounts)
      .sort(([, a], [, b]) => b - a)
      .slice(0, 8);

    const topAbandonedFields = sortedFields.map(([fieldName, completedCount]) => {
      // Estimate abandonment: fields appearing in completed forms are a proxy for
      // high-traffic fields; actual abandon count estimated from overall abandon rate.
      const estimatedAbandonCount = Math.round(
        (totalStarts - totalCompletes) * safeDivide(completedCount, totalCompletes),
      );
      const fieldAbandonRate =
        Math.round(safeDivide(estimatedAbandonCount, totalStarts) * 10000) / 100;

      return {
        fieldName,
        abandonCount: estimatedAbandonCount,
        abandonRate: fieldAbandonRate,
        medianTimeSpent: 0, // not available from current schema
        errorRate: 0,       // not available from current schema
        recommendation: getFieldRecommendation(fieldName),
      };
    });

    return {
      totalStarts,
      totalCompletes,
      abandonRate,
      topAbandonedFields,
    };
  } catch (err) {
    console.warn("[signal-center] aggregateFormAbandonment failed:", err instanceof Error ? err.message : err);
    return null;
  }
}

// ─── Sub-section D: Channel Mix ───────────────────────────────────────────────

async function aggregateChannelMix(
  brandId: string,
  since: Date,
  totalSessions: number,
): Promise<SignalCenter["channelMix"]> {
  try {
    // intentionally unfiltered: audit report; needs broader conversion history
    const conversions = await prisma.siteConversion.groupBy({
      by: ["conversionType"],
      where: {
        brandId,
        timestamp: { gte: since },
        isDuplicateOf: null,
        invalidatedReason: null,
      },
      _count: { id: true },
    });

    const countByType: Record<string, number> = {};
    for (const row of conversions) {
      countByType[row.conversionType] = row._count.id;
    }

    // Chat: check SiteEvent for chat_interaction events
    let chatCount = 0;
    try {
      const chatResult = await prisma.siteEvent.count({
        where: {
          brandId,
          timestamp: { gte: since },
          eventType: "chat_interaction",
        },
      });
      chatCount = chatResult;
    } catch {
      // chat_interaction may not exist — handle gracefully
      chatCount = 0;
    }

    const phoneCount = (countByType["phone_number_click"] ?? 0) + (countByType["phone_call"] ?? 0);
    const formCount = countByType["form_submit"] ?? 0;
    const paperworkCount = (countByType["booking"] ?? 0) + (countByType["calendly"] ?? 0);

    const totalConversions = phoneCount + formCount + paperworkCount + chatCount;

    function channelShare(count: number): number {
      return totalConversions === 0 ? 0 : Math.round(safeDivide(count, totalConversions) * 10000) / 100;
    }

    function channelConvRate(count: number): number | null {
      if (totalSessions === 0) return null;
      return Math.round(safeDivide(count, totalSessions) * 10000) / 100;
    }

    return {
      phone: {
        count: phoneCount,
        share: channelShare(phoneCount),
        conversionRate: channelConvRate(phoneCount),
      },
      form: {
        count: formCount,
        share: channelShare(formCount),
        conversionRate: channelConvRate(formCount),
      },
      paperwork: {
        count: paperworkCount,
        share: channelShare(paperworkCount),
        conversionRate: channelConvRate(paperworkCount),
      },
      chat: {
        count: chatCount,
        share: channelShare(chatCount),
        conversionRate: channelConvRate(chatCount),
      },
    };
  } catch (err) {
    console.warn("[signal-center] aggregateChannelMix failed:", err instanceof Error ? err.message : err);
    return {
      phone: { count: 0, share: 0, conversionRate: null },
      form: { count: 0, share: 0, conversionRate: null },
      paperwork: { count: 0, share: 0, conversionRate: null },
      chat: { count: 0, share: 0, conversionRate: null },
    };
  }
}

// ─── Sub-section E: Scroll-to-Conversion ─────────────────────────────────────

async function aggregateScrollToConversion(
  brandId: string,
  since: Date,
): Promise<SignalCenter["scrollToConversion"]> {
  try {
    // Median scroll depth before conversion via PERCENTILE_CONT
    type MedianRow = [{ median: number | null }];
    const medianResult = await prisma.$queryRaw<MedianRow>(Prisma.sql`
      SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY he."maxScrollPercent")::float as median
      FROM "HeatmapEvent" he
      INNER JOIN "SiteConversion" sc ON he."sessionId" = sc."sessionId"
      WHERE he."brandId" = ${brandId} AND he."eventType" = 'scroll' AND he."timestamp" >= ${since}
      AND sc."isDuplicateOf" IS NULL AND sc."invalidatedReason" IS NULL
      AND he."maxScrollPercent" IS NOT NULL
    `);
    const medianScrollDepth = medianResult[0]?.median ?? 50;

    // Aggregate scroll per page: average maxScrollPercent
    type ScrollPageRow = { pageUrl: string; avgScroll: number };
    const scrollPageRows = await prisma.$queryRaw<ScrollPageRow[]>(Prisma.sql`
      SELECT "pageUrl", AVG("maxScrollPercent")::float as "avgScroll"
      FROM "HeatmapEvent"
      WHERE "brandId" = ${brandId} AND "eventType" = 'scroll' AND "timestamp" >= ${since}
      AND "maxScrollPercent" IS NOT NULL
      GROUP BY "pageUrl"
    `);

    // Conversion count per pageUrl
    // intentionally unfiltered: audit report; needs broader conversion history
    const conversionsByPage = await prisma.siteConversion.groupBy({
      by: ["pageUrl"],
      where: {
        brandId,
        timestamp: { gte: since },
        isDuplicateOf: null,
        invalidatedReason: null,
      },
      _count: { id: true },
    });

    const convCountByUrl: Record<string, number> = {};
    for (const row of conversionsByPage) {
      convCountByUrl[row.pageUrl] = row._count.id;
    }

    const highScrollLowConvPages: Array<{ url: string; avgScroll: number; conversions: number }> = [];
    const lowScrollHighConvPages: Array<{ url: string; avgScroll: number; conversions: number }> = [];

    for (const row of scrollPageRows) {
      const avgScroll = Math.round(row.avgScroll * 10) / 10;
      const conversions = convCountByUrl[row.pageUrl] ?? 0;

      if (avgScroll > 70 && conversions < 2) {
        highScrollLowConvPages.push({ url: row.pageUrl, avgScroll, conversions });
      } else if (avgScroll < 40 && conversions >= 2) {
        lowScrollHighConvPages.push({ url: row.pageUrl, avgScroll, conversions });
      }
    }

    // Sort for relevance
    highScrollLowConvPages.sort((a, b) => b.avgScroll - a.avgScroll);
    lowScrollHighConvPages.sort((a, b) => b.conversions - a.conversions);

    let recommendation = "Monitor scroll depth alongside conversion rates to identify content-CTA alignment gaps.";
    if (highScrollLowConvPages.length > 0 && lowScrollHighConvPages.length > 0) {
      recommendation =
        `${highScrollLowConvPages.length} page(s) have high scroll depth but low conversions — add CTAs earlier in the page. ` +
        `${lowScrollHighConvPages.length} page(s) convert well above the fold — replicate that layout on lower-performing pages.`;
    } else if (highScrollLowConvPages.length > 0) {
      recommendation =
        `${highScrollLowConvPages.length} page(s) have high scroll depth but few conversions — consider adding CTAs higher on the page or in the content body.`;
    } else if (lowScrollHighConvPages.length > 0) {
      recommendation =
        `Above-fold conversion is working well on ${lowScrollHighConvPages.length} page(s). Replicate those layouts on pages with low conversion rates.`;
    }

    return {
      medianScrollDepthBeforeConversion: Math.round(medianScrollDepth),
      highScrollLowConvPages,
      lowScrollHighConvPages,
      recommendation,
    };
  } catch (err) {
    console.warn("[signal-center] aggregateScrollToConversion failed:", err instanceof Error ? err.message : err);
    return {
      medianScrollDepthBeforeConversion: 50,
      highScrollLowConvPages: [],
      lowScrollHighConvPages: [],
      recommendation: "Insufficient scroll data available for analysis.",
    };
  }
}

// ─── Sub-section F: Device & Geo ──────────────────────────────────────────────

async function aggregateDeviceGeo(
  brandId: string,
  trackedSiteId: string,
  since: Date,
): Promise<SignalCenter["deviceGeo"]> {
  try {
    type DeviceSessionRow = { device: string | null; sessions: number };
    const deviceSessionRows = await prisma.$queryRaw<DeviceSessionRow[]>(Prisma.sql`
      SELECT "device", COUNT(*)::int as sessions FROM "TrackedSession"
      WHERE "trackedSiteId" = ${trackedSiteId} AND "startedAt" >= ${since}
      GROUP BY "device"
    `);

    // Skip null-only device data
    const totalDeviceSessions = deviceSessionRows.reduce((s, r) => s + r.sessions, 0);
    if (totalDeviceSessions === 0) return null;

    type DeviceConvRow = { device: string | null; converted_sessions: number };
    const deviceConvRows = await prisma.$queryRaw<DeviceConvRow[]>(Prisma.sql`
      SELECT ts."device", COUNT(DISTINCT ts."sessionId")::int as converted_sessions
      FROM "TrackedSession" ts
      INNER JOIN "SiteConversion" sc ON ts."sessionId" = sc."sessionId"
      WHERE ts."trackedSiteId" = ${trackedSiteId} AND ts."startedAt" >= ${since}
      AND sc."brandId" = ${brandId} AND sc."isDuplicateOf" IS NULL AND sc."invalidatedReason" IS NULL
      GROUP BY ts."device"
    `);

    const sessionsByDevice: Record<string, number> = { mobile: 0, desktop: 0, tablet: 0 };
    const convsByDevice: Record<string, number> = { mobile: 0, desktop: 0, tablet: 0 };

    for (const row of deviceSessionRows) {
      // Null device → treat as desktop
      const device = row.device ?? "desktop";
      const key = device === "mobile" ? "mobile" : device === "tablet" ? "tablet" : "desktop";
      sessionsByDevice[key] = (sessionsByDevice[key] ?? 0) + row.sessions;
    }

    for (const row of deviceConvRows) {
      const device = row.device ?? "desktop";
      const key = device === "mobile" ? "mobile" : device === "tablet" ? "tablet" : "desktop";
      convsByDevice[key] = (convsByDevice[key] ?? 0) + row.converted_sessions;
    }

    const totalSessions = sessionsByDevice.mobile + sessionsByDevice.desktop + sessionsByDevice.tablet;

    function devShare(count: number): number {
      return Math.round(safeDivide(count, totalSessions) * 10000) / 100;
    }

    function devConvRate(convs: number, sessions: number): number {
      return Math.round(safeDivide(convs, sessions) * 10000) / 100;
    }

    const mobileConvRate = devConvRate(convsByDevice.mobile, sessionsByDevice.mobile);
    const desktopConvRate = devConvRate(convsByDevice.desktop, sessionsByDevice.desktop);
    const tabletConvRate = devConvRate(convsByDevice.tablet, sessionsByDevice.tablet);

    const mobileDesktopDelta =
      Math.round((mobileConvRate - desktopConvRate) * 100) / 100;

    // Top metros
    type MetroRow = {
      city: string;
      region: string | null;
      sessions: number;
      conversions: number;
    };
    const metroRows = await prisma.$queryRaw<MetroRow[]>(Prisma.sql`
      SELECT ts."city", ts."region", COUNT(*)::int as sessions,
        COUNT(sc."id")::int as conversions
      FROM "TrackedSession" ts
      LEFT JOIN "SiteConversion" sc ON ts."sessionId" = sc."sessionId"
        AND sc."brandId" = ${brandId} AND sc."isDuplicateOf" IS NULL AND sc."invalidatedReason" IS NULL
      WHERE ts."trackedSiteId" = ${trackedSiteId} AND ts."startedAt" >= ${since}
      AND ts."city" IS NOT NULL
      GROUP BY ts."city", ts."region"
      ORDER BY sessions DESC
      LIMIT 10
    `);

    const topMetros = metroRows.map((r) => ({
      city: r.city,
      region: r.region ?? "",
      sessions: r.sessions,
      conversionRate: Math.round(safeDivide(r.conversions, r.sessions) * 10000) / 100,
    }));

    // Geo concentration: top metro conversions / total conversions
    const totalConversions = metroRows.reduce((s, r) => s + r.conversions, 0);
    const topMetroConversions = metroRows[0]?.conversions ?? 0;
    const geoConcentration =
      totalConversions > 0
        ? Math.round(safeDivide(topMetroConversions, totalConversions) * 10000) / 100
        : 0;

    return {
      deviceMix: {
        mobile: {
          sessions: sessionsByDevice.mobile,
          share: devShare(sessionsByDevice.mobile),
          conversionRate: mobileConvRate,
        },
        desktop: {
          sessions: sessionsByDevice.desktop,
          share: devShare(sessionsByDevice.desktop),
          conversionRate: desktopConvRate,
        },
        tablet: {
          sessions: sessionsByDevice.tablet,
          share: devShare(sessionsByDevice.tablet),
          conversionRate: tabletConvRate,
        },
      },
      mobileDesktopDelta,
      topMetros,
      geoConcentration,
    };
  } catch (err) {
    console.warn("[signal-center] aggregateDeviceGeo failed:", err instanceof Error ? err.message : err);
    return null;
  }
}

// ─── Main export ──────────────────────────────────────────────────────────────

export async function aggregateSignalCenter(
  brandId: string,
  windowDays: number = 30,
): Promise<SignalCenter | null> {
  const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000);

  // 1. Look up TrackedSite
  const trackedSite = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true, firstEventAt: true },
  });
  if (!trackedSite) return null;

  // 2. Check minimum thresholds
  const daysActive = trackedSite.firstEventAt
    ? (Date.now() - trackedSite.firstEventAt.getTime()) / (86400 * 1000)
    : 0;
  if (daysActive < 7) return null;

  const totalSessions = await prisma.trackedSession.count({
    where: { trackedSiteId: trackedSite.id, startedAt: { gte: since } },
  });
  if (totalSessions < 100) return null;

  const dataConfidence: SignalCenter["dataConfidence"] =
    totalSessions >= 1000 ? "high" : totalSessions >= 300 ? "moderate" : "low";

  // 3. Parallel aggregation of all 6 sub-sections
  const trackedSiteId = trackedSite.id;
  const [frictionEvents, funnelData, formData, channelData, scrollData, deviceGeoData] =
    await Promise.all([
      withQueryTimeout(aggregateFrictionEvents(brandId, trackedSiteId, since), "friction-events")
        .catch((e: Error) => { console.warn("[signal-center] friction-events timed out:", e.message); return [] as Awaited<ReturnType<typeof aggregateFrictionEvents>>; }),
      withQueryTimeout(aggregateConversionFunnel(brandId, trackedSiteId, since, totalSessions), "funnel-stages")
        .catch((e: Error) => { console.warn("[signal-center] funnel-stages timed out:", e.message); return { landing: { sessions: totalSessions, percent: 100, dropoffFromPrior: 0 }, engagement: { sessions: 0, percent: 0, dropoffFromPrior: 0 }, intent: { sessions: 0, percent: 0, dropoffFromPrior: 0 }, conversion: { sessions: 0, percent: 0, dropoffFromPrior: 0 }, biggestDropoff: { from: "landing", to: "engagement", percent: 0 }, topDropoffPages: [] } as Awaited<ReturnType<typeof aggregateConversionFunnel>>; }),
      withQueryTimeout(aggregateFormAbandonment(brandId, since), "form-abandonment")
        .catch((e: Error) => { console.warn("[signal-center] form-abandonment timed out:", e.message); return null; }),
      withQueryTimeout(aggregateChannelMix(brandId, since, totalSessions), "channel-mix")
        .catch((e: Error) => { console.warn("[signal-center] channel-mix timed out:", e.message); return { phone: { count: 0, share: 0, conversionRate: null }, form: { count: 0, share: 0, conversionRate: null }, paperwork: { count: 0, share: 0, conversionRate: null }, chat: { count: 0, share: 0, conversionRate: null } } as Awaited<ReturnType<typeof aggregateChannelMix>>; }),
      withQueryTimeout(aggregateScrollToConversion(brandId, since), "scroll-conversion")
        .catch((e: Error) => { console.warn("[signal-center] scroll-conversion timed out:", e.message); return { medianScrollDepthBeforeConversion: 50, highScrollLowConvPages: [], lowScrollHighConvPages: [], recommendation: "Insufficient scroll data available for analysis." } as Awaited<ReturnType<typeof aggregateScrollToConversion>>; }),
      withQueryTimeout(aggregateDeviceGeo(brandId, trackedSiteId, since), "device-geo")
        .catch((e: Error) => { console.warn("[signal-center] device-geo timed out:", e.message); return null; }),
    ]);

  return {
    windowDays,
    totalSessions,
    dataConfidence,
    topFrictionEvents: frictionEvents,
    conversionFunnel: funnelData,
    formAbandonment: formData,
    channelMix: channelData,
    scrollToConversion: scrollData,
    deviceGeo: deviceGeoData,
  };
}
