/**
 * GET /api/site-tag/analytics?brandId=xxx&range=30d
 * --------------------------------------------------
 * Returns tracked event analytics for a brand:
 *   - traffic summary (page views, sessions, top pages, referrers, UTMs)
 *   - conversion summary (form submits, calls, SMS, conversion rate)
 *   - hourly event trends
 *
 * Optional: &type=traffic|conversions|signals|page&pages=/path1,/path2
 */

import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { verifyBrandAccess } from "@/lib/brand-access";
import { prisma } from "@/lib/prisma";
import {
  getSiteTagAnalytics,
  getTrafficSummary,
  getConversionSummary,
  getPagePerformance,
  detectTrackedSignals,
} from "@/lib/services/site-tag-analytics";
import { getSiteTagInstalledAt, siteTagAgeDays, getSiteTagMaturity } from "@/lib/services/site-tag";
import { getConnectedSources } from "@/lib/services/analytics-adapter";
import { getAllEffectiveConfigs, getConfigForType } from "@/lib/conversions/brand-config";
import type { ConversionTier } from "@/lib/conversions/tiers";

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

export const dynamic = "force-dynamic";
/** Vercel function timeout is controlled by vercel.json (60s for this route).
 *  The handler enforces a tighter 20s deadline so users get a partial response
 *  well before Vercel kills the function. */

/** Hard deadline for the entire handler. After this we abandon any
 *  in-flight work and return a partial response so the page never
 *  hangs on an infinite spinner. Set to 20s so the response ships
 *  before Vercel's 60s function timeout (configured in vercel.json). */
const HANDLER_DEADLINE_MS = 20_000;

/** Wraps a value in a {winner: "ok"} envelope so a Promise.race
 *  with a deadline can distinguish "completed" from "deadline-fired"
 *  via discriminated union rather than identity tricks. */
type Raced<T> = { winner: "ok"; data: T } | { winner: "deadline" };

async function withDeadline<T>(promise: Promise<T>, ms: number): Promise<Raced<T>> {
  return Promise.race<Raced<T>>([
    promise.then((data) => ({ winner: "ok", data })),
    new Promise<Raced<T>>((resolve) => setTimeout(() => resolve({ winner: "deadline" }), ms)),
  ]);
}

/** Cache headers for non-live requests (dashboard summary cards, direct
 *  navigation, any consumer that isn't the live-polling page). 30s of
 *  fresh hits amortises short bursts, swr=120 keeps back/forward
 *  navigation instant. */
const CACHE_HEADERS = {
  "Cache-Control": "private, max-age=30, stale-while-revalidate=120",
};

/** Cache headers for ?live=true polls from the Site Tag Analytics
 *  page. The page polls every 5s with a LIVE badge — any HTTP cache
 *  layer (browser, service worker, Vercel edge) serving a 30s-old
 *  body makes "Last Event" and new visits look stuck even though the
 *  DB has newer rows. no-store + no-cache ensures every poll reaches
 *  the origin and the browser never retains a prior response. The
 *  client additionally appends &_t=<ms> as defense-in-depth against
 *  caches that ignore Cache-Control entirely. */
const LIVE_CACHE_HEADERS = {
  "Cache-Control": "private, no-store, no-cache, max-age=0, must-revalidate",
  "Pragma": "no-cache",
  "Expires": "0",
};

export async function GET(req: Request) {
  // [PERF] timing — Site Tag Analytics page polls this every 30s, so
  // keeping it fast is the single biggest lever on app-wide perceived
  // latency. Logs range, sub-path, and total.
  const perfStart = Date.now();
  const incomingUrl = new URL(req.url);
  try {
    const user = await getCurrentUser();
    if (!user) {
      return NextResponse.json(
        { error: "Unauthorized", reason: "session_expired" },
        { status: 401 },
      );
    }

    const searchParams = incomingUrl.searchParams;
    const brandId = searchParams.get("brandId");
    const range = (searchParams.get("range") || "30d") as Range;
    const type = searchParams.get("type"); // optional: narrow the response
    // `live=true` is set by the Site Tag Analytics page's 5s poll. When
    // present we return no-store headers (see LIVE_CACHE_HEADERS) so
    // the browser/CDN/SW can't serve a 30s-stale body. All other
    // consumers (dashboard cards, direct nav) get the normal 30s max-age.
    const isLive = searchParams.get("live") === "true";
    const cacheHeaders = isLive ? LIVE_CACHE_HEADERS : CACHE_HEADERS;

    if (!brandId) {
      return NextResponse.json({ error: "brandId required", reason: "brandId_missing" }, { status: 400 });
    }

    // Per-brand access gate — owners/admins see everything in the
    // org; members only see brands with an explicit BrandMembership.
    // Falls under the existing try/catch so a DB blip here yields a
    // clean 500 rather than an unhandled rejection.
    if (!(await verifyBrandAccess(user.id, brandId, req))) {
      return NextResponse.json({ error: "You don't have access to this brand" }, { status: 403 });
    }

    // Permission check
    const brand = await prisma.brand.findUnique({
      where: { id: brandId },
      include: { organization: { include: { memberships: true } } },
    });
    if (!brand) {
      return NextResponse.json(
        { error: "Brand not found", reason: "brand_not_found" },
        { status: 404 },
      );
    }
    if (!brand.organization.memberships.some((m) => m.userId === user.id)) {
      return NextResponse.json(
        { error: "Access denied", reason: "not_a_member" },
        { status: 403 },
      );
    }

    // Return specific data type if requested — each wrapped so a failure
    // returns empty data instead of 500.
    if (type === "traffic") {
      try {
        const data = await getTrafficSummary(brandId, range);
        return NextResponse.json(data ?? { pageViews: 0, sessions: 0, pagesPerSession: 0, topPages: [], byReferrer: [], byUtmSource: [] });
      } catch (err) {
        console.error("[site-tag/analytics] traffic sub-path failed", { brandId, error: String(err) });
        return NextResponse.json({ pageViews: 0, sessions: 0, pagesPerSession: 0, topPages: [], byReferrer: [], byUtmSource: [] });
      }
    }

    if (type === "conversions") {
      try {
        const data = await getConversionSummary(brandId, range);
        return NextResponse.json(data ?? { totalConversions: 0, byType: {}, conversionRate: 0, topConvertingPages: [], byUtmSource: [] });
      } catch (err) {
        console.error("[site-tag/analytics] conversions sub-path failed", { brandId, error: String(err) });
        return NextResponse.json({ totalConversions: 0, byType: {}, conversionRate: 0, topConvertingPages: [], byUtmSource: [] });
      }
    }

    if (type === "signals") {
      try {
        const signals = await detectTrackedSignals(brandId);
        return NextResponse.json(signals);
      } catch (err) {
        console.error("[site-tag/analytics] signals sub-path failed", { brandId, error: String(err) });
        return NextResponse.json([]);
      }
    }

    if (type === "page") {
      try {
        const pages = (searchParams.get("pages") || "").split(",").filter(Boolean);
        const data = await getPagePerformance(brandId, pages, range);
        return NextResponse.json(data);
      } catch (err) {
        console.error("[site-tag/analytics] page sub-path failed", { brandId, error: String(err) });
        return NextResponse.json([]);
      }
    }

    // Default: return full analytics bundle with tag status metadata.
    // Use findUnique on brandId (matches the /api/site-tag management endpoint)
    // so we find the site regardless of status. The status value is included
    // in tagStatus so the frontend can distinguish active/paused/disabled.
    const siteSelect = {
      id: true, status: true, lastEventAt: true, createdAt: true,
      sitePlatform: true, sitePlatformVersion: true, sitePlatformConfidence: true,
      sitePlatformCategory: true, sitePlatformDetectedAt: true,
      siteHosting: true, siteHostingDetectedAt: true,
    } as const;
    type SiteRow = Awaited<ReturnType<typeof prisma.trackedSite.findUnique<{ where: { brandId: string }; select: typeof siteSelect }>>>;
    let site: SiteRow = null;
    try {
      site = await prisma.trackedSite.findUnique({ where: { brandId }, select: siteSelect });
    } catch (err) {
      console.error("[site-tag/analytics] trackedSite.findUnique failed", { brandId, error: String(err) });
    }

    // Lifetime total event count for this brand's TrackedSite.
    // Two consumers read this field off the response:
    //   1. The Data Quality card in overview-tab.tsx renders it as
    //      "Total lifetime · All time, this brand" — needs a real,
    //      uncapped number.
    //   2. The partial-install banner on site-tag-analytics/page.tsx
    //      line ~425 gates on `0 < totalEvents < 10`, needing only
    //      enough resolution to distinguish "no events" / "1-9 events"
    //      / "10+ events".
    //
    // Previously this was a bounded `findMany({ take: 10 })` whose
    // sample.length was used for both purposes — capping the Data
    // Quality display at 10 on any brand with more than 10 lifetime
    // events (which is essentially every live brand). We now race a
    // real count against a 5s timeout: if it resolves, the Data
    // Quality card gets the real number; if it times out on a
    // massive-event site, we fall back to a 10-row sample whose
    // length is enough to satisfy the partial-install banner logic
    // without fronting a fake capped number as the lifetime total.
    // totalEvents feeds two consumers:
    //   1. Data Quality card ("Total lifetime / All time, this brand")
    //   2. Partial-install banner gating on 0 < totalEvents < 10
    //
    // Previously this ran an unbounded TrackedEvent.count (no date filter)
    // raced against a 5s timeout. On high-volume brands the count query
    // took 60s+; the 5s race fired and orphaned the Postgres connection.
    // Worse, this block sits OUTSIDE the 20s withDeadline wrapper below,
    // so if the fallback findMany also stalled (connection pool exhausted
    // from prior orphaned queries), the handler could reach the Vercel 60s
    // function timeout before ever sending a response.
    //
    // Fix: derive from rollup rows (30-day sum of eventCountsByType). Fast
    // indexed read, no TrackedEvent scan. For new brands with no rollup rows
    // yet, fall back to findMany take:10 which is enough for the banner check.
    // lifetimeEventsExact stays null (it is a 30d sum, not a lifetime count).
    let totalEvents = 0;
    let lifetimeEventsExact: number | null = null;
    if (site) {
      try {
        const rollupSince30d = new Date(Date.now() - 30 * 86_400_000);
        const recentRollupRows = await prisma.siteTagDailyRollup.findMany({
          where: { brandId, date: { gte: rollupSince30d } },
          select: { eventCountsByType: true },
        }).catch(() => []);

        if (recentRollupRows.length > 0) {
          let sum = 0;
          for (const row of recentRollupRows) {
            const ect = (row.eventCountsByType ?? {}) as Record<string, number>;
            sum += Object.values(ect).reduce((a, b) => a + b, 0);
          }
          totalEvents = sum;
          // lifetimeEventsExact intentionally null: this is a 30d sum.
        } else {
          // No rollup rows -- new brand or cron lag. Use bounded findMany
          // to satisfy the partial-install banner check (0 < totalEvents < 10).
          const sample = await prisma.trackedEvent.findMany({
            where: { trackedSiteId: site.id, synthetic: false },
            select: { id: true },
            take: 10,
          }).catch(() => []);
          totalEvents = sample.length;
        }
      } catch (err) {
        console.error("[site-tag/analytics] totalEvents derivation failed", { brandId, error: String(err) });
      }
    }

    const tagStatus = {
      installed: !!site,
      status: site?.status ?? null,
      totalEvents,
      // Exact lifetime count when the `count()` race succeeded;
      // null when the timeout fired and we fell back to the bounded
      // sample. Consumers rendering a "total lifetime" number should
      // prefer this field and show "—" when it's null so a stale
      // sample-cap doesn't get misread as the real volume.
      lifetimeEventsExact,
      lastEventAt: site?.lastEventAt ?? null,
    };

    // Race the heavy aggregation against the handler deadline. The
    // service has historically had pathological cases (a single
    // groupBy on TrackedEvent over the full table can take 50s+
    // when the connection pool is hot). Without this race the
    // function would sit until Vercel killed it at 300s — the page
    // saw an infinite spinner. With the race, the response always
    // ships within HANDLER_DEADLINE_MS; on miss we ship tagStatus
    // alone with empty analytics, and a `partial: true` flag so
    // the page can show "still loading…" copy instead of "0 events".
    const elapsed = Date.now() - perfStart;
    const remaining = Math.max(1000, HANDLER_DEADLINE_MS - elapsed);
    // Each promise gets its own timing wrapper. allSettled instead of
    // all means one hung promise can't blackhole the other — previously
    // when getSiteTagInstalledAt's unbounded `findFirst orderBy asc`
    // hung past 12s, the analytics call (resolved in 1.7s) was thrown
    // away because Promise.all couldn't deliver until both finished.
    type Tagged<T> = { tag: string; result: T };
    const timed = <T>(tag: string, p: Promise<T>): Promise<Tagged<T>> =>
      p.then((result) => ({ tag, result }));

    const settled = await withDeadline(
      Promise.allSettled([
        timed("getSiteTagAnalytics", getSiteTagAnalytics(brandId, range).catch((err) => {
          console.error("[site-tag/analytics] getSiteTagAnalytics failed", { brandId, error: String(err) });
          return null;
        })),
        timed("getSiteTagInstalledAt", getSiteTagInstalledAt(brandId).catch(() => null)),
        timed("getConnectedSources", getConnectedSources(brandId).catch(() => ({ ga4: false, gsc: false }))),
        timed("getSiteTagMaturity", getSiteTagMaturity(brandId).catch(() => ({ activeDays: 0, captureStart: null }))),
      ]),
      remaining,
    );

    let analytics: Awaited<ReturnType<typeof getSiteTagAnalytics>> | null = null;
    let installedAtDate: Date | null = null;
    let connectedSources: { ga4: boolean; gsc: boolean } = { ga4: false, gsc: false };
    let maturity: { activeDays: number; captureStart: Date | null } = { activeDays: 0, captureStart: null };
    let partial = false;
    if (settled.winner === "ok") {
      const results = settled.data;
      // Walk results by tag so a swap of the source array order
      // doesn't silently mis-bind. Each entry is wrapped by `timed()`
      // so the inner result lives on .result.
      for (const r of results) {
        if (r.status === "rejected") {
          console.warn("[site-tag/analytics:race] promise rejected (unexpected — inner catches should swallow)", { brandId, reason: String(r.reason) });
          continue;
        }
        if (r.value.tag === "getSiteTagAnalytics") {
          analytics = r.value.result as Awaited<ReturnType<typeof getSiteTagAnalytics>> | null;
        } else if (r.value.tag === "getSiteTagInstalledAt") {
          installedAtDate = r.value.result as Date | null;
        } else if (r.value.tag === "getConnectedSources") {
          connectedSources = (r.value.result as { ga4: boolean; gsc: boolean }) ?? { ga4: false, gsc: false };
        } else if (r.value.tag === "getSiteTagMaturity") {
          maturity = (r.value.result as { activeDays: number; captureStart: Date | null }) ?? { activeDays: 0, captureStart: null };
        }
      }
      // Even when the race returns "ok", individual promises may have
      // returned null (own timeout / catch). Mark partial only when
      // the CORE analytics is missing — a missing installedAt is just
      // a freshness hint we can leave null without harming the page.
      partial = !analytics;
    } else {
      partial = true;
      console.warn(`[site-tag/analytics] handler deadline (${HANDLER_DEADLINE_MS}ms) hit — returning partial`, { brandId, range });
    }

    // Use rollup-based maturity for the banner when we have active capture days
    // (excludes stray early test events). Fall back to the firstEventAt-derived
    // value only when no qualifying rollup days exist yet (deployed but no real
    // traffic yet), preserving the "waiting for first event" state.
    const siteTagAge = maturity.activeDays > 0 ? maturity.activeDays : siteTagAgeDays(installedAtDate);
    const maturityInstalledAt = maturity.captureStart ?? installedAtDate;

    // Compute per-brand tier breakdown. Runs alongside the audit block
    // so it doesn't add to the critical-path wait time (analytics was
    // already resolved above).
    const convByType = analytics?.conversions?.byType ?? {};
    let conversionTiers: {
      counted: number;
      byTier: Record<ConversionTier | "unknown", { total: number; counted: number }>;
      byType: Record<string, { count: number; tier: ConversionTier | "unknown"; counted: boolean; label: string }>;
    } | null = null;
    if (analytics) {
      const brandConfigs = await getAllEffectiveConfigs(brandId).catch(() => ({}));
      const tierTotals: Record<string, { total: number; counted: number }> = {
        completed: { total: 0, counted: 0 },
        intent: { total: 0, counted: 0 },
        signal: { total: 0, counted: 0 },
        unknown: { total: 0, counted: 0 },
      };
      let counted = 0;
      const byTypeDetail: Record<string, { count: number; tier: ConversionTier | "unknown"; counted: boolean; label: string }> = {};
      for (const [type, rawCount] of Object.entries(convByType)) {
        const count = rawCount as number;
        const cfg = getConfigForType(brandConfigs, type);
        if (!tierTotals[cfg.tier]) {
          // Unregistered type with no tier mapping -- log and skip rather
          // than crashing the entire analytics endpoint.
          console.warn("[site-tag/analytics] Unregistered conversion type missing tier mapping", {
            brandId, type, tier: cfg.tier, count,
          });
          continue;
        }
        tierTotals[cfg.tier].total += count;
        if (cfg.isCounted) {
          tierTotals[cfg.tier].counted += count;
          counted += count;
        }
        byTypeDetail[type] = { count, tier: cfg.tier, counted: cfg.isCounted, label: cfg.displayLabel };
      }
      conversionTiers = {
        counted,
        byTier: tierTotals as Record<ConversionTier | "unknown", { total: number; counted: number }>,
        byType: byTypeDetail,
      };
    }

    // Reconciliation audit — included in every response so the frontend
    // can verify internal consistency. Sums should match: if they don't,
    // a query drift has introduced a mismatch.
    const convByTypeSum = Object.values(convByType).reduce((s: number, n) => s + (n as number), 0);
    const funnelChannels = analytics?.formFunnel?.channels;
    const funnelHardSum = funnelChannels
      ? (funnelChannels.formSubmits + funnelChannels.phoneCalls + funnelChannels.bookingConfirmed + funnelChannels.purchases + funnelChannels.smsClicks + funnelChannels.emailClicks)
      : 0;
    const funnelSoftSum = funnelChannels
      ? (funnelChannels.addToCarts + funnelChannels.checkoutStarteds + funnelChannels.fileDownloads + funnelChannels.newsletterSignups + funnelChannels.waitlistSignups + funnelChannels.bookingSubmits)
      : 0;

    return NextResponse.json({
      tagStatus,
      siteTagAge,
      installedAt: maturityInstalledAt?.toISOString() ?? null,
      // Which analytics integrations are genuinely connected for this brand.
      // Populated by the same getConnectedSources() helper used by
      // /api/conversion-intelligence, so both routes are always in sync.
      // The site-tag-analytics page reads sources.ga4 from here to gate
      // GA4 cards — avoiding a separate conv-intel call just for status.
      sources: connectedSources,
      // TrackedSite row-creation timestamp — when the user deployed
      // the Site Tag on this brand. Distinct from `installedAt`
      // (which is the timestamp of the FIRST captured event, and
      // null until any events arrive). The overview history bar
      // uses this to render "Site Tag deployed X ago — waiting for
      // first event" when installedAt is still null. Without this
      // field the banner would either disappear (current behavior
      // post-fix) or backdate to TrackedSite.createdAt as the
      // pretend-first-event date (the prior bug — see commit
      // history for details).
      siteTagDeployedAt: site?.createdAt?.toISOString() ?? null,
      sitePlatform: site ? {
        name: site.sitePlatform ?? null,
        version: site.sitePlatformVersion ?? null,
        confidence: site.sitePlatformConfidence ?? null,
        category: site.sitePlatformCategory ?? null,
        detectedAt: site.sitePlatformDetectedAt?.toISOString() ?? null,
        hosting: site.siteHosting ?? null,
        hostingDetectedAt: site.siteHostingDetectedAt?.toISOString() ?? null,
      } : null,
      partial,
      conversionTiers,
      ...(analytics ?? {
        traffic: { pageViews: 0, sessions: 0, pagesPerSession: 0, topPages: [], byReferrer: [], byUtmSource: [] },
        conversions: { totalConversions: 0, byType: {}, conversionRate: 0, topConvertingPages: [], byUtmSource: [] },
        trends: [],
        dailyPageViews: [],
        recentEvents: [],
        eventsToday: 0,
        eventsThisWeek: 0,
        activePagesCount: 0,
        outboundClicks: { totalClicks: 0, topDestinations: [] },
        todayByType: [],
        period: { from: new Date(), to: new Date() },
      }),
      _audit: {
        deduplicatedTotal: analytics?.conversions?.totalConversions ?? 0,
        deduplicatedByTypeSum: convByTypeSum,
        rawInteractions: analytics?.conversions?.totalInteractions ?? 0,
        funnelHard: analytics?.formFunnel?.conversions ?? 0,
        funnelSoft: analytics?.formFunnel?.actions ?? 0,
        funnelHardByChannelSum: funnelHardSum,
        funnelSoftByChannelSum: funnelSoftSum,
        sumsMatch: convByTypeSum === (analytics?.conversions?.totalConversions ?? 0)
          && funnelHardSum === (analytics?.formFunnel?.conversions ?? 0)
          && funnelSoftSum === (analytics?.formFunnel?.actions ?? 0),
      },
    }, { headers: cacheHeaders });
  } catch (error) {
    const err = error as Error;
    console.error("[site-tag/analytics] TOP LEVEL CRASH:", err.message, err.stack?.split("\n").slice(0, 5));
    return NextResponse.json({
      error: err.message,
      tagStatus: { installed: false, status: null, totalEvents: 0, lastEventAt: null },
      traffic: { pageViews: 0, sessions: 0, pagesPerSession: 0, topPages: [], byReferrer: [], byUtmSource: [] },
      conversions: { totalConversions: 0, byType: {}, conversionRate: 0, topConvertingPages: [], byUtmSource: [] },
      trends: [],
      dailyPageViews: [],
      recentEvents: [],
      eventsToday: 0,
      eventsThisWeek: 0,
      activePagesCount: 0,
      period: { from: new Date(), to: new Date() },
    }, { status: 200 });
  }
}
