/**
 * GET /api/site-tag/analytics/errors?brandId=X&range=30d
 *
 * Returns aggregated HTTP errors, broken links, redirects, and resource
 * errors detected by the Site Tag client. Used by the Errors & Redirects
 * tab in Site Tag Analytics.
 */

import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getErrorAnalytics } from "@/lib/services/site-tag-analytics";

export const dynamic = "force-dynamic";

/** Day-count helper used by the runtime section below — the
 *  existing getErrorAnalytics takes a labelled range but our
 *  raw SiteEvent findMany needs a date. */
function rangeToSince(range: string): Date {
  const days = range === "1d" ? 1 : range === "7d" ? 7 : range === "28d" ? 28 : range === "90d" ? 90 : 30;
  return new Date(Date.now() - days * 86_400_000);
}

/** Cache headers for sub-tab analytics routes. 30s fresh absorbs
 *  the typical user flipping between tabs; swr=120 keeps the
 *  previous response usable while a fresh fetch runs in the
 *  background on tab-revisit. private because brand-scoped +
 *  auth-gated. Matches the pattern used by the main
 *  /api/site-tag/analytics route for consistency. */
const CACHE_HEADERS = {
  "Cache-Control": "private, max-age=30, stale-while-revalidate=120",
};

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") as "1d" | "7d" | "30d" | "90d";

  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 });
  }

  try {
    // Existing heuristic-based error streams (http_error / soft_404
    // / broken_link / redirect_detected / legacy resource_error)
    // come through the shared service. The runtime block below is
    // additive — it surfaces the new client-emitted events
    // (js_error, new-format resource_error, page_not_found) that
    // t.js started sending in the CWV + a11y modules. Rendered
    // as a top section on the Errors tab so operators can see
    // real browser errors without the existing sub-tabs changing.
    const since = rangeToSince(range);
    const [legacy, jsErrorRows, resourceErrorRows, notFoundRows] = await Promise.all([
      getErrorAnalytics(brandId, range),
      prisma.siteEvent
        .findMany({
          where: { brandId, eventType: "js_error", timestamp: { gte: since } },
          orderBy: { timestamp: "desc" },
          take: 500,
          select: { eventData: true, timestamp: true, pageUrl: true },
        })
        .catch(() => [] as Array<{ eventData: unknown; timestamp: Date; pageUrl: string }>),
      prisma.siteEvent
        .findMany({
          where: { brandId, eventType: "resource_error", timestamp: { gte: since } },
          orderBy: { timestamp: "desc" },
          take: 500,
          select: { eventData: true, timestamp: true, pageUrl: true },
        })
        .catch(() => [] as Array<{ eventData: unknown; timestamp: Date; pageUrl: string }>),
      prisma.siteEvent
        .findMany({
          where: { brandId, eventType: "page_not_found", timestamp: { gte: since } },
          orderBy: { timestamp: "desc" },
          take: 500,
          select: { eventData: true, timestamp: true, pageUrl: true },
        })
        .catch(() => [] as Array<{ eventData: unknown; timestamp: Date; pageUrl: string }>),
    ]);

    // ── JS errors — grouped by message, capped at 50 ────────
    // t.js emits { message, filename, lineNumber, url }. Dedup
    // caps at 3 per message per session inside t.js; grouping
    // here collapses across sessions so "Cannot read properties
    // of undefined" shows one row with a count rather than 50
    // identical rows.
    type JsErrGroup = {
      message: string;
      filename: string | null;
      lineNumber: number | null;
      pageUrl: string;
      count: number;
      firstSeen: string;
      lastSeen: string;
    };
    const jsMap = new Map<string, JsErrGroup>();
    for (const row of jsErrorRows) {
      const m = (row.eventData ?? {}) as Record<string, unknown>;
      const message = typeof m.message === "string" ? m.message : "Unknown error";
      const filename = typeof m.filename === "string" ? m.filename : null;
      const lineNumber = typeof m.lineNumber === "number" ? m.lineNumber : null;
      const existing = jsMap.get(message);
      if (existing) {
        existing.count++;
        // Keep the MOST RECENT occurrence's filename + page (the
        // event findMany is desc-ordered, so the first time we
        // see a message is the most recent occurrence). First-seen
        // tracks the oldest.
        existing.firstSeen = row.timestamp.toISOString();
      } else {
        jsMap.set(message, {
          message,
          filename,
          lineNumber,
          pageUrl: row.pageUrl,
          count: 1,
          firstSeen: row.timestamp.toISOString(),
          lastSeen: row.timestamp.toISOString(),
        });
      }
    }
    const jsErrors = Array.from(jsMap.values())
      .sort((a, b) => b.count - a.count)
      .slice(0, 50);

    // ── Resource errors (new format) — grouped by URL ───────
    // t.js emits { resource_url, resource_type, duration_ms, url }.
    // Separate bucket from the legacy resourceErrors list (which
    // reads resourceUrl camelCase from a different code path) so
    // the existing sub-tab keeps its historical data and the new
    // section surfaces the new stream.
    type ResErrGroup = {
      resourceUrl: string;
      resourceType: string | null;
      pageUrl: string;
      count: number;
      lastSeen: string;
    };
    const resMap = new Map<string, ResErrGroup>();
    for (const row of resourceErrorRows) {
      const m = (row.eventData ?? {}) as Record<string, unknown>;
      const resourceUrl = typeof m.resource_url === "string" ? m.resource_url : "";
      if (!resourceUrl) continue;
      const resourceType = typeof m.resource_type === "string" ? m.resource_type : null;
      const key = resourceUrl;
      const existing = resMap.get(key);
      if (existing) {
        existing.count++;
      } else {
        resMap.set(key, {
          resourceUrl,
          resourceType,
          pageUrl: row.pageUrl,
          count: 1,
          lastSeen: row.timestamp.toISOString(),
        });
      }
    }
    const runtimeResourceErrors = Array.from(resMap.values())
      .sort((a, b) => b.count - a.count)
      .slice(0, 50);

    // ── 404 pages — grouped by requested_url ────────────────
    type NotFoundGroup = {
      requestedUrl: string;
      referrer: string | null;
      pageUrl: string;
      count: number;
      lastSeen: string;
    };
    const nfMap = new Map<string, NotFoundGroup>();
    for (const row of notFoundRows) {
      const m = (row.eventData ?? {}) as Record<string, unknown>;
      const requestedUrl = typeof m.requested_url === "string" ? m.requested_url : row.pageUrl;
      const referrer = typeof m.referrer === "string" ? m.referrer : null;
      const key = requestedUrl;
      const existing = nfMap.get(key);
      if (existing) {
        existing.count++;
      } else {
        nfMap.set(key, {
          requestedUrl,
          referrer,
          pageUrl: row.pageUrl,
          count: 1,
          lastSeen: row.timestamp.toISOString(),
        });
      }
    }
    const notFounds = Array.from(nfMap.values())
      .sort((a, b) => b.count - a.count)
      .slice(0, 50);

    const runtime = {
      jsErrors,
      resourceErrors: runtimeResourceErrors,
      notFounds,
      totals: {
        jsErrors: jsErrorRows.length,
        resourceErrors: resourceErrorRows.length,
        notFounds: notFoundRows.length,
      },
    };

    const elapsed = Date.now() - t0;
    const rowCount = legacy.httpErrors.length + legacy.brokenLinks.length + legacy.redirects.length + legacy.resourceErrors.length;
    console.info("[site-tag-analytics:errors] query completed", {
      elapsed,
      rowCount,
      jsRows: jsErrorRows.length,
      resRows: resourceErrorRows.length,
      notFoundRows: notFoundRows.length,
      range,
      brandId,
    });
    return NextResponse.json({ ...legacy, runtime }, { headers: CACHE_HEADERS });
  } catch (err) {
    const elapsed = Date.now() - t0;
    console.error("[site-tag-analytics:errors] failed", { elapsed, range, brandId, err: err instanceof Error ? err.message : "unknown" });
    return NextResponse.json({
      httpErrors: [],
      brokenLinks: [],
      redirects: [],
      resourceErrors: [],
      runtime: { jsErrors: [], resourceErrors: [], notFounds: [], totals: { jsErrors: 0, resourceErrors: 0, notFounds: 0 } },
    });
  }
}
