/**
 * POST /api/admin/fix-attribution
 *
 * One-time cleanup for the fbclid / ttclid mis-classification bug
 * (fixed in commit 8c84017). Before the fix, the Site Tag's
 * attribution module set channel="paid_social" on any visitor
 * landing with an fbclid or ttclid query param — but Facebook and
 * TikTok append those click IDs to EVERY outbound click (organic
 * shares, Messenger links, profile bio links, embedded videos…),
 * not just paid ad clicks. The result: brands not running paid
 * social ads accumulated SiteConversion rows with first/last
 * touch channel="paid_social" that were really organic Facebook /
 * TikTok referrals.
 *
 * The classifier fix prevents new bad rows from being written; this
 * endpoint cleans up the historical ones.
 *
 * Heuristic — flips channel from "paid_social" to "social" when:
 *   firstTouchChannel  === "paid_social" AND firstTouchSource IN ("facebook", "tiktok")
 *   OR
 *   lastTouchChannel   === "paid_social" AND lastTouchSource  IN ("facebook", "tiktok")
 *   OR
 *   assistedChannels JSON array contains "paid_social" AND any source matched is fb/tt
 *
 * IMPORTANT: this is destructive and partially irreversible. A brand
 * that ACTUALLY runs paid Facebook / TikTok ads has rows of the
 * same shape that should NOT be flipped. The endpoint is per-brand
 * (the requester picks brandId) and dry-run by default — pass
 * `{ execute: true }` only after confirming the brand has no real
 * paid social spend.
 *
 * Body:
 *   { "brandId": "...", "execute"?: true }
 *
 * Response (dry-run, default):
 *   {
 *     dryRun: true, brandId,
 *     wouldUpdate: { firstTouch: N, lastTouch: M, assisted: K },
 *     sample: [{ id, conversionType, firstTouchSource, lastTouchSource, ... }, ...]
 *   }
 *
 * Response (execute):
 *   { dryRun: false, brandId, updated: { firstTouch: N, lastTouch: M, assisted: K } }
 *
 * Requires admin auth. Idempotent — re-running after execute returns
 * 0/0/0 because the affected rows are no longer "paid_social".
 */

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

export const dynamic = "force-dynamic";

const SOCIAL_SOURCES = ["facebook", "tiktok"];

export async function POST(req: Request) {
  const auth = await requireAdmin();
  if (!auth.ok) return NextResponse.json({ error: "Unauthorized" }, { status: auth.status });

  const body = await req.json().catch(() => null) as { brandId?: string; execute?: boolean } | null;
  const brandId = body?.brandId;
  const execute = body?.execute === true;
  if (!brandId) return NextResponse.json({ error: "brandId required" }, { status: 400 });

  const brand = await prisma.brand.findUnique({ where: { id: brandId }, select: { id: true, name: true } });
  if (!brand) return NextResponse.json({ error: "Brand not found" }, { status: 404 });

  // ── Dry-run counts ──────────────────────────────────────────────
  // Done as three separate queries so we can report which touch
  // dimension is producing the bad rows. firstTouch + lastTouch
  // overlap is allowed (a single conversion that landed and
  // converted in one fb/tt visit is counted in both).
  const tCount = Date.now();
  const [firstTouchCount, lastTouchCount] = await Promise.all([
    // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
    prisma.siteConversion.count({
      where: {
        brandId,
        firstTouchChannel: "paid_social",
        firstTouchSource: { in: SOCIAL_SOURCES },
      },
    }),
    // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
    prisma.siteConversion.count({
      where: {
        brandId,
        lastTouchChannel: "paid_social",
        lastTouchSource: { in: SOCIAL_SOURCES },
      },
    }),
  ]);

  // assistedChannels is a JSON column — we can't filter
  // with Prisma's array contains for plain JSON, so scan rows that
  // include "paid_social" anywhere in the JSON via Postgres ILIKE on
  // the cast-to-text representation. Cheap with a small per-brand
  // scan; we don't index this path.
  const assistedRows = await prisma.$queryRaw<Array<{ id: string }>>`
    SELECT id FROM "SiteConversion"
    WHERE "brandId" = ${brandId}
      AND "assistedChannels"::text ILIKE '%paid_social%'
  `.catch(() => []);
  const assistedCount = assistedRows.length;
  console.info(`[fix-attribution] count phase ${Date.now() - tCount}ms`, { brandId, firstTouchCount, lastTouchCount, assistedCount });

  // ── Sample for dry-run output ──────────────────────────────────
  // Show the user 5 candidate rows so they can sanity-check before
  // running with execute: true. Picks the most recent by timestamp.
  // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
  const sample = await prisma.siteConversion.findMany({
    where: {
      brandId,
      OR: [
        { firstTouchChannel: "paid_social", firstTouchSource: { in: SOCIAL_SOURCES } },
        { lastTouchChannel: "paid_social", lastTouchSource: { in: SOCIAL_SOURCES } },
      ],
    },
    select: {
      id: true, conversionType: true, timestamp: true,
      firstTouchChannel: true, firstTouchSource: true,
      lastTouchChannel: true, lastTouchSource: true,
    },
    orderBy: { timestamp: "desc" },
    take: 5,
  }).catch(() => []);

  if (!execute) {
    return NextResponse.json({
      dryRun: true,
      brandId,
      brandName: brand.name,
      wouldUpdate: { firstTouch: firstTouchCount, lastTouch: lastTouchCount, assisted: assistedCount },
      sample,
      hint: "Re-POST with { execute: true } to apply the changes.",
    });
  }

  // ── Apply ──────────────────────────────────────────────────────
  // Three independent updateMany calls — touch-dimension overlap
  // is fine: an already-flipped row falls out of the next match.
  const tApply = Date.now();
  const [firstTouchUpdated, lastTouchUpdated] = await Promise.all([
    prisma.siteConversion.updateMany({
      where: {
        brandId,
        firstTouchChannel: "paid_social",
        firstTouchSource: { in: SOCIAL_SOURCES },
      },
      data: {
        firstTouchChannel: "social",
        firstTouchSource: undefined, // keep the source value (facebook/tiktok)
      },
    }),
    prisma.siteConversion.updateMany({
      where: {
        brandId,
        lastTouchChannel: "paid_social",
        lastTouchSource: { in: SOCIAL_SOURCES },
      },
      data: {
        lastTouchChannel: "social",
      },
    }),
  ]);

  // assistedChannels JSON rewrite — Postgres jsonb_set can't conditionally
  // replace string array members cleanly, so we hand-build the new array
  // per row. assistedChannels is a small list (<= ~20 entries) so this
  // is cheap even for thousands of rows.
  let assistedUpdated = 0;
  if (assistedCount > 0) {
    // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
    const rows = await prisma.siteConversion.findMany({
      where: { id: { in: assistedRows.map((r) => r.id) } },
      select: { id: true, assistedChannels: true },
    });
    for (const row of rows) {
      const channels = Array.isArray(row.assistedChannels) ? row.assistedChannels as string[] : [];
      if (!channels.includes("paid_social")) continue;
      const next = channels.map((c) => c === "paid_social" ? "social" : c);
      // Dedupe in case the row already had "social" alongside "paid_social"
      const deduped = Array.from(new Set(next));
      await prisma.siteConversion.update({
        where: { id: row.id },
        data: { assistedChannels: deduped },
      }).then(() => { assistedUpdated++; }).catch(() => { /* skip bad row */ });
    }
  }
  console.info(`[fix-attribution] apply phase ${Date.now() - tApply}ms`, {
    brandId,
    firstTouch: firstTouchUpdated.count,
    lastTouch: lastTouchUpdated.count,
    assisted: assistedUpdated,
  });

  return NextResponse.json({
    dryRun: false,
    brandId,
    brandName: brand.name,
    updated: {
      firstTouch: firstTouchUpdated.count,
      lastTouch: lastTouchUpdated.count,
      assisted: assistedUpdated,
    },
  });
}
