import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";

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

const CACHE_HEADERS = {
  "Cache-Control": "private, max-age=300, stale-while-revalidate=600",
};

export async function GET(req: Request) {
  const t0 = Date.now();
  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";
  const page = searchParams.get("page"); // optional page filter

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

  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (!brand || !brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  const days = range === "1d" ? 1 : range === "7d" ? 7 : range === "28d" ? 28 : range === "90d" ? 90 : 30;
  const since = new Date(Date.now() - days * 86400000);

  try {
  const pageFilter = page ? { pageUrl: { contains: page } } : {};

  // Parallelise the 5 independent DB reads. Previously sequential
  // (pages → clicks → scrolls → attention → cta = ~5× one-round-
  // trip), now one Promise.all. Worst-case wall time drops from
  // sum-of-5 to max-of-5 on cold pool; on warm pool the connection
  // pool cap of 5 matches the fan-out exactly so no queueing.
  const [pages, clicks, scrolls, attention, ctaEvents] = await Promise.all([
    // Pages with heatmap data
    prisma.pageRecord.findMany({
      where: { brandId },
      select: { url: true },
      orderBy: { totalVisits: "desc" },
      take: 100,
    }),
    // Click data — orderBy timestamp DESC so the index (brandId, eventType, timestamp)
    // is scanned in reverse and Postgres stops at take:200 without a full table scan.
    prisma.heatmapEvent.findMany({
      where: { brandId, eventType: "clicks", timestamp: { gte: since }, ...pageFilter },
      select: { pageUrl: true, events: true },
      orderBy: { timestamp: "desc" },
      take: 200,
    }),
    // Scroll depth data
    prisma.heatmapEvent.findMany({
      where: { brandId, eventType: "scroll", timestamp: { gte: since }, ...pageFilter },
      select: { pageUrl: true, maxScrollPercent: true, milestonesReached: true },
      orderBy: { timestamp: "desc" },
      take: 500,
    }),
    // Attention data
    prisma.heatmapEvent.findMany({
      where: { brandId, eventType: "attention", timestamp: { gte: since }, ...pageFilter },
      select: { pageUrl: true, sectionTimes: true, events: true },
      orderBy: { timestamp: "desc" },
      take: 200,
    }),
    // CTA visibility from SiteEvent
    prisma.siteEvent.findMany({
      where: { brandId, eventType: "element_visible", timestamp: { gte: since }, ...pageFilter },
      select: { pageUrl: true, eventData: true },
      take: 500,
    }),
  ]);

  // Aggregate clicks by element. Prefer the heatmap.js-computed
  // `label` (top-level or .target.label) which runs the 8-step
  // priority chain (aria → text → id → title → alt → href → src
  // → value/placeholder → tag+class). Fall back to selector / text
  // for legacy payloads written before labels were shipped. The
  // "unknown" bucket stays as the absolute last resort so an
  // unparseable click still lands somewhere instead of being
  // dropped silently.
  interface ClickEvent {
    selector?: string; text?: string;
    x?: number; y?: number;
    label?: string;
    // Newer field names used by public/t.js click events
    // (click_to_call / outbound_click / sms_submit / *intent).
    // TrackedEvent-sourced clicks read through this shape; heatmap-
    // sourced clicks (from src/lib/site-tag/heatmap.js) continue to
    // use `label` / `target.label`. Accepting both here keeps the
    // aggregator source-agnostic.
    element_label?: string;
    elementLabel?: string;
    tag?: string;
    element_tag?: string;
    positionX?: number; positionY?: number;
    aboveFold?: boolean;
    target?: { label?: string; tag?: string; positionX?: number; positionY?: number; aboveFold?: boolean };
  }
  interface ClickAgg {
    element: string;
    count: number;
    page: string;
    aboveFoldCount: number;
    positionXSum: number;
    positionYSum: number;
    positionSamples: number;
  }
  const clickMap: Record<string, ClickAgg> = {};
  for (const c of clicks) {
    const evts = (c.events as ClickEvent[]) || [];
    for (const e of evts) {
      // Label priority: any of the four label field names (new
       // public/t.js snake_case + camelCase + the older heatmap.js
       // shape + legacy selector/text fallbacks).
      const label = (
        e.elementLabel
        || e.element_label
        || e.label
        || e.target?.label
        || e.selector
        || e.text
        || ""
      ).trim();
      const element = label || "unknown";
      const key = `${c.pageUrl}::${element}`;
      if (!clickMap[key]) {
        clickMap[key] = {
          element,
          count: 0,
          page: c.pageUrl,
          aboveFoldCount: 0,
          positionXSum: 0,
          positionYSum: 0,
          positionSamples: 0,
        };
      }
      clickMap[key].count++;
      const aboveFold = typeof e.aboveFold === "boolean" ? e.aboveFold : e.target?.aboveFold;
      if (aboveFold) clickMap[key].aboveFoldCount++;
      const px = typeof e.positionX === "number" ? e.positionX : e.target?.positionX;
      const py = typeof e.positionY === "number" ? e.positionY : e.target?.positionY;
      if (typeof px === "number" && typeof py === "number") {
        clickMap[key].positionXSum += px;
        clickMap[key].positionYSum += py;
        clickMap[key].positionSamples++;
      }
    }
  }
  const topClicks = Object.values(clickMap)
    .sort((a, b) => b.count - a.count)
    .slice(0, 50)
    .map((c) => ({
      element: c.element,
      count: c.count,
      page: c.page,
      // Fraction of clicks on this element that landed above the
      // fold — informs the UI's "above fold %" column.
      aboveFoldPct: c.count > 0 ? Math.round((c.aboveFoldCount / c.count) * 100) : 0,
      // Average position (viewport-percent coordinates) for a future
      // visual-heatmap overlay. Null when no sample had position data.
      avgPositionX: c.positionSamples > 0 ? Math.round(c.positionXSum / c.positionSamples) : null,
      avgPositionY: c.positionSamples > 0 ? Math.round(c.positionYSum / c.positionSamples) : null,
    }));

  // Scroll depth aggregation (scrolls was already fetched above in
  // the Promise.all parallel batch).
  const milestoneCount = { 25: 0, 50: 0, 75: 0, 90: 0, 100: 0 };
  for (const s of scrolls) {
    const milestones = (s.milestonesReached as number[]) || [];
    for (const m of [25, 50, 75, 90, 100] as const) {
      if (milestones.includes(m) || (s.maxScrollPercent ?? 0) >= m) milestoneCount[m]++;
    }
  }
  const scrollTotal = scrolls.length || 1;
  const scrollDepth = Object.entries(milestoneCount).map(([pct, count]) => ({
    milestone: Number(pct),
    count,
    pct: Math.round((count / scrollTotal) * 100),
  }));

  // Attention aggregation (attention was already fetched above).
  // Aggregate per-section attention across all pageviews. Each event row is
  // ALREADY one pageview's time-in-section in MILLISECONDS — we average
  // those across the number of pageviews that included the section.
  //
  // GROUPING KEY INVARIANT: the CSS selector (e.selector / sel) is the sole
  // aggregation key. Labels are display-only — they may change over time
  // (e.g. heading text edited) or be null, so using them as a key would
  // split or merge data incorrectly. Never group by bucket.label.
  //
  // FORMAT PREFERENCE: commit 7d22dc3 introduced a new `events` array payload
  // shape that carries { selector, label, ms }. Legacy payloads only have
  // `sectionTimes` as { selector: ms }. The client (heatmap.js) currently
  // sends BOTH fields for back-compat, so we must pick exactly one — prefer
  // `events` when it's a non-empty array, otherwise fall back to
  // `sectionTimes`. Never read both from the same row (would double-count).
  const sectionTimeMap: Record<string, { totalMs: number; samples: number; label: string | null }> = {};
  for (const a of attention) {
    const evts = (a.events as Array<{ selector?: string; label?: string | null; ms?: number }>) || null;
    if (Array.isArray(evts) && evts.length > 0) {
      // New format — preferred. Skip sectionTimes for this row entirely.
      for (const e of evts) {
        if (!e?.selector || typeof e.ms !== "number") continue;
        const bucket = sectionTimeMap[e.selector] ?? { totalMs: 0, samples: 0, label: null };
        bucket.totalMs += e.ms;
        bucket.samples++;
        // Label is stored for display only — never used as a key
        if (!bucket.label && e.label) bucket.label = e.label;
        sectionTimeMap[e.selector] = bucket;
      }
    } else {
      // Legacy format: only sectionTimes JSON, no labels
      const sections = (a.sectionTimes as Record<string, number>) || {};
      for (const [sel, ms] of Object.entries(sections)) {
        if (typeof ms !== "number") continue;
        const bucket = sectionTimeMap[sel] ?? { totalMs: 0, samples: 0, label: null };
        bucket.totalMs += ms;
        bucket.samples++;
        sectionTimeMap[sel] = bucket;
      }
    }
  }

  // Historical rows written before commit 7d22dc3 carry a selector
  // like "section#idx4" and no label field. The UI displays
  // `label || selector` so those rows show "section#idx4" verbatim,
  // which isn't useful. Prettify by stripping the `section#`
  // prefix and substituting a readable "Section N" form for the
  // idx pattern, while leaving real CSS selectors (#hero,
  // .testimonials) alone.
  function prettifySelector(sel: string): string {
    const idxMatch = sel.match(/^[a-z]+#idx(\d+)$/i);
    if (idxMatch) return `Section ${idxMatch[1]}`;
    if (sel.startsWith("section#")) return "Section " + sel.replace(/^section#/, "");
    return sel;
  }
  const topSections = Object.entries(sectionTimeMap)
    .map(([selector, { totalMs, samples, label }]) => ({
      selector,
      // When no label was captured (historical rows), render a
      // prettified version of the selector so the UI never shows
      // raw "section#idx4" strings.
      label: label || prettifySelector(selector),
      // Average ms per pageview, converted to seconds with one decimal
      avgTime: samples > 0 ? Math.round((totalMs / samples / 1000) * 10) / 10 : 0,
      samples,
    }))
    // Drop tiny noise (< 0.5s avg) so the list shows meaningful sections only
    .filter((s) => s.avgTime >= 0.5)
    .sort((a, b) => b.avgTime - a.avgTime)
    .slice(0, 20);

  // CTA visibility aggregation (ctaEvents was already fetched above).
  // Cap individual samples at 120s. The client-side timer runs off
  // `Date.now() - pageLoad`, which keeps ticking while the user has the
  // tab in the background — a single "oh I forgot about this tab for
  // five minutes" session otherwise dominates the average and inflates
  // 'Avg Time to See' into the 5-6 minute range. Above 120s is almost
  // always idle-tab time, not real scroll-and-engage time.
  const TIME_TO_SEE_CAP_SEC = 120;

  interface CtaAgg {
    page: string;
    elementType: string;
    elementTag: string;
    text: string;
    label: string;
    seen: number;
    sampledForTime: number;
    totalTimeToSee: number;
  }
  const ctaMap: Record<string, CtaAgg> = {};
  for (const e of ctaEvents) {
    const d = e.eventData as Record<string, unknown>;
    const key = `${e.pageUrl}::${d.elementSelector || d.elementType || "unknown"}`;
    if (!ctaMap[key]) {
      ctaMap[key] = {
        page: e.pageUrl,
        elementType: (d.elementType as string) || "unknown",
        elementTag: (d.elementTag as string) || "",
        text: ((d.elementText as string) || "").slice(0, 50),
        label: ((d.elementLabel as string) || "").slice(0, 50),
        seen: 0,
        sampledForTime: 0,
        totalTimeToSee: 0,
      };
    }
    ctaMap[key].seen++;
    // elementTag / elementLabel weren't emitted before this commit; fall
    // back to whatever already got captured so existing rows still render.
    if (!ctaMap[key].elementTag && typeof d.elementTag === "string") ctaMap[key].elementTag = d.elementTag;
    if (!ctaMap[key].label && typeof d.elementLabel === "string") ctaMap[key].label = d.elementLabel.slice(0, 50);
    const t = typeof d.timeToSee === "number" ? d.timeToSee : 0;
    if (t > 0 && t <= TIME_TO_SEE_CAP_SEC) {
      ctaMap[key].totalTimeToSee += t;
      ctaMap[key].sampledForTime++;
    }
  }
  const ctaVisibility = Object.values(ctaMap)
    .map((c) => ({
      page: c.page,
      elementType: c.elementType,
      elementTag: c.elementTag,
      text: c.text,
      label: c.label,
      seen: c.seen,
      avgTimeToSee: c.sampledForTime > 0 ? Math.round((c.totalTimeToSee / c.sampledForTime) * 10) / 10 : 0,
    }))
    .sort((a, b) => b.seen - a.seen)
    .slice(0, 30);

  const elapsed = Date.now() - t0;
  const rowCount = clicks.length + scrolls.length + attention.length + ctaEvents.length;
  console.info("[site-tag-analytics:heatmap] query completed", { elapsed, rowCount, range, brandId, page: page ?? null });

  return NextResponse.json({
    pages: pages.map((p) => p.url),
    clicks: topClicks,
    scrollDepth,
    scrollSamples: scrolls.length,
    attention: topSections,
    ctaVisibility,
  }, { headers: CACHE_HEADERS });
  } catch (err) {
    const elapsed = Date.now() - t0;
    console.error("[site-tag-analytics:heatmap] failed", { elapsed, range, brandId, err: err instanceof Error ? err.message : "unknown" });
    return NextResponse.json({ pages: [], clicks: [], scrollDepth: [], scrollSamples: 0, attention: [], ctaVisibility: [] });
  }
}
