/**
 * POST /api/collect — meSEO Event Collection Endpoint
 * -----------------------------------------------------
 * This is the endpoint the tracker script (t.js) sends events to.
 * It runs WITHOUT authentication — the tracker script on the user's
 * website doesn't have a session.
 *
 * Validation is done via:
 *   1. site_id must exist in the database
 *   2. site must be "active"
 *   3. page URL's domain must be in the allowed domains list
 *
 * Request body:
 *   {
 *     site_id:    string   — the public site ID (e.g. "ms_clxyz...")
 *     event_type: string   — "page_view", "session_start", or custom
 *     page_url:   string   — full URL of the page
 *     referrer:   string?  — document.referrer
 *     timestamp:  string?  — ISO string
 *     session_id: string?  — random session ID from the tracker
 *     metadata:   object?  — extra key-value pairs (UTM params, etc.)
 *   }
 *
 * Response:
 *   { ok: true } on success
 *   { ok: false, error: string } on failure
 *
 * CORS headers are set to allow requests from any origin (the tracker
 * script runs on the customer's website, not on our domain).
 */

import { NextResponse } from "next/server";
import { validateTrackingEvent, recordEvent } from "@/lib/services/site-tag";
import { checkRateLimit } from "@/lib/services/site-tag-cache";
import { getCorsHeaders } from "@/lib/services/page-lifecycle";
import { extractClientContext } from "@/lib/user-agent";
import { prisma } from "@/lib/prisma";
import { log } from "@/lib/logger";
import { boundedFireAndForget } from "@/lib/utils/bounded-async";
import { upsertDetectedIntegration } from "@/lib/site-tag/upsert-detected-integration";
import { upsertDetectedPlatform } from "@/lib/site-tag/upsert-detected-platform";

const SIDE_EFFECTS_ENABLED = process.env.SITE_TAG_SIDE_EFFECTS_ENABLED === "true";

/** Max request body size (64 KB -- accommodates batches of ~100 events) */
const MAX_BODY_SIZE = 65_536;

// Module-level set for O(1) lookup in both single and batch paths.
const ALLOWED_EVENTS = new Set([
  "page_view", "session_start",
  "click_to_call", "sms_click", "phone_number_visible", "call_tracking_detected",
  "file_download", "pdf_opened", "pdf_time_spent", "page_printed",
  "competitor_visit", "competitor_referral", "competitive_journey",
  "outbound_click", "external_conversion_initiated",
  "form_start", "form_submit", "sms_submit", "appointment_intent", "form_intent",
  "form_iframe_present", "booking_widget_opened", "booking_step",
  "booking_submit", "booking_confirmed", "calendar_add",
  "appointment_modify_intent", "availability_check", "waitlist_signup",
  "plugin_detected", "custom_event",
  "purchase", "purchase_completed", "checkout_started", "checkout_step",
  "payment_method_selected", "add_to_cart",
  "product_view", "product_click", "cart_view", "cart_update", "cart_abandoned",
  "chat_widget_detected", "chat_opened", "chat_message_sent",
  "email_click", "email_copied", "newsletter_signup",
  "video_play", "video_progress", "video_complete", "video_engagement",
  "social_share", "content_copied",
  "review_widget_visible", "review_widget_clicked",
  "social_proof_visible", "social_proof_attention",
  "content_engagement", "text_selected", "tab_pattern",
  "site_search", "return_visit", "multi_session_conversion",
  "conversion_path_complete", "paid_click_landing",
  "click", "scroll", "custom",
]);

export const dynamic = "force-dynamic";

/** Handle preflight OPTIONS request */
export async function OPTIONS(req: Request) {
  return new NextResponse(null, { status: 204, headers: getCorsHeaders(req) });
}

export async function POST(req: Request) {
  const cors = getCorsHeaders(req);
  try {
    // ── Size check ──
    const contentLength = parseInt(req.headers.get("content-length") ?? "0", 10);
    if (contentLength > MAX_BODY_SIZE) {
      return NextResponse.json(
        { ok: false, error: "Request too large" },
        { status: 413, headers: cors }
      );
    }

    const body = await req.json();

    // ── Batch path: { site_id, visitor_id?, events: [...] } ─────────────────
    if (Array.isArray(body.events) && body.events.length > 0) {
      const batchSiteId = body.site_id as string;
      if (!batchSiteId) {
        return NextResponse.json({ ok: false, error: "site_id required" }, { status: 400, headers: cors });
      }
      const rateCheck = checkRateLimit(batchSiteId);
      if (!rateCheck.allowed) {
        return NextResponse.json(
          { ok: false, error: "Rate limit exceeded" },
          { status: 429, headers: { ...cors, "Retry-After": String(Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000)) } }
        );
      }
      const firstEvent = body.events[0] as Record<string, unknown>;
      const firstUrl = (firstEvent?.page_url as string) || "";
      const validation = await validateTrackingEvent(batchSiteId, firstUrl);
      if (!validation.valid || !validation.site?.id) {
        return NextResponse.json({ ok: false, dropped: true }, { status: 200, headers: cors });
      }
      const trackedSiteId = validation.site.id;

      // Resolve client session string -> TrackedSession DB cuid, same as the
      // non-starter branch of recordEvent(). One lookup per batch (all events
      // in a buffer flush share the same session). Null if the session row
      // hasn't been created yet -- matches single-event non-starter behavior.
      const clientSessionId = (firstEvent?.session_id as string) || null;
      let dbSessionId: string | null = null;
      if (clientSessionId) {
        const existing = await prisma.trackedSession.findUnique({
          where: { sessionId: clientSessionId.slice(0, 100) },
          select: { id: true },
        });
        if (existing) {
          dbSessionId = existing.id;
          void boundedFireAndForget(
            () => prisma.trackedSession.update({ where: { id: existing.id }, data: { lastSeenAt: new Date() } }),
            "trackedSession.lastSeenAt.batch",
            3000,
          );
        }
      }

      const rows: import("@prisma/client").Prisma.TrackedEventCreateManyInput[] = [];
      for (const e of (body.events as Array<unknown>).slice(0, 200)) {
        if (!e || typeof (e as Record<string, unknown>).event_type !== "string") continue;
        const et = (e as Record<string, unknown>).event_type as string;
        if (!ALLOWED_EVENTS.has(et)) continue;
        const pu = ((e as Record<string, unknown>).page_url as string) || firstUrl;
        if (!pu) continue;
        const tsRaw = (e as Record<string, unknown>).timestamp;
        rows.push({
          eventType: et,
          pageUrl: pu.slice(0, 2048),
          referrer: ((e as Record<string, unknown>).referrer as string) || null,
          timestamp: tsRaw ? new Date(tsRaw as string) : new Date(),
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          metadata: (e as any).metadata ?? {},
          synthetic: false,
          trackedSiteId,
          sessionId: dbSessionId,
        });
      }
      if (!rows.length) {
        return NextResponse.json({ ok: false, error: "No valid events in batch" }, { status: 400, headers: cors });
      }
      await prisma.trackedEvent.createMany({ data: rows });
      return NextResponse.json({ ok: true, count: rows.length }, { headers: cors });
    }

    // ── Single-event path (legacy) ───────────────────────────────────────────
    const siteId = body.site_id as string;
    const eventType = body.event_type as string;
    const pageUrl = body.page_url as string;

    if (!siteId || !eventType || !pageUrl) {
      return NextResponse.json(
        { ok: false, error: "site_id, event_type, and page_url are required" },
        { status: 400, headers: cors }
      );
    }

    if (!ALLOWED_EVENTS.has(eventType)) {
      log.trackingEvent(siteId, eventType, pageUrl, "rejected", `Unknown event type: ${eventType}`);
      return NextResponse.json(
        { ok: false, error: `Unknown event type: ${eventType}` },
        { status: 400, headers: cors }
      );
    }

    // ── Rate limit check (before DB calls) ──
    const rateCheck = checkRateLimit(siteId);
    if (!rateCheck.allowed) {
      log.warn("collect", `Rate limited: ${siteId}`, { metadata: { siteId, retryAfterMs: rateCheck.retryAfterMs } });
      return NextResponse.json(
        { ok: false, error: "Rate limit exceeded" },
        { status: 429, headers: { ...cors, "Retry-After": String(Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000)) } }
      );
    }

    // ── Validate site + domain (uses in-memory cache) ──
    const validation = await validateTrackingEvent(siteId, pageUrl);
    if (!validation.valid) {
      log.trackingEvent(siteId, eventType, pageUrl, "rejected", validation.error);
      return NextResponse.json(
        { ok: false, dropped: true },
        { status: 200, headers: cors }
      );
    }

    // ── Record the event (pass resolved site + brand IDs to avoid a second lookup) ──
    // clientContext comes from the incoming request's headers —
    // User-Agent + forwarded-for IP + Vercel-injected geo headers.
    // recordEvent() only writes these to TrackedSession on the
    // session-starter events (session_start / first page_view), so
    // they're captured once per session and never overwritten.
    const clientContext = extractClientContext(req);
    const incomingMeta = (body.metadata as Record<string, unknown>) || undefined;
    const result = await recordEvent({
      siteId,
      eventType,
      pageUrl,
      referrer: (body.referrer as string) || undefined,
      timestamp: (body.timestamp as string) || undefined,
      sessionId: (body.session_id as string) || undefined,
      visitorId: typeof body.visitor_id === "string" && body.visitor_id ? body.visitor_id : undefined,
      metadata: incomingMeta,
      resolvedSiteDbId: validation.site?.id,
      resolvedBrandId: validation.site?.brandId,
      clientContext,
      synthetic: incomingMeta?._test === true || incomingMeta?.test === true,
    });

    log.trackingEvent(siteId, eventType, pageUrl, "ok");

    // ── page_view platform hook: async upsert into TrackedSite ──────────────
    if (SIDE_EFFECTS_ENABLED && eventType === "page_view" && validation.site?.id) {
      const platformData = incomingMeta?.platform as Record<string, unknown> | undefined;
      if (platformData && typeof platformData.name === "string") {
        void boundedFireAndForget(
          () => upsertDetectedPlatform({
            trackedSiteId: validation.site!.id!,
            platform: {
              name: platformData.name as string,
              version: platformData.version as string | undefined,
              confidence: typeof platformData.confidence === "number" ? platformData.confidence : 50,
              category: ((platformData.category as string) || "other") as import("@/lib/site-tag/platform-signatures").PlatformCategory,
            },
          }),
          "upsertDetectedPlatform",
          8000,
        );
      }
    }

    // ── plugin_detected hook: async upsert into DetectedIntegration ───────────
    if (SIDE_EFFECTS_ENABLED && eventType === "plugin_detected" && validation.site?.brandId) {
      void boundedFireAndForget(
        () => upsertDetectedIntegration({
          brandId: validation.site!.brandId!,
          eventType,
          metadata: incomingMeta as Record<string, unknown> | undefined,
          pageUrl,
          timestamp: new Date(),
        }),
        "upsert-detected-integration",
        8000,
      );
    }

    // ── External converter check ───────────────────────────────────────────────────────────
    // When an outbound_click lands on a registered external converter domain
    // for this brand, fire an additional external_conversion_initiated event
    // so paid-media and signal attribution can count it as a conversion.
    if (eventType === "outbound_click" && validation.site?.brandId) {
      try {
        const meta = (incomingMeta ?? {}) as Record<string, unknown>;
        const linkUrl = (meta.link_url ?? meta.link_domain ?? "") as string;
        const clickDomain = linkUrl.startsWith("http")
          ? new URL(linkUrl).hostname.replace(/^www\./, "")
          : String(linkUrl).replace(/^www\./, "");
        if (clickDomain) {
          const brand = await prisma.brand.findUnique({
            where: { id: validation.site.brandId },
          });
          const rawConverters = brand ? (brand as unknown as { externalConverters: unknown }).externalConverters : null;
          const converters = Array.isArray(rawConverters)
            ? (rawConverters as Array<{ domain: string; nickname: string; isActive: boolean }>)
            : [];
          const match = converters.find((c) => c.isActive && c.domain === clickDomain);
          if (match) {
            await recordEvent({
              siteId,
              eventType: "external_conversion_initiated",
              pageUrl,
              referrer: (body.referrer as string) || undefined,
              sessionId: (body.session_id as string) || undefined,
              metadata: {
                destinationDomain: clickDomain,
                converterNickname: match.nickname,
                sourcePageUrl: pageUrl,
              },
              resolvedSiteDbId: validation.site?.id,
              resolvedBrandId: validation.site?.brandId,
              clientContext,
              synthetic: false,
            });
          }
        }
      } catch { /* external converter check must never break the collect path */ }
    }

    return NextResponse.json(
      { ok: true, eventId: result.eventId },
      { status: 200, headers: cors }
    );
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : "Unknown error";
    // Log with enough context to pinpoint which site/event caused the failure.
    // siteId / eventType may be undefined here if the error fired before parsing.
    log.error("collect", `Collect endpoint error: ${errMsg}`, {
      path: "/api/collect",
      metadata: {
        error: errMsg,
        stack: err instanceof Error ? err.stack : undefined,
        siteId: (err as Record<string, unknown>)?.siteId,
      },
    });
    console.error("[collect] CORE FAILURE", { error: errMsg, stack: err instanceof Error ? err.stack?.split("\n").slice(0, 6).join("\n") : undefined });
    return NextResponse.json(
      { ok: false, error: "Internal server error" },
      { status: 500, headers: cors }
    );
  }
}
