/**
 * POST /api/collect/shopify
 *
 * Shopify Web Pixel ingest endpoint. Called from the Customer Events
 * Web Pixel that runs in Shopify's browser sandbox on the checkout
 * domain. The main-domain Site Tag does NOT load on Shopify's hosted
 * checkout, so this is the only reliable path for purchase capture.
 *
 * Auth: siteId ("ms_...") acts as the shared secret, same pattern as
 * /api/collect. No Clerk session required -- this is a public ingest
 * endpoint, CORS open, must be in INGEST_PATHS in middleware.ts.
 *
 * Payload (JSON body):
 *   {
 *     siteId:    string          -- "ms_..." Site Tag ID
 *     orderId:   string          -- Shopify order ID or name (#1234)
 *     total:     number          -- order total (float)
 *     currency:  string          -- ISO 4217 (USD, GBP, EUR, ...)
 *     lineItems: Array<{         -- purchased items
 *       title: string
 *       quantity: number
 *       price: number
 *       variantId?: string
 *       productId?: string
 *     }>
 *     visitorId?: string         -- meSEO _meseo_vid from localStorage
 *     sessionId?: string         -- meSEO meseo_sid from sessionStorage
 *     checkoutToken?: string     -- Shopify checkout token (optional, for dedup)
 *   }
 *
 * On success: writes one SiteConversion with conversionType="purchase",
 * conversionValue=total, currency, orderId. Line items stored in the
 * dedicated lineItems Json? column (capped at 50 items, sanitized to
 * {title, quantity, price, variantId} only).
 *
 * Attribution: if visitorId is provided (set as a Shopify cart custom
 * attribute by the storefront script), it is used as the visitor key
 * for the SiteConversion row so the order shows up with full channel
 * attribution in the Ecommerce tab. Without it the row is still written
 * but channel/source will be null (shown as "attribution pending" in the UI).
 *
 * Side effects (AI attribution, multi-session touch) are gated behind
 * SITE_TAG_SIDE_EFFECTS_ENABLED, same as the rest of the ingest path.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { boundedFireAndForget } from "@/lib/utils/bounded-async";
import { getCorsHeaders } from "@/lib/services/page-lifecycle";

export const dynamic = "force-dynamic";

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

interface LineItem {
  title: string;
  quantity: number;
  price: number;
  variantId?: string;
  productId?: string;
}

// OPTIONS pre-flight -- Shopify's checkout sandbox sends this before POST.
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);

  let body: {
    siteId?: unknown;
    orderId?: unknown;
    total?: unknown;
    currency?: unknown;
    lineItems?: unknown;
    visitorId?: unknown;
    sessionId?: unknown;
    checkoutToken?: unknown;
  };

  try {
    body = await req.json();
  } catch {
    return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 200, headers: cors });
  }

  // Validate siteId
  const siteId = typeof body.siteId === "string" ? body.siteId.trim() : null;
  if (!siteId || !siteId.startsWith("ms_")) {
    return NextResponse.json({ ok: false, error: "Invalid siteId" }, { status: 200, headers: cors });
  }

  // Validate order total
  const total = typeof body.total === "number" && Number.isFinite(body.total) && body.total >= 0
    ? body.total
    : null;

  const orderId =
    typeof body.orderId === "string" && body.orderId.trim()
      ? body.orderId.trim().slice(0, 255)
      : null;

  const currency =
    typeof body.currency === "string" && /^[A-Za-z]{3}$/.test(body.currency.trim())
      ? body.currency.trim().toUpperCase()
      : null;

  const visitorId =
    typeof body.visitorId === "string" && body.visitorId.trim()
      ? body.visitorId.trim().slice(0, 255)
      : null;

  const sessionId =
    typeof body.sessionId === "string" && body.sessionId.trim()
      ? body.sessionId.trim().slice(0, 100)
      : null;

  // Validate and sanitize line items -- written to dedicated lineItems Json? column.
  // Cap at 50 items; keep only {title, quantity, price, variantId} for a clean schema.
  const rawItems = Array.isArray(body.lineItems) ? body.lineItems : [];
  const lineItems: LineItem[] = rawItems
    .slice(0, 50) // hard cap -- consistent with spec
    .filter(
      (item): item is Record<string, unknown> =>
        item !== null && typeof item === "object",
    )
    .map((item) => ({
      title:
        typeof item.title === "string" ? item.title.slice(0, 255) : "Unknown item",
      quantity:
        typeof item.quantity === "number" && item.quantity > 0 ? Math.floor(item.quantity) : 1,
      price:
        typeof item.price === "number" && Number.isFinite(item.price) ? item.price : 0,
      ...(typeof item.variantId === "string" ? { variantId: item.variantId.slice(0, 100) } : {}),
      ...(typeof item.productId === "string" ? { productId: item.productId.slice(0, 100) } : {}),
    }));

  // Resolve site to brand
  const site = await prisma.trackedSite.findUnique({
    where: { siteId },
    select: { id: true, brandId: true, status: true },
  });
  if (!site || site.status !== "active") {
    // Return 200 so the pixel doesn't log console errors on failure
    return NextResponse.json({ ok: false, error: "Site not found or inactive" }, { status: 200, headers: cors });
  }

  const brandId = site.brandId;

  // Resolve session for attribution -- if the pixel passes a sessionId
  // that matches an existing TrackedSession, link the conversion to it.
  // Otherwise synthesize a stable session key from the orderId so dedup
  // works if the pixel fires twice (e.g. user refreshes the thank-you page).
  let resolvedSessionId = `shopify_pixel:${orderId ?? Date.now()}`;
  if (sessionId) {
    try {
      const existing = await prisma.trackedSession.findUnique({
        where: { sessionId: sessionId },
        select: { sessionId: true },
      });
      if (existing) {
        resolvedSessionId = existing.sessionId;
      }
    } catch {
      // Attribution lookup failure is non-fatal -- proceed with synthetic session key.
    }
  }

  // Build the sourceDetail stored in formFields.
  // Mirrors the pattern in writeConversionSideEffect -- underscore-prefixed
  // synthetic keys + domain data.
  const sourceDetail: Record<string, unknown> = {
    _source:            "shopify_pixel",
    _originalEvent:     "checkout_completed",
    _tier:              "completed",
    _eventType:         "purchase_completed",
    _value:             total,
    _currency:          currency,
    _attributionStatus: visitorId ? "stitched" : "pending",
  };

  // Return 200 immediately -- all DB work below is fire-and-forget so the
  // pixel sandbox doesn't wait on Prisma round-trips.
  void boundedFireAndForget(
    async () => {
      const created = await prisma.siteConversion.create({
        data: {
          brandId,
          visitorId:       visitorId ?? `shopify_pixel:${brandId}:${Date.now()}`,
          sessionId:       resolvedSessionId,
          conversionType:  "purchase",
          pageUrl:         `shopify:${orderId ?? "checkout_completed"}`,
          formProvider:    "shopify_pixel",
          formFields:      sourceDetail as object,
          detectionMethod: "shopify_pixel",
          conversionValue: total,
          currency,
          orderId,
          // Dedicated lineItems column -- sanitized to {title, quantity, price, variantId}
          // Cast needed: Prisma Json? accepts any JSON-serializable value.
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          lineItems:       lineItems.length > 0 ? (lineItems as any) : undefined,
          synthetic:       false,
          timestamp:       new Date(),
        },
        select: { id: true },
      });

      // Post-write side effects: AI attribution stitch (only if we have a
      // real visitorId to link against). Gated behind SIDE_EFFECTS_ENABLED
      // so the kill switch disables them without dropping the purchase write.
      if (SIDE_EFFECTS_ENABLED && visitorId) {
        void boundedFireAndForget(
          async () => {
            const { attributeConversion } = await import("@/lib/services/ai-attribution");
            await attributeConversion({
              brandId,
              sessionId:      resolvedSessionId,
              conversionType: "purchase",
              conversionPage: `shopify:${orderId ?? "checkout_completed"}`,
              conversionId:   created.id,
            });
          },
          "shopify:attributeConversion",
          5000,
        );
      }

      console.info("[collect/shopify] purchase recorded", {
        brandId,
        orderId,
        total,
        currency,
        lineItemCount: lineItems.length,
        hasVisitorId:  !!visitorId,
        hasSessionId:  !!sessionId,
      });
    },
    "shopify:writeConversion",
    8000,
  );

  return NextResponse.json({ ok: true }, { status: 200, headers: cors });
}
