/**
 * POST /api/site-tag/conversion
 *
 * Ingest endpoint for form submits, phone clicks, and booking
 * conversions detected by public/t.js. Resolves the TrackedSite via
 * resolveSiteFromTag, rolls up first/last/assisted touch attribution
 * from the touchpoint history supplied by the client, writes a
 * SiteConversion row, and (best-effort) bumps the daily conversion
 * counter on the matching PageRecord.
 */

import { NextResponse } from "next/server";
import { getCorsHeaders, resolveSiteFromTag } from "@/lib/services/page-lifecycle";
import { prisma } from "@/lib/prisma";
import { canonicalConversionType } from "@/lib/conversion-taxonomy";
import { classifyChannel } from "@/lib/attribution/channel-classifier";
import { findPhoneDuplicateId, findBookingDuplicateId } from "@/lib/services/conversion-resolver";
import { normalizePageUrl } from "@/lib/url-utils";

// Maps formProvider values to the coarse providerCategory stored on the row.
// Lets the Embedded Conversions dashboard group and filter without per-query joins.
const PROVIDER_CATEGORY: Record<string, string> = {
  // Booking
  calendly: "booking", acuity: "booking", nexhealth: "booking",
  patientpop: "booking", simplepractice: "booking", tebra: "booking",
  athenahealth: "booking", mychart: "booking", cerner: "booking",
  doctible: "booking", squareup: "booking",
  // Form builders
  hubspot: "form", typeform: "form", jotform: "form",
  formstack: "form", wufoo: "form", gohighlevel: "form",
  native: "form",
  // SMS
  attentive: "sms", postscript: "sms", klaviyo_sms: "sms",
  smsbump: "sms", recart: "sms", voyage: "sms",
  eztexting: "sms", simpletexting: "sms", twilio: "sms",
  textmagic: "sms", textedly: "sms", slicktext: "sms",
  tatango: "sms", trumpia: "sms", salesmsg: "sms", heymarket: "sms",
  community: "sms", subtext: "sms", textrequest: "sms", sakari: "sms",
  podium_sms: "sms", birdeye_sms: "sms",
  sms_link: "sms", sms_keyword: "sms", explicit_checkbox: "sms",
  two_step_form: "sms", form_field: "sms",
  // Chat
  podium: "chat", intercom: "chat", drift: "chat", zendesk: "chat",
};

export const dynamic = "force-dynamic";

export async function OPTIONS(req: Request) {
  return new NextResponse(null, { status: 204, headers: getCorsHeaders(req) });
}

interface Touchpoint {
  channel?: string;
  source?: string;
  medium?: string | null;
  campaign?: string | null;
  timestamp?: string;
}

interface ConversionBody {
  siteId?: string;
  tagId?: string;
  visitorId?: string;
  sessionId?: string;
  conversionType?: string;
  pageUrl?: string;
  formId?: string | null;
  formProvider?: string | null;
  formFields?: unknown;
  detectionMethod?: string | null;
  phoneNumber?: string | null;
  elementLocation?: string | null;
  firstTouchChannel?: string | null;
  firstTouchSource?: string | null;
  lastTouchChannel?: string | null;
  lastTouchSource?: string | null;
  touchpoints?: Touchpoint[];
  // SMS-specific fields (all optional, never contain personal data)
  smsKeyword?: string | null;
  smsShortcode?: string | null;
  smsConsentMethod?: string | null;
  smsFollowupOfConversionId?: string | null;
  detectionLatencyMs?: number | null;
  successConfirmed?: boolean;
}

export async function POST(req: Request) {
  const cors = getCorsHeaders(req);
  let body: ConversionBody;
  try {
    body = (await req.json()) as ConversionBody;
  } catch {
    return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400, headers: cors });
  }

  const siteId = body.siteId || body.tagId;
  const resolved = await resolveSiteFromTag(siteId, body.pageUrl || "");
  if (!resolved.ok) {
    return NextResponse.json({ ok: false, error: resolved.error }, { status: resolved.status, headers: cors });
  }
  if (!body.conversionType || !body.pageUrl) {
    return NextResponse.json({ ok: false, error: "conversionType and pageUrl required" }, { status: 400, headers: cors });
  }

  const brandId = resolved.site.brandId;

  // ── Server-side intercept rejection (permanent) ───────────────────
  // The fetch/XHR intercepts have been removed from form-phone.js
  // because they fire for every same-origin POST (HubSpot chat, cookie
  // consent, CMS AJAX) — not just form submissions. Reject any events
  // from these methods at the server level too so old cached clients
  // can't pollute the database.
  if (body.detectionMethod === "fetch_intercept" || body.detectionMethod === "xhr_intercept") {
    return NextResponse.json({ ok: true, filtered: true }, { headers: cors });
  }

  // ── Reject deprecated phone_pattern detections (permanent) ────────
  // The phone_pattern method created conversions when phone numbers
  // were merely VISIBLE in the DOM — 865 false records on April 27.
  // New form-phone.js uses tel_click / form_input / clipboard instead.
  // Reject at the server so stale cached Site Tags can't repollute.
  if (body.detectionMethod === "phone_pattern") {
    console.warn("[site-tag/conversion] rejected phone_pattern", { brandId });
    return NextResponse.json({ ok: true, filtered: true, reason: "phone_pattern_deprecated" }, { headers: cors });
  }

  // ── Server-side rapid-fire dedup (permanent) ──────────────────────
  // Multiple detection paths (native submit, fetch intercept, XHR
  // intercept, MutationObserver, postMessage) fire for the same user
  // action. This is a hard gate: only the first form_submit or
  // form_filled for a given session + page in a 30-second window is
  // stored. Subsequent duplicates return 200 without writing.
  //
  // The identity key MUST match what gets written to the DB — the write
  // stores `sessionId: String(body.sessionId || "anon")`, so the dedup
  // uses the exact same expression.
  if (body.conversionType === "form_submit" || body.conversionType === "form_filled") {
    const storedSessionId = String(body.sessionId || "anon");
    const sixtySecondsAgo = new Date(Date.now() - 60_000);
    try {
      const recent = await prisma.siteConversion.findFirst({
        where: {
          brandId,
          sessionId: storedSessionId,
          pageUrl: body.pageUrl,
          conversionType: body.conversionType,
          timestamp: { gte: sixtySecondsAgo },
        },
        select: { id: true },
      });
      if (recent) {
        if (body.successConfirmed === true) {
          try {
            await prisma.siteConversion.update({
              where: { id: recent.id },
              data: { successConfirmed: true },
            });
          } catch { /* best-effort upgrade */ }
        }
        return NextResponse.json({ ok: true, deduped: true }, { headers: cors });
      }
    } catch (err) {
      // If the dedup query fails (DB hiccup), let the write proceed
      // rather than blocking a real conversion. Log for monitoring.
      console.error("[conversion-dedup] query failed, allowing write", err);
    }
  }

  const touchpoints = Array.isArray(body.touchpoints) ? body.touchpoints : [];
  const first = touchpoints[0] || null;
  const last = touchpoints[touchpoints.length - 1] || null;
  const firstTouchChannel = body.firstTouchChannel ?? first?.channel ?? null;
  const firstTouchSource = body.firstTouchSource ?? first?.source ?? null;
  const lastTouchChannel = body.lastTouchChannel ?? last?.channel ?? null;
  const lastTouchSource = body.lastTouchSource ?? last?.source ?? null;

  // Assisted channels: unique channels between first and last touch
  const assistedSet = new Set<string>();
  for (let i = 1; i < Math.max(0, touchpoints.length - 1); i++) {
    const ch = touchpoints[i]?.channel;
    if (ch) assistedSet.add(ch);
  }
  const assistedChannels = Array.from(assistedSet);

  const classification = classifyChannel({
    firstTouchChannel,
    firstTouchSource,
    lastTouchChannel,
    lastTouchSource,
    pageUrl: body.pageUrl,
    touchpoints: touchpoints as unknown as object,
  });

  // Path-agnostic success signal dedup: a thank-you nav fires on the thank-you
  // URL, a different path from the original form submit, so the rapid-fire dedup
  // above won't catch it. Upgrade the most recent form_submitted for this visitor
  // within 5 minutes rather than creating a second row.
  if (body.successConfirmed === true && canonicalConversionType(String(body.conversionType)) === "form_submitted") {
    const storedVisitorId = String(body.visitorId || "anon");
    if (storedVisitorId !== "anon") {
      try {
        const recentSubmit = await prisma.siteConversion.findFirst({
          where: {
            brandId,
            visitorId: storedVisitorId,
            conversionType: "form_submitted",
            isDuplicateOf: null,
            invalidatedReason: null,
            timestamp: { gte: new Date(Date.now() - 300_000) },
          },
          select: { id: true },
          orderBy: { timestamp: "desc" },
        });
        if (recentSubmit) {
          try {
            await prisma.siteConversion.update({ where: { id: recentSubmit.id }, data: { successConfirmed: true } });
          } catch { /* best-effort */ }
          return NextResponse.json({ ok: true, deduped: true, successUpgraded: true }, { headers: cors });
        }
      } catch { /* fall through to normal write path */ }
    }
  }

  // Normalize phone digits once so findPhoneDuplicateId can match across both
  // write paths. Raw tel: href values like "+1-800-555-1234" become "18005551234".
  const normalizedPhoneDigits = body.phoneNumber ? (body.phoneNumber.replace(/\D/g, "") || null) : null;

  // Phone dedup: both t.source.js (->event) and form-phone.js (->here) fire for
  // the same tel: click. Insert the later row with isDuplicateOf pointing to the
  // first rather than dropping it, so every click is recoverable.
  let phoneDuplicateOf: string | null = null;
  const canonicalType = canonicalConversionType(String(body.conversionType)) ?? String(body.conversionType);
  if (canonicalType === "phone_call" && normalizedPhoneDigits) {
    const storedVisitorId = String(body.visitorId || "anon");
    phoneDuplicateOf = await findPhoneDuplicateId(brandId, storedVisitorId, normalizedPhoneDigits, new Date());
  }

  // Booking dedup: both t.source.js (->event) and form-phone.js (->here) fire for
  // the same Calendly/Acuity/NexHealth scheduling confirmation. Insert the later
  // row with isDuplicateOf pointing to the first so the booking stays recoverable.
  let bookingDuplicateOf: string | null = null;
  if (canonicalType === "appointment_booked") {
    const storedVisitorId = String(body.visitorId || "anon");
    bookingDuplicateOf = await findBookingDuplicateId(brandId, storedVisitorId, body.formProvider ?? null, new Date());
  }

  // Same-visitor same-path same-day dedup: widen form_submitted dedup to one
  // calendar day for known visitors, matching CRM per-lead semantics. Excluded
  // when successConfirmed=true (those are success signals, handled above by the
  // path-agnostic upgrade path). Anonymous visitors keep the tight session window.
  let formDuplicateOf: string | null = null;
  if (canonicalType === "form_submitted" && !body.successConfirmed) {
    const storedVisitorId = String(body.visitorId || "anon");
    if (storedVisitorId !== "anon") {
      const normalizedPath = normalizePageUrl(String(body.pageUrl));
      const now = new Date();
      const dayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
      const dayEnd   = new Date(dayStart.getTime() + 86_400_000);
      try {
        // intentionally unfiltered: write-time dedup lookup; needs isDuplicateOf=null rows only, not counted conversions
        const candidates = await prisma.siteConversion.findMany({
          where: {
            brandId,
            visitorId: storedVisitorId,
            conversionType: "form_submitted",
            isDuplicateOf: null,
            timestamp: { gte: dayStart, lt: dayEnd },
          },
          select: { id: true, pageUrl: true },
          orderBy: { timestamp: "asc" },
        });
        const match = candidates.find((r) => normalizePageUrl(r.pageUrl) === normalizedPath);
        formDuplicateOf = match?.id ?? null;
      } catch { /* fall through — allow write on DB error */ }
    }
  }

  try {
    const baseData = {
      brandId: resolved.site.brandId,
      visitorId: String(body.visitorId || "anon"),
      sessionId: String(body.sessionId || "anon"),
      conversionType: canonicalType,
      pageUrl: String(body.pageUrl).slice(0, 2048),
      formId: body.formId ?? null,
      formProvider: body.formProvider ?? null,
      formFields: (body.formFields as object) ?? undefined,
      detectionMethod: body.detectionMethod ?? null,
      phoneNumber: normalizedPhoneDigits,
      isDuplicateOf: phoneDuplicateOf ?? bookingDuplicateOf ?? formDuplicateOf,
      invalidatedReason: formDuplicateOf ? "same_visitor_same_path_day" : null,
      elementLocation: body.elementLocation ?? null,
      firstTouchChannel,
      firstTouchSource,
      lastTouchChannel,
      lastTouchSource,
      touchpoints: touchpoints as unknown as object,
      assistedChannels: assistedChannels as unknown as object,
      classifiedChannel: classification.channel,
      classificationReason: classification.reasonCode,
      classificationEvidence: JSON.parse(JSON.stringify(classification.evidence)),
      providerCategory: (body.formProvider && PROVIDER_CATEGORY[body.formProvider]) || null,
      smsKeyword: typeof body.smsKeyword === "string" ? body.smsKeyword.slice(0, 50) : null,
      smsShortcode: typeof body.smsShortcode === "string" ? body.smsShortcode.slice(0, 20) : null,
      smsConsentMethod: typeof body.smsConsentMethod === "string" ? body.smsConsentMethod.slice(0, 50) : null,
      smsFollowupOfConversionId: typeof body.smsFollowupOfConversionId === "string" ? body.smsFollowupOfConversionId.slice(0, 30) : null,
      detectionLatencyMs: typeof body.detectionLatencyMs === "number" && Number.isFinite(body.detectionLatencyMs) ? Math.round(body.detectionLatencyMs) : null,
      successConfirmed: body.successConfirmed === true,
    };

    try {
      await prisma.siteConversion.create({ data: baseData });
    } catch (classifyErr) {
      // Fallback: if classification columns don't exist yet (migration
      // pending), retry without them so core conversion data isn't lost.
      const { classifiedChannel: _a, classificationReason: _b, classificationEvidence: _c, ...coreData } = baseData;
      await prisma.siteConversion.create({ data: coreData });
      void _a; void _b; void _c;
      console.warn("[site-tag/conversion] classification columns missing — wrote without classification", classifyErr instanceof Error ? classifyErr.message : "");
    }

    // Best-effort: bump daily conversion counter on matching PageRecord
    try {
      const urlPath = new URL(body.pageUrl).pathname || "/";
      const page = await prisma.pageRecord.findUnique({
        where: { brandId_url: { brandId: resolved.site.brandId, url: urlPath } },
        select: { id: true },
      });
      if (page) {
        const day = new Date();
        day.setUTCHours(0, 0, 0, 0);
        await prisma.pageDailyMetric.upsert({
          where: { pageId_date: { pageId: page.id, date: day } },
          create: { pageId: page.id, date: day, conversions: 1 },
          update: { conversions: { increment: 1 } },
        });
      }
    } catch {
      /* ignore — PageRecord increment is best-effort */
    }
  } catch (err) {
    console.error("[site-tag/conversion] write failed", err);
    return NextResponse.json({ ok: false, error: "Write failed" }, { status: 500, headers: cors });
  }

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