/**
 * GET /api/admin/analytics/funnel?brandId=X&range=1d|7d|30d|90d
 *
 * Aggregated conversion funnel across every session for a brand in
 * the selected window. Five-stage monotonic funnel derived from
 * TrackedSession + TrackedEvent:
 *
 *   1. First Visit          — every session in the window
 *   2. Page Engagement      — session.pageCount ≥ 2
 *   3. Form / Booking Intent — any form_intent | appointment_intent |
 *                              booking_widget_opened
 *   4. Form / Booking Started — any form_start | booking_step
 *   5. Conversion            — any form_submit | booking_confirmed |
 *                              click_to_call
 *
 * Returned counts are monotonic — a stage-5 session also counts at
 * stages 1-4. Drop-off percentages are computed in the UI so the
 * raw counts here stay easy to diff.
 *
 * Also returns bySource (UTM source / medium, referrer domain,
 * direct) and byEntryPage breakdowns so operators can see where
 * their best-converting traffic lands.
 *
 * Superadmin-gated.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSuperadmin, withAdminTiming } from "@/lib/admin-auth";

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

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

function rangeStart(r: Range): Date {
  const days = r === "1d" ? 1 : r === "7d" ? 7 : r === "90d" ? 90 : 30;
  return new Date(Date.now() - days * 86_400_000);
}

/** Event types we need to classify a session's funnel stage.
 *  Kept as a fixed allowlist so the groupBy below doesn't scan the
 *  whole event stream — just the ones that actually move the
 *  funnel. */
const FUNNEL_EVENT_TYPES = [
  "page_view",
  "form_intent",
  "form_start",
  "form_submit",
  "appointment_intent",
  "booking_widget_opened",
  "booking_step",
  "booking_submit",
  "booking_confirmed",
  "click_to_call",
] as const;

const STAGE3_INTENT = new Set(["form_intent", "appointment_intent", "booking_widget_opened"]);
const STAGE4_STARTED = new Set(["form_start", "booking_step"]);
const STAGE5_CONVERTED = new Set(["form_submit", "booking_confirmed", "click_to_call", "booking_submit"]);

/** Resolve a session's traffic source from its UTM tags +
 *  referrer. Mirrors what analytics platforms do: utm wins, then
 *  referrer host (sans subdomain prefix), then "direct". */
function resolveSource(
  utmSource: string | null,
  utmMedium: string | null,
  referrer: string | null,
): string {
  if (utmSource) {
    const src = utmSource.toLowerCase();
    const med = utmMedium?.toLowerCase();
    return med ? `${src}/${med}` : src;
  }
  if (referrer) {
    try {
      const host = new URL(referrer).host.replace(/^www\./, "");
      return host ? `${host}/referral` : "direct";
    } catch {
      return "referral";
    }
  }
  return "direct";
}

/** Strip query + fragment from an entry-page URL so the byEntryPage
 *  breakdown doesn't shatter one landing page into fifty unique
 *  rows (each with a different utm_campaign suffix). */
function normalisePath(url: string | null | undefined): string {
  if (!url) return "/";
  try {
    const u = new URL(url);
    return u.pathname || "/";
  } catch {
    const q = url.indexOf("?");
    return q >= 0 ? url.slice(0, q) : url;
  }
}

export async function GET(req: Request) {
  const denied = await requireSuperadmin();
  if (denied) return denied;

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

  return NextResponse.json(await withAdminTiming("admin.analytics.funnel", async () => {
    const site = await prisma.trackedSite.findUnique({
      where: { brandId },
      select: { id: true },
    });
    if (!site) {
      return {
        scope: { brandId, range },
        stages: emptyStages(),
        bySource: [],
        byEntryPage: [],
        error: "No TrackedSite for this brand.",
      };
    }

    const from = rangeStart(range);

    // One round-trip: every session in the window + a groupBy on the
    // funnel-relevant events keyed by (sessionId, eventType). The
    // groupBy stays small because the allowlist filters out the
    // long tail of event types we don't classify.
    const [sessions, eventGroups] = await Promise.all([
      prisma.trackedSession.findMany({
        where: { trackedSiteId: site.id, startedAt: { gte: from } },
        select: {
          id: true,
          sessionId: true,
          pageCount: true,
          entryPage: true,
          utmSource: true,
          utmMedium: true,
          referrer: true,
        },
        take: 20_000,
      }).catch(() => []),
      // Filtering by sessionId-in-list is tempting but the list can
      // be 20k items, which explodes the query plan. Scope by
      // trackedSiteId + timestamp instead — the same [brandId,
      // eventType, timestamp] index Postgres already uses elsewhere.
      prisma.trackedEvent.groupBy({
        by: ["sessionId", "eventType"],
        where: {
          trackedSiteId: site.id,
          timestamp: { gte: from },
          eventType: { in: FUNNEL_EVENT_TYPES as unknown as string[] },
          sessionId: { not: null },
        },
        _count: true,
      }).catch(() => [] as Array<{ sessionId: string | null; eventType: string; _count: number }>),
    ]);

    // Build per-session eventType set. sessionId here is the
    // internal cuid (TrackedSession.id), matching the session rows.
    const eventsBySession = new Map<string, Set<string>>();
    for (const g of eventGroups) {
      if (!g.sessionId) continue;
      const set = eventsBySession.get(g.sessionId) ?? new Set<string>();
      set.add(g.eventType);
      eventsBySession.set(g.sessionId, set);
    }

    // Classify each session's max funnel stage.
    // Counters: stages are monotonic — every stage-5 session also
    // counts as stage 4, stage 3, etc.
    let s1 = 0, s2 = 0, s3 = 0, s4 = 0, s5 = 0;
    const bySourceRaw = new Map<string, SourceBucket>();
    const byPageRaw = new Map<string, PageBucket>();

    for (const sess of sessions) {
      const events = eventsBySession.get(sess.id) ?? new Set<string>();
      const reachedStage2 = (sess.pageCount ?? 1) >= 2 || (events.has("page_view") && (eventsBySession.get(sess.id)?.size ?? 0) > 1);
      const reachedStage3 = hasAny(events, STAGE3_INTENT);
      const reachedStage4 = hasAny(events, STAGE4_STARTED);
      const reachedStage5 = hasAny(events, STAGE5_CONVERTED);

      // Monotonic: stage N reached ⇒ every lower stage reached too.
      const maxStage = reachedStage5 ? 5 : reachedStage4 ? 4 : reachedStage3 ? 3 : reachedStage2 ? 2 : 1;
      if (maxStage >= 1) s1++;
      if (maxStage >= 2) s2++;
      if (maxStage >= 3) s3++;
      if (maxStage >= 4) s4++;
      if (maxStage >= 5) s5++;

      const source = resolveSource(sess.utmSource, sess.utmMedium, sess.referrer);
      const sBucket = bySourceRaw.get(source) ?? emptySourceBucket(source);
      sBucket.visits++;
      if (maxStage >= 2) sBucket.engaged++;
      if (maxStage >= 3) sBucket.intent++;
      if (maxStage >= 4) sBucket.started++;
      if (maxStage >= 5) sBucket.converted++;
      bySourceRaw.set(source, sBucket);

      const page = normalisePath(sess.entryPage);
      const pBucket = byPageRaw.get(page) ?? emptyPageBucket(page);
      pBucket.visits++;
      if (maxStage >= 5) pBucket.converted++;
      byPageRaw.set(page, pBucket);
    }

    const bySource = Array.from(bySourceRaw.values())
      .map((b) => ({
        ...b,
        conversionRate: b.visits > 0 ? Math.round((b.converted / b.visits) * 1000) / 10 : 0,
      }))
      .sort((a, b) => b.visits - a.visits)
      .slice(0, 20);

    const byEntryPage = Array.from(byPageRaw.values())
      .map((b) => ({
        ...b,
        conversionRate: b.visits > 0 ? Math.round((b.converted / b.visits) * 1000) / 10 : 0,
      }))
      .sort((a, b) => b.visits - a.visits)
      .slice(0, 20);

    return {
      scope: { brandId, range, fromISO: from.toISOString() },
      stages: [
        { id: 1, label: "First Visit", count: s1 },
        { id: 2, label: "Page Engagement", count: s2 },
        { id: 3, label: "Form / Booking Intent", count: s3 },
        { id: 4, label: "Form / Booking Started", count: s4 },
        { id: 5, label: "Conversion", count: s5 },
      ],
      bySource,
      byEntryPage,
      totals: {
        sessions: s1,
        conversions: s5,
        conversionRate: s1 > 0 ? Math.round((s5 / s1) * 1000) / 10 : 0,
      },
    };
  }));
}

// ── tiny helpers ───────────────────────────────────────────────────────────

function hasAny(set: Set<string>, pool: Set<string>): boolean {
  for (const v of pool) if (set.has(v)) return true;
  return false;
}

interface SourceBucket {
  source: string;
  visits: number;
  engaged: number;
  intent: number;
  started: number;
  converted: number;
}
function emptySourceBucket(source: string): SourceBucket {
  return { source, visits: 0, engaged: 0, intent: 0, started: 0, converted: 0 };
}

interface PageBucket {
  page: string;
  visits: number;
  converted: number;
}
function emptyPageBucket(page: string): PageBucket {
  return { page, visits: 0, converted: 0 };
}

function emptyStages() {
  return [
    { id: 1, label: "First Visit", count: 0 },
    { id: 2, label: "Page Engagement", count: 0 },
    { id: 3, label: "Form / Booking Intent", count: 0 },
    { id: 4, label: "Form / Booking Started", count: 0 },
    { id: 5, label: "Conversion", count: 0 },
  ];
}
