/**
 * POST /api/admin/debug/reclassify-custom-events
 *
 * One-shot retroactive fix for the 256 (and counting) TrackedEvent
 * rows that landed as eventType="custom_event" because the early
 * gtag interceptor used strict equality against CONVERSION_MAP and
 * missed brand-specific event names like "make_an_appointment",
 * "book_appointment2025", "Contact-Us".
 *
 * The Apr-2026 t.js commit (0445989) replaced the strict lookup
 * with a normalised map + a substring pattern fallback. New events
 * route correctly. This endpoint walks the existing custom_event
 * backlog, applies the SAME resolution logic, updates eventType,
 * and writes the missing SiteConversion rows so the dashboard
 * recovers the lost conversion data without a re-tracking pass on
 * the visitor's browser.
 *
 * Body (all optional):
 *   { brandId?, dryRun?, limit? }
 *     - brandId: scope to one brand. Defaults to all brands.
 *     - dryRun: when true (default), only reports what WOULD change.
 *               Pass dryRun=false to apply the updates.
 *     - limit:  cap rows scanned per call (default 5_000, max 50_000).
 *
 * Returns:
 *   { dryRun, scanned, reclassified, byType, conversionsCreated,
 *     skipped: { stillUnknown, alreadyClassified } }
 *
 * Lives under /api/admin/* so the admin-host middleware lets it
 * through. Superadmin-gated. POST-only so a logged-out browser
 * can't trigger it via prefetch.
 */

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 = 60;

// ─── Resolution logic — kept in sync with public/t.js ────────────
//
// Mirror of the GTAG_CONVERSION_MAP + gtagPatternMatch in t.js.
// Duplicated here intentionally — t.js runs in browsers, this runs
// on the server, no shared module to import. If the t.js logic
// evolves, sync these too. The map is normalised once at module
// load to keep per-row work O(1).

const GTAG_CONVERSION_MAP: Record<string, string> = {
  "make_an_appointment":   "booking_submit",
  "makeanappointment":     "booking_submit",
  "make-an-appointment":   "booking_submit",
  "book_appointment":      "booking_submit",
  "bookappointment":       "booking_submit",
  "book_appointment2025":  "booking_submit",
  "bookappointment2025":   "booking_submit",
  "book_appointment_2025": "booking_submit",
  "book-appointment":      "booking_submit",
  "schedule_appointment":  "booking_submit",
  "schedule_visit":        "booking_submit",
  "appointment_request":   "booking_submit",
  "request_appointment":   "booking_submit",
  "appointment_scheduled": "booking_confirmed",
  "appointment_confirmed": "booking_confirmed",
  "booking_confirmed":     "booking_confirmed",
  "contact_us":            "form_submit",
  "contactus":             "form_submit",
  "contact-us":            "form_submit",
  "contact_form_submit":   "form_submit",
  "contact_form":          "form_submit",
  "form_submit":           "form_submit",
  "form_submission":       "form_submit",
  "generate_lead":         "form_submit",
  "lead_form":             "form_submit",
  "sign_up":               "form_submit",
  "signup":                "form_submit",
  "register":              "form_submit",
  "subscribe":             "form_submit",
  "newsletter_signup":     "form_submit",
  "newsletter":            "form_submit",
  "request_quote":         "form_submit",
  "request_a_quote":       "form_submit",
  "free_consultation":     "form_submit",
  "phone_call":            "click_to_call",
  "click_to_call":         "click_to_call",
  "click2call":            "click_to_call",
  "call_click":            "click_to_call",
  "call_now":              "click_to_call",
  "tel_click":             "click_to_call",
  "purchase":              "purchase",
  "order_complete":        "purchase",
  "transaction":           "purchase",
  "begin_checkout":        "checkout_started",
  "checkout_started":      "checkout_started",
  "add_to_cart":           "add_to_cart",
  "addtocart":             "add_to_cart",
};

function normaliseEventName(name: string): string {
  return name.toLowerCase().replace(/[^a-z0-9]/g, "");
}

const NORMALISED_MAP: Record<string, string> = (() => {
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(GTAG_CONVERSION_MAP)) {
    out[normaliseEventName(k)] = v;
  }
  return out;
})();

function gtagPatternMatch(rawName: string): string | null {
  const name = (rawName || "").toLowerCase();
  if (!name) return null;
  if (/appointment|booking|schedule|reservation|reserve_a_/.test(name)) return "booking_submit";
  if (/purchase|order_complete|transaction|checkout_complete/.test(name)) return "purchase";
  if (/begin_checkout|checkout_start/.test(name)) return "checkout_started";
  if (/add_to_cart|addtocart/.test(name)) return "add_to_cart";
  if (/(^|_)call($|_)|click_to_call|phone_call|tel_click|call_now/.test(name)) return "click_to_call";
  if (/contact|form|submit|lead|sign(_|-)?up|subscribe|newsletter|inquiry|enquiry|request/.test(name)) return "form_submit";
  return null;
}

function resolveType(originalEvent: string): { type: string | null; via: "map" | "pattern" | "none" } {
  if (!originalEvent) return { type: null, via: "none" };
  const mapHit = NORMALISED_MAP[normaliseEventName(originalEvent)];
  if (mapHit) return { type: mapHit, via: "map" };
  const patternHit = gtagPatternMatch(originalEvent);
  if (patternHit) return { type: patternHit, via: "pattern" };
  return { type: null, via: "none" };
}

// SiteConversion mapping mirrors src/lib/services/site-tag.ts:
// CONVERSION_EVENT_MAP. Reclassifying a custom_event row to one of
// these eventTypes also requires writing the matching SiteConversion
// so the dashboard's strict-conversion KPI picks it up retroactively.
const CONVERSION_EVENT_MAP: Record<string, string> = {
  form_submit: "form_submission",
  booking_submit: "booking_attempt",
  booking_confirmed: "booking_confirmed",
  purchase: "purchase",
};

interface ReclassifyBody {
  brandId?: string;
  dryRun?: boolean;
  limit?: number;
}

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

  const body = (await req.json().catch(() => ({}))) as ReclassifyBody;
  const dryRun = body.dryRun !== false; // default to dry-run for safety
  const limit = Math.max(1, Math.min(50_000, body.limit ?? 5_000));
  const brandId = body.brandId ?? null;

  return NextResponse.json(await withAdminTiming("debug.reclassify-custom-events", async () => {
    // Resolve trackedSiteId(s) for the scope. custom_event rows live on
    // TrackedEvent which keys off trackedSiteId; we translate from
    // brandId via TrackedSite when scoping to a single brand.
    let trackedSiteIds: string[] | null = null;
    if (brandId) {
      const site = await prisma.trackedSite.findUnique({ where: { brandId }, select: { id: true } });
      if (!site) {
        return { error: `No TrackedSite for brand ${brandId}.` };
      }
      trackedSiteIds = [site.id];
    }

    // Pull the candidate rows. We restrict to gtag_intercept-stamped
    // events so manually-tracked custom events (window.meSEO.track)
    // never get touched. The metadata.source field is set by t.js's
    // handleGtagEvent — anything else is out of scope.
    const candidates = await prisma.trackedEvent.findMany({
      where: {
        ...(trackedSiteIds ? { trackedSiteId: { in: trackedSiteIds } } : {}),
        eventType: "custom_event",
        metadata: { path: ["source"], equals: "gtag_intercept" },
      },
      select: {
        id: true, trackedSiteId: true, sessionId: true, pageUrl: true,
        timestamp: true, metadata: true,
      },
      take: limit,
      orderBy: { timestamp: "desc" },
    });

    // Resolve brandId for each row (needed for SiteConversion writes).
    // One lookup keyed by trackedSiteId, cached locally.
    const brandIdBySite = new Map<string, string>();
    const distinctSiteIds = Array.from(new Set(candidates.map((r) => r.trackedSiteId)));
    if (distinctSiteIds.length > 0) {
      const sites = await prisma.trackedSite.findMany({
        where: { id: { in: distinctSiteIds } },
        select: { id: true, brandId: true },
      });
      for (const s of sites) brandIdBySite.set(s.id, s.brandId);
    }

    // Predicate matching the same junk filter t.js's
    // shouldIgnoreGtagEvent uses. Rows whose original_event starts
    // with one of these prefixes are GTM / Google Optimize / gtag
    // API-introspection internals — they should never have been
    // ingested in the first place. The early gtag interceptor (before
    // commit dffd040) used a strict-equality plumbing list and let
    // the long tail through; this endpoint cleans them up.
    function isGtagJunk(eventName: string): boolean {
      const n = (eventName || "").toLowerCase();
      return n.startsWith("gtm.") || n.startsWith("optimize.") || n.startsWith("gtagapiget");
    }

    const byType: Record<string, number> = {};
    let reclassified = 0;
    let stillUnknown = 0;
    let conversionsCreated = 0;
    let junkRowsDeleted = 0;
    const junkSample: string[] = [];
    const sampleUnknown: string[] = [];

    // Loop sequentially — the workload is small (5K rows max per
    // call), each row is one read of metadata.original_event + one
    // optional update + one optional create. Parallelising would
    // pile on Postgres connections without a meaningful win.
    for (const row of candidates) {
      const meta = (row.metadata ?? {}) as Record<string, unknown>;
      const originalEvent = typeof meta.original_event === "string" ? meta.original_event : "";
      if (!originalEvent) { stillUnknown++; continue; }

      // ── Junk path ──
      // gtm.* / optimize.* / gtagApiGet* rows are GTM / Google
      // Optimize / gtag-API plumbing that leaked through before the
      // prefix filter landed in t.js (commit dffd040). They have no
      // analytic value — DELETE them rather than reclassify. They
      // never had a SiteConversion (custom_event isn't in the
      // conversion map), so cleanup is just one delete per row.
      if (isGtagJunk(originalEvent)) {
        if (junkSample.length < 25 && !junkSample.includes(originalEvent)) {
          junkSample.push(originalEvent);
        }
        if (!dryRun) {
          try {
            await prisma.trackedEvent.delete({ where: { id: row.id } });
            junkRowsDeleted++;
          } catch (err) {
            console.warn("[reclassify-custom-events] junk delete failed", {
              rowId: row.id,
              originalEvent,
              err: err instanceof Error ? err.message : "unknown",
            });
          }
        } else {
          // Dry-run: still count what WOULD be deleted so the operator
          // sees the impact before applying.
          junkRowsDeleted++;
        }
        continue;
      }

      const { type, via } = resolveType(originalEvent);
      if (!type) {
        stillUnknown++;
        if (sampleUnknown.length < 25 && !sampleUnknown.includes(originalEvent)) {
          sampleUnknown.push(originalEvent);
        }
        continue;
      }

      byType[type] = (byType[type] ?? 0) + 1;
      reclassified++;

      if (dryRun) continue;

      // Mutating path. Update the eventType + stamp metadata so a
      // future re-run is idempotent (we can tell already-reclassified
      // rows from genuine custom_events by metadata._reclassified).
      try {
        const newMeta = { ...meta, _reclassified_from: "custom_event", _reclassified_via: via };
        await prisma.trackedEvent.update({
          where: { id: row.id },
          data: {
            eventType: type,
            metadata: JSON.parse(JSON.stringify(newMeta)),
          },
        });

        // Mirror the SiteConversion side-effect from
        // src/lib/services/site-tag.ts:writeConversionSideEffect for
        // conversion-relevant types. Same 60-second dedup gate so a
        // rerun of this endpoint can't double-write.
        const conversionType = CONVERSION_EVENT_MAP[type];
        if (conversionType) {
          const brand = brandIdBySite.get(row.trackedSiteId);
          if (brand) {
            const sessionKey = row.sessionId ?? "anon";
            const sixtySecondsAgo = new Date(row.timestamp.getTime() - 60_000);
            const existing = await prisma.siteConversion.findFirst({
              where: {
                brandId: brand,
                sessionId: sessionKey,
                pageUrl: row.pageUrl,
                conversionType,
                timestamp: { gte: sixtySecondsAgo },
              },
              select: { id: true },
            });
            if (!existing) {
              const formProvider = typeof meta.source === "string" ? meta.source : null;
              await prisma.siteConversion.create({
                data: {
                  brandId: brand,
                  visitorId: sessionKey,
                  sessionId: sessionKey,
                  conversionType,
                  pageUrl: row.pageUrl.slice(0, 2048),
                  formId: null,
                  formProvider,
                  detectionMethod: `t.js:${type} (reclassified from custom_event)`,
                  phoneNumber: null,
                  timestamp: row.timestamp,
                },
              });
              conversionsCreated++;
            }
          }
        }
      } catch (err) {
        console.warn("[reclassify-custom-events] update/insert failed", {
          rowId: row.id,
          err: err instanceof Error ? err.message : "unknown",
        });
      }
    }

    const result = {
      dryRun,
      scope: brandId ?? "all",
      scanned: candidates.length,
      reclassified,
      byType,
      conversionsCreated,
      // gtm.* / optimize.* / gtagApiGet* junk that was either deleted
      // (apply mode) or counted as would-delete (dry-run). junkSample
      // surfaces the actual event names so the operator can sanity-
      // check the prefix filter before applying.
      junk: {
        rowsDeleted: junkRowsDeleted,
        sample: junkSample,
      },
      skipped: {
        stillUnknown,
        alreadyClassified: 0, // rows that already had a non-custom_event type were filtered out by the WHERE
      },
      sampleUnknown,
      hint: dryRun
        ? `Dry run — would reclassify ${reclassified} rows (${Object.keys(byType).length} types) and delete ${junkRowsDeleted} GTM/Optimize junk rows. Pass { dryRun: false } to apply. Stays under the row cap; re-run to keep paging.`
        : `Applied ${reclassified} updates, wrote ${conversionsCreated} new SiteConversion rows, and deleted ${junkRowsDeleted} GTM/Optimize junk rows.`,
    };

    console.info("[reclassify-custom-events] result:", result);
    return result;
  }));
}
