/**
 * POST /api/admin/debug/backfill-conversions[?brandId=<id>]
 *
 * Backfills SiteConversion rows from historical TrackedEvent rows
 * for every conversion-relevant eventType. Catches the gap that
 * existed for brands whose t.js fired conversion events BEFORE the
 * CONVERSION_EVENT_MAP side-effect writer shipped (or while it was
 * temporarily broken) — those events have TrackedEvent rows but no
 * matching SiteConversion, so dashboards under-count.
 *
 * Dedup: skips inserts where a SiteConversion already exists for the
 * same (brandId, sessionId, pageUrl, conversionType) tuple. The
 * resolver's 30-second window isn't appropriate for historical
 * backfill — those timestamps are often hours/days apart — so we
 * use exact-tuple matching instead.
 *
 * Scope: defaults to ALL brands. Pass ?brandId=<id> to scope to a
 * single brand. Processes one eventType at a time per brand, so a
 * failure on one category doesn't skip the rest.
 *
 * Response:
 *   {
 *     results: {
 *       "<Brand Name>": { checked, created, skipped, errors }
 *     },
 *     totals: { brands, checked, created, skipped, errors }
 *   }
 *
 * Superadmin-only. Safe to call repeatedly — idempotent because of
 * the dedup check. Fire-and-forget on individual inserts so one bad
 * row doesn't abort the whole sweep.
 */

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

export const dynamic = "force-dynamic";
// Backfill for a single large brand can touch 10k+ events — give
// the route the full Vercel Pro budget.
export const maxDuration = 300;

/** Maps a TrackedEvent.eventType to the SiteConversion.conversionType
 *  it should write. Matches CONVERSION_EVENT_MAP in site-tag.ts so
 *  the backfilled rows use the same storage names new writes use;
 *  the taxonomy canonicaliser handles them identically at read time. */
const CONVERSION_MAP: Record<string, string> = {
  form_submit: "form_submission",
  click_to_call: "phone_call",
  booking_submit: "booking_attempt",
  booking_confirmed: "booking_confirmed",
  purchase: "purchase",
  purchase_completed: "purchase",
  email_click: "email_contact",
  newsletter_signup: "newsletter_signup",
  sms_click: "sms_contact",
  add_to_cart: "add_to_cart",
  checkout_started: "checkout_started",
  file_download: "file_download",
  waitlist_signup: "waitlist_signup",
};

interface BrandResult {
  brandId: string;
  checked: number;
  created: number;
  skipped: number;
  errors: number;
}

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

  const { searchParams } = new URL(req.url);
  const targetBrandId = searchParams.get("brandId");

  // Scope sites to either the target brand or all brands. Loading
  // the brand name up front keeps the per-brand result keyed on a
  // human-readable label in the log stream.
  const sites = await prisma.trackedSite.findMany({
    where: targetBrandId ? { brandId: targetBrandId } : {},
    select: {
      id: true,
      brandId: true,
      brand: { select: { name: true } },
    },
  });

  const results: Record<string, BrandResult> = {};
  const totals = { brands: sites.length, checked: 0, created: 0, skipped: 0, errors: 0 };

  for (const site of sites) {
    const brandName = site.brand.name;
    const row: BrandResult = { brandId: site.brandId, checked: 0, created: 0, skipped: 0, errors: 0 };
    results[brandName] = row;

    for (const [eventType, conversionType] of Object.entries(CONVERSION_MAP)) {
      const events = await prisma.trackedEvent.findMany({
        where: { trackedSiteId: site.id, eventType },
        select: { sessionId: true, pageUrl: true, timestamp: true, metadata: true },
        take: 50_000,
      });

      // Build a cuid → raw-client-string lookup table. TrackedEvent
      // .sessionId is a FK to TrackedSession.id (a cuid), but
      // SiteConversion.sessionId stores the RAW client session string
      // (the localStorage UUID from t.js). Without this resolution
      // the dedup check would never find existing SiteConversion rows
      // because it'd compare "clxyz..." (cuid) against "sess_abc..."
      // (raw string) — always mismatching. This was the root cause of
      // the 97 TrackedEvent → 6 SiteConversion gap: the backfill ran,
      // found no matches (wrong ID format), but the resolver's 30s
      // window prevented duplicate creates for rapid-fire events.
      const sessionCuids = [...new Set(events.map((e) => e.sessionId).filter(Boolean))] as string[];
      const sessionRows = sessionCuids.length > 0
        ? await prisma.trackedSession.findMany({
            where: { id: { in: sessionCuids } },
            select: { id: true, sessionId: true },
          })
        : [];
      const cuidToRaw = new Map(sessionRows.map((s) => [s.id, s.sessionId]));

      for (const ev of events) {
        row.checked++;
        totals.checked++;
        // Resolve the raw client session string from the cuid FK.
        // Falls back to the cuid itself when TrackedSession doesn't
        // exist (historical rows from before the session-linking
        // code shipped).
        const rawSessionId = (ev.sessionId ? cuidToRaw.get(ev.sessionId) : null) ?? ev.sessionId ?? "anon";
        const pageUrl = (ev.pageUrl ?? "").slice(0, 2048);
        try {
          // Check for existing SiteConversion using the RAW session
          // string — matches both form-phone.js rows and live
          // side-effect rows.
          const existing = await prisma.siteConversion.findFirst({
            where: {
              brandId: site.brandId,
              sessionId: rawSessionId,
              pageUrl,
              conversionType,
            },
            select: { id: true },
          });
          if (existing) {
            row.skipped++;
            totals.skipped++;
            continue;
          }
          await prisma.siteConversion.create({
            data: {
              brandId: site.brandId,
              visitorId: rawSessionId,
              sessionId: rawSessionId,
              conversionType,
              pageUrl,
              formId: null,
              // Attribution / source fields left null — historical
              // data doesn't carry them. formFields records the
              // backfill provenance so the reconciliation view can
              // distinguish these rows from live-written ones.
              formProvider: "backfill",
              formFields: { _source: "backfill", _originalEvent: eventType, _eventType: eventType } as object,
              detectionMethod: `backfill:${eventType}`,
              timestamp: ev.timestamp,
            },
          });
          row.created++;
          totals.created++;
        } catch (err) {
          row.errors++;
          totals.errors++;
          console.warn("[backfill-conversions] insert failed", {
            brandId: site.brandId,
            eventType,
            err: err instanceof Error ? err.message : String(err),
          });
        }
      }
    }

    console.info(
      `[backfill-conversions] ${brandName}: checked=${row.checked} created=${row.created} skipped=${row.skipped} errors=${row.errors}`,
    );
  }

  console.info("[backfill-conversions] totals", JSON.stringify(totals));

  return NextResponse.json({ results, totals });
}
