/**
 * POST /api/admin/backfill-gtag-corroboration
 *
 * Retroactively marks historical gtag_intercept form_submitted rows as
 * "uncorroborated_gtag" when they have no corroborating form interaction
 * from the same visitor + page.
 *
 * Background: the gtag event map maps broad GA4 events (sign_up, subscribe,
 * newsletter, conversion, etc.) to form_submit, creating standalone
 * SiteConversion rows for homepage GA events that are not real leads. This
 * backfill marks those rows invalidatedReason="uncorroborated_gtag" so they
 * are excluded from all counted paths (isDuplicateOf IS NULL AND
 * invalidatedReason IS NULL) without deleting data.
 *
 * Corroboration: a row is corroborated when any non-gtag_intercept
 * form_submitted or form_filled exists for the same brand + visitor +
 * normalizePageUrl(pageUrl) within 60 seconds of the gtag row.
 *
 * Idempotent: rows already having invalidatedReason IS NOT NULL are skipped.
 *
 * ── Single-brand mode (default) ──────────────────────────────────────────
 * Body:
 *   brandId   string   required
 *   since     string   ISO date; default 90 days ago
 *   until     string   ISO date; default now
 *   dryRun    boolean  default true (body.dryRun !== false)
 *
 * Response (dryRun): { ok, dryRun, brandId, since, until, candidatesScanned, wouldMark, sampleIds }
 * Response (live):   { ok, dryRun, brandId, since, until, candidatesScanned, marked, sampleIds }
 *
 * ── Sweep mode ───────────────────────────────────────────────────────────
 * Body:
 *   mode          "sweep"
 *   since         string   ISO date; default 90 days ago
 *   until         string   ISO date; default now
 *   dryRun        boolean  default true (body.dryRun !== false)
 *   minRatio      number   minimum wouldMark/scanned to auto-apply; default 0.9
 *   applyBrandIds string[] brands to apply regardless of ratio (optional)
 *
 * dryRun response: { ok, dryRun, mode, brands[], totalScanned, totalWouldMark }
 *   brands sorted by wouldMark desc; each entry: { brandId, scanned, wouldMark, corroborated, ratio }
 *
 * live response: { ok, dryRun:false, mode, marked[], skipped_low_ratio[], totals }
 *   marked: brands where ratio >= minRatio or brandId in applyBrandIds
 *   skipped_low_ratio: brands below threshold for human review
 *   NEVER blanket-applies all brands; low-ratio brands require explicit applyBrandIds.
 *
 * Gated by requireSuperadmin(). Callable from the browser console at
 * app.meseoapp.com while logged in as a superadmin.
 */

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

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

const DEFAULT_MS = 90 * 24 * 60 * 60 * 1_000;
const CORR_WINDOW_MS = 60_000;
const CHUNK_SIZE = 500;
const MAX_CANDIDATES = 20_000;
const DEFAULT_MIN_RATIO = 0.9;

interface BackfillBody {
  mode?: "sweep";
  brandId?: string;
  since?: string;
  until?: string;
  dryRun?: boolean;
  minRatio?: number;
  applyBrandIds?: string[];
}

interface BrandScore {
  brandId: string;
  scanned: number;
  toMark: string[];
  corroborated: number;
}

// Shared corroboration logic for one brand over [since, until].
// Returns the set of candidate IDs to mark plus summary counts.
// Reused by both single-brand mode and sweep mode.
async function scoreBrand(brandId: string, since: Date, until: Date): Promise<BrandScore> {
  // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
  const candidates = await prisma.siteConversion.findMany({
    where: {
      brandId,
      conversionType: "form_submitted",
      formProvider: "gtag_intercept",
      isDuplicateOf: null,
      invalidatedReason: null,
      timestamp: { gte: since, lte: until },
    },
    select: { id: true, visitorId: true, pageUrl: true, timestamp: true, submissionId: true },
    orderBy: { timestamp: "desc" },
    take: MAX_CANDIDATES,
  });

  if (candidates.length === 0) {
    return { brandId, scanned: 0, toMark: [], corroborated: 0 };
  }

  const corrWindowPad  = new Date(since.getTime() - CORR_WINDOW_MS);
  const corrWindowCeil = new Date(until.getTime() + CORR_WINDOW_MS);

  // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
  const corroborators = await prisma.siteConversion.findMany({
    where: {
      brandId,
      conversionType: { in: ["form_submitted", "form_filled"] },
      formProvider: { not: "gtag_intercept" },
      isDuplicateOf: null,
      timestamp: { gte: corrWindowPad, lte: corrWindowCeil },
    },
    select: { visitorId: true, pageUrl: true, timestamp: true, submissionId: true },
  });

  const corrByVisitor = new Map<string, Array<{ path: string; ts: number; submissionId: string | null }>>();
  for (const r of corroborators) {
    if (!r.visitorId || r.visitorId === "anon") continue;
    const path = normalizePageUrl(r.pageUrl);
    const bucket = corrByVisitor.get(r.visitorId);
    if (bucket) {
      bucket.push({ path, ts: r.timestamp.getTime(), submissionId: r.submissionId });
    } else {
      corrByVisitor.set(r.visitorId, [{ path, ts: r.timestamp.getTime(), submissionId: r.submissionId }]);
    }
  }

  const corrSubmissionIds = new Set<string>();
  for (const r of corroborators) {
    if (r.submissionId) corrSubmissionIds.add(r.submissionId);
  }

  const toMark: string[] = [];
  let corroborated = 0;
  for (const cand of candidates) {
    if (cand.submissionId && corrSubmissionIds.has(cand.submissionId)) { corroborated++; continue; }
    if (!cand.visitorId || cand.visitorId === "anon") { corroborated++; continue; }

    const candPath = normalizePageUrl(cand.pageUrl);
    const candTs   = cand.timestamp.getTime();
    const bucket   = corrByVisitor.get(cand.visitorId) ?? [];
    const isCorroborated = bucket.some(
      (r) => r.path === candPath && Math.abs(r.ts - candTs) <= CORR_WINDOW_MS,
    );
    if (isCorroborated) { corroborated++; } else { toMark.push(cand.id); }
  }

  return { brandId, scanned: candidates.length, toMark, corroborated };
}

// Writes the toMark IDs for one brand; returns the count actually updated.
async function applyBrand(toMark: string[]): Promise<number> {
  let marked = 0;
  for (let i = 0; i < toMark.length; i += CHUNK_SIZE) {
    const chunk = toMark.slice(i, i + CHUNK_SIZE);
    const result = await prisma.siteConversion.updateMany({
      where: { id: { in: chunk }, invalidatedReason: null },
      data: { invalidatedReason: "uncorroborated_gtag" },
    });
    marked += result.count;
  }
  return marked;
}

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

  const body = (await req.json().catch(() => ({}))) as BackfillBody;
  const dryRun = body.dryRun !== false;

  const since = body.since ? new Date(body.since) : new Date(Date.now() - DEFAULT_MS);
  const until = body.until ? new Date(body.until) : new Date();

  if (isNaN(since.getTime()) || isNaN(until.getTime())) {
    return NextResponse.json({ ok: false, error: "Invalid since or until date" }, { status: 400 });
  }
  if (since >= until) {
    return NextResponse.json({ ok: false, error: "since must be before until" }, { status: 400 });
  }

  // ── Sweep mode ──────────────────────────────────────────────────────────
  if (body.mode === "sweep") {
    const minRatio = typeof body.minRatio === "number"
      ? Math.max(0, Math.min(1, body.minRatio))
      : DEFAULT_MIN_RATIO;
    const applySet = new Set<string>(Array.isArray(body.applyBrandIds) ? body.applyBrandIds : []);

    // Find all brands that have qualifying candidates in the window.
    // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
    const brandRows = await prisma.siteConversion.findMany({
      where: {
        conversionType: "form_submitted",
        formProvider: "gtag_intercept",
        isDuplicateOf: null,
        invalidatedReason: null,
        timestamp: { gte: since, lte: until },
      },
      select: { brandId: true },
      distinct: ["brandId"],
    });

    const brandIds = brandRows.map((r) => r.brandId);

    if (brandIds.length === 0) {
      return NextResponse.json({
        ok: true,
        dryRun,
        mode: "sweep",
        since: since.toISOString(),
        until: until.toISOString(),
        brands: [],
        totalScanned: 0,
        totalWouldMark: 0,
        hint: "No qualifying gtag_intercept form_submitted rows found across any brand.",
      });
    }

    // Score every brand sequentially to avoid parallel DB saturation.
    const scores = await Promise.all(brandIds.map((id) => scoreBrand(id, since, until)));

    type BrandDryRunEntry = {
      brandId: string;
      scanned: number;
      wouldMark: number;
      corroborated: number;
      ratio: number;
    };

    const brands: BrandDryRunEntry[] = scores
      .map((s) => ({
        brandId: s.brandId,
        scanned: s.scanned,
        wouldMark: s.toMark.length,
        corroborated: s.corroborated,
        ratio: s.scanned > 0 ? s.toMark.length / s.scanned : 0,
      }))
      .sort((a, b) => b.wouldMark - a.wouldMark);

    const totalScanned   = brands.reduce((n, b) => n + b.scanned, 0);
    const totalWouldMark = brands.reduce((n, b) => n + b.wouldMark, 0);

    if (dryRun) {
      return NextResponse.json({
        ok: true,
        dryRun: true,
        mode: "sweep",
        since: since.toISOString(),
        until: until.toISOString(),
        minRatio,
        brands,
        totalScanned,
        totalWouldMark,
        hint: `Dry run: ${brandIds.length} brands scanned, ${totalScanned} rows total, would mark ${totalWouldMark}. Pass dryRun: false to apply brands with ratio >= ${minRatio} (or list them in applyBrandIds).`,
      });
    }

    // Live sweep: apply only brands that pass the ratio gate or are explicitly listed.
    type MarkedEntry = { brandId: string; scanned: number; marked: number; ratio: number };
    type SkippedEntry = { brandId: string; scanned: number; wouldMark: number; ratio: number };

    const markedEntries: MarkedEntry[] = [];
    const skippedEntries: SkippedEntry[] = [];

    for (const s of scores) {
      const ratio = s.scanned > 0 ? s.toMark.length / s.scanned : 0;
      if (ratio >= minRatio || applySet.has(s.brandId)) {
        const marked = await applyBrand(s.toMark);
        markedEntries.push({ brandId: s.brandId, scanned: s.scanned, marked, ratio });
      } else if (s.toMark.length > 0) {
        skippedEntries.push({ brandId: s.brandId, scanned: s.scanned, wouldMark: s.toMark.length, ratio });
      }
    }

    const totalMarked = markedEntries.reduce((n, e) => n + e.marked, 0);

    return NextResponse.json({
      ok: true,
      dryRun: false,
      mode: "sweep",
      since: since.toISOString(),
      until: until.toISOString(),
      minRatio,
      marked: markedEntries,
      skipped_low_ratio: skippedEntries,
      totals: {
        brandsApplied: markedEntries.length,
        brandsSkipped: skippedEntries.length,
        totalMarked,
      },
      hint: skippedEntries.length > 0
        ? `${skippedEntries.length} brand(s) skipped (ratio < ${minRatio}). Review skipped_low_ratio[] and re-run with applyBrandIds to apply them.`
        : `All qualifying brands applied.`,
    });
  }

  // ── Single-brand mode ───────────────────────────────────────────────────
  if (!body.brandId) {
    return NextResponse.json(
      { ok: false, error: "brandId is required (or pass mode: 'sweep' to scan all brands)" },
      { status: 400 },
    );
  }

  const score = await scoreBrand(body.brandId, since, until);

  if (score.scanned === 0) {
    return NextResponse.json({
      ok: true,
      dryRun,
      brandId: body.brandId,
      since: since.toISOString(),
      until: until.toISOString(),
      candidatesScanned: 0,
      wouldMark: 0,
      marked: 0,
      sampleIds: [],
      hint: "No unchecked gtag_intercept form_submitted rows found in range.",
    });
  }

  const sampleIds = score.toMark.slice(0, 20);

  if (dryRun) {
    return NextResponse.json({
      ok: true,
      dryRun: true,
      brandId: body.brandId,
      since: since.toISOString(),
      until: until.toISOString(),
      candidatesScanned: score.scanned,
      wouldMark: score.toMark.length,
      sampleIds,
      hint: `Dry run: scanned ${score.scanned} rows, would mark ${score.toMark.length} as uncorroborated_gtag. Pass dryRun: false to apply.`,
    });
  }

  const marked = await applyBrand(score.toMark);

  return NextResponse.json({
    ok: true,
    dryRun: false,
    brandId: body.brandId,
    since: since.toISOString(),
    until: until.toISOString(),
    candidatesScanned: score.scanned,
    marked,
    sampleIds,
    hint: `Applied: scanned ${score.scanned} rows, marked ${marked} as uncorroborated_gtag.`,
  });
}
