/**
 * GET /api/admin/conversion-reconciliation?brandId=...&range=30d
 *
 * Compares Site Tag conversion counts (SiteConversion + TrackedEvent)
 * against GA4's per-event counts (the ground truth) to surface gaps:
 *
 *   • Site Tag missing X conversions → likely a gtag event name
 *     not yet in CONVERSION_MAP, or events occurred before the gtag
 *     interceptor shipped. Recovery path: add the event name to the
 *     map + run /api/admin/debug/reclassify-custom-events.
 *   • Site Tag exceeds GA4 → expected when gtag interception +
 *     plugin paths catch conversions GA4 misses (Calendly
 *     postMessage events GA4 sees no record of, etc.). Healthy.
 *
 * Calls GA4 directly via the brand's stored OAuth credentials.
 * Skips the comparison gracefully when GA4 isn't connected for the
 * brand.
 *
 * Lives under /api/admin/* so the admin-host middleware lets it
 * through. Superadmin-gated.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSuperadmin, withAdminTiming } from "@/lib/admin-auth";
import { getCredentials } from "@/lib/services/integration-credentials";
import { apiCallAuthed } from "@/lib/integrations/types";
import {
  HARD_CONVERSION_TYPES, CONVERSION_TYPES,
  canonicalConversionType,
  type CanonicalConversionType,
} from "@/lib/conversion-taxonomy";
import { getFormMetrics } from "@/lib/services/platform-metrics";
import { getSiteTagInstalledAt } from "@/lib/services/site-tag";

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

const GA4_API_BASE = "https://analyticsdata.googleapis.com/v1beta";

// GA4 event names we ask about. Pulled from CONVERSION_TYPES.sources
// but stripped of the "gtag:" prefix and the canonical-type names
// (which aren't real GA4 events). Plus the tier-2 form_start / intent
// signals so the report can show the full funnel.
const GA4_EVENTS_OF_INTEREST = (() => {
  const set = new Set<string>();
  for (const def of Object.values(CONVERSION_TYPES)) {
    for (const src of def.sources) {
      if (src.startsWith("gtag:")) set.add(src.slice(5));
    }
  }
  // Add the GA4 standard events we care about even when not in the
  // taxonomy: form_start (engagement), conversion (generic GA4),
  // first_visit (sanity check that the GA4 account is actually live).
  ["form_start", "conversion", "first_visit", "session_start"].forEach((e) => set.add(e));
  return Array.from(set);
})();

interface Ga4EventCount { eventName: string; eventCount: number }

interface RangeWindow { rangeId: string; days: number; sinceISO: string }

function rangeToWindow(rangeId: string): RangeWindow {
  // 28d default matches the app dashboard so both surfaces read the
  // same window. 30d kept as an explicit option for callers that
  // still pass it.
  const days = rangeId === "today" ? 1
    : rangeId === "7d" ? 7
    : rangeId === "28d" ? 28
    : rangeId === "30d" ? 30
    : rangeId === "90d" ? 90
    : 28;
  return {
    rangeId,
    days,
    sinceISO: new Date(Date.now() - days * 86_400_000).toISOString(),
  };
}

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

  const { searchParams } = new URL(req.url);
  const brandId = searchParams.get("brandId");
  const range = searchParams.get("range") ?? "28d";
  if (!brandId) {
    return NextResponse.json({ error: "brandId required" }, { status: 400 });
  }

  const window = rangeToWindow(range);

  return NextResponse.json(await withAdminTiming("conversion-reconciliation", async () => {
    // Resolve the Site Tag install date — earliest TrackedEvent for
    // the brand (fallback: TrackedSite.createdAt). The reconciliation
    // window is clamped to the OVERLAP between the requested range
    // and the Site Tag lifetime, so we never compare a full 30-day
    // GA4 number against a Site Tag that's only been live for 6 days
    // ("15K vs 110K sessions looks terrible but the daily rate is
    // actually 68% capture" problem).
    // Install date from TrackedSite.firstEventAt (the cached column
    // populated by recordEvent() on the very first event for the
    // brand). getSiteTagInstalledAt reads that column first and
    // self-heals against the underlying earliest TrackedEvent when
    // the cache is missing, so this one call replaces the old inline
    // "findUnique TrackedSite + sorted findFirst TrackedEvent" path
    // AND stops falling back to TrackedSite.createdAt (which, for
    // the canary brand, reads Mar 31 — 10 days BEFORE the first
    // actual event on Apr 10, inflating the "active for N days"
    // banner copy by 10 days and pushing the "full comparison
    // available after" date to Apr 30 instead of May 10).
    const installedAt = await getSiteTagInstalledAt(brandId).catch(() => null);

    const rangeSince = new Date(window.sinceISO);
    const since = installedAt && installedAt > rangeSince ? installedAt : rangeSince;
    const now = new Date();
    const effectiveDays = Math.max(
      1,
      Math.ceil((now.getTime() - since.getTime()) / 86_400_000),
    );
    const partialOverlap = installedAt != null && installedAt > rangeSince;

    // ── Site Tag counts ─────────────────────────────────────────
    // SiteConversion: per canonical tier-1 type. Read raw stored
    // conversionType then canonicalise so legacy + new rows fold
    // together correctly under the new taxonomy.
    // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
    const stRaw = await prisma.siteConversion.groupBy({
      by: ["conversionType"],
      where: { brandId, timestamp: { gte: since } },
      _count: true,
    });
    const siteTagByCanonical: Record<CanonicalConversionType, number> = {
      appointment_booked: 0,
      form_submitted: 0,
      phone_call: 0,
      email_contact: 0,
      sms_contact: 0,
      purchase: 0,
      sms_optin: 0,
      sms_community_join: 0,
      appointment_attempted: 0,
      form_started: 0,
      appointment_intent: 0,
      chat_initiated: 0,
      newsletter_signup: 0,
      add_to_cart: 0,
      checkout_started: 0,
      file_download: 0,
      waitlist_signup: 0,
      sms_keyword_displayed: 0,
      sms_outbound_click: 0,
      phone_field_completed: 0,
      sms_optin_followup: 0,
    };
    let siteTagUnknown = 0;
    const unknownTypes: Record<string, number> = {};
    for (const row of stRaw) {
      const canon = canonicalConversionType(row.conversionType);
      if (canon) siteTagByCanonical[canon] += row._count;
      else {
        siteTagUnknown += row._count;
        unknownTypes[row.conversionType] = row._count;
      }
    }
    if (siteTagUnknown > 0) {
      console.warn(`[reconciliation] ${siteTagUnknown} unknown conversionTypes:`, JSON.stringify(unknownTypes));
    }
    const siteTagHard = HARD_CONVERSION_TYPES
      .reduce((sum, k) => sum + siteTagByCanonical[k], 0);

    // form_start events come through TrackedEvent.eventType, not
    // form_start via shared helper — intent signal from TrackedEvent.
    // The shared getFormMetrics returns { starts, submits, completionRate };
    // we only need starts here (submits come from SiteConversion above).
    const formMetrics = await getFormMetrics(brandId, since);
    siteTagByCanonical.form_started += formMetrics.starts;

    // ── GA4 counts ──────────────────────────────────────────────
    const ga4Integration = await prisma.brandIntegration.findFirst({
      where: { brandId, integrationId: "ga4", connected: true },
      select: { id: true },
    });

    let ga4Connected = false;
    let ga4Counts: Record<string, number> = {};
    let ga4Error: string | null = null;
    let ga4PropertyId: string | null = null;

    if (!ga4Integration) {
      ga4Error = "GA4 not connected for this brand. Connect it in Settings → Integrations to see the reconciliation comparison.";
    } else {
      try {
        const creds = await getCredentials(brandId, "ga4");
        ga4PropertyId = (typeof creds?.propertyId === "string" ? creds.propertyId : null);
        if (!creds || !ga4PropertyId) {
          ga4Error = "GA4 credentials missing propertyId — reconnect GA4.";
        } else {
          ga4Connected = true;
          // GA4 takes a date range — use the SAME effective window the
          // Site Tag side aggregates over (since, not window.days-from-
          // now) so we never compare different-length windows.
          const startDate = since.toISOString().slice(0, 10);
          const endDate = new Date().toISOString().slice(0, 10);

          const res = await apiCallAuthed(
            `${GA4_API_BASE}/properties/${ga4PropertyId}:runReport`,
            {
              method: "POST",
              body: {
                dateRanges: [{ startDate, endDate }],
                dimensions: [{ name: "eventName" }],
                metrics: [{ name: "eventCount" }],
                // Filter to our events of interest so we don't pay
                // for the long tail. GA4 returns matched rows only.
                dimensionFilter: {
                  filter: {
                    fieldName: "eventName",
                    inListFilter: { values: GA4_EVENTS_OF_INTEREST },
                  },
                },
                limit: "100",
              },
            },
            { brandId, integrationId: "ga4" },
          );

          if (res.ok) {
            const data = res.data as { rows?: Array<{
              dimensionValues?: Array<{ value: string }>;
              metricValues?: Array<{ value: string }>;
            }> };
            const counts: Record<string, number> = {};
            for (const row of data.rows ?? []) {
              const name = row.dimensionValues?.[0]?.value;
              const count = parseInt(row.metricValues?.[0]?.value ?? "0", 10);
              if (name) counts[name] = count;
            }
            ga4Counts = counts;
          } else {
            ga4Error = `GA4 query failed: ${res.error}`;
          }
        }
      } catch (err) {
        ga4Error = err instanceof Error ? err.message : "GA4 query failed";
      }
    }

    // ── Roll GA4 raw events up into canonical tier-1 buckets ──
    const ga4ByCanonical: Record<CanonicalConversionType, number> = {
      appointment_booked: 0,
      form_submitted: 0,
      phone_call: 0,
      email_contact: 0,
      sms_contact: 0,
      purchase: 0,
      sms_optin: 0,
      sms_community_join: 0,
      appointment_attempted: 0,
      form_started: 0,
      appointment_intent: 0,
      chat_initiated: 0,
      newsletter_signup: 0,
      add_to_cart: 0,
      checkout_started: 0,
      file_download: 0,
      waitlist_signup: 0,
      sms_keyword_displayed: 0,
      sms_outbound_click: 0,
      phone_field_completed: 0,
      sms_optin_followup: 0,
    };
    let ga4OtherEvents = 0;
    for (const [name, count] of Object.entries(ga4Counts)) {
      const canon = canonicalConversionType(`gtag:${name}`) ?? canonicalConversionType(name);
      if (canon) {
        ga4ByCanonical[canon] += count;
      } else if (name === "form_start") {
        ga4ByCanonical.form_started += count;
      } else {
        ga4OtherEvents += count;
      }
    }
    const ga4Hard = HARD_CONVERSION_TYPES
      .reduce((sum, k) => sum + ga4ByCanonical[k], 0);

    // ── Gap analysis ────────────────────────────────────────────
    // For each tier-1 type, compute capture rate = siteTag / ga4 × 100.
    // > 100% is healthy (Site Tag catching plugin events GA4 misses).
    // < 90% is a concern (likely an unmapped gtag event name).
    const gaps: Array<{
      type: CanonicalConversionType;
      label: string;
      ga4: number;
      siteTag: number;
      missing: number;
      captureRate: string;
    }> = [];
    for (const k of HARD_CONVERSION_TYPES) {
      const ga4Count = ga4ByCanonical[k];
      const stCount = siteTagByCanonical[k];
      const missing = Math.max(0, ga4Count - stCount);
      const rate = ga4Count > 0
        ? Math.round((stCount / ga4Count) * 1000) / 10
        : (stCount > 0 ? 100 : 0);
      gaps.push({
        type: k,
        label: CONVERSION_TYPES[k].label,
        ga4: ga4Count,
        siteTag: stCount,
        missing,
        captureRate: ga4Connected ? `${rate}%` : "n/a",
      });
    }

    const overallMissing = ga4Connected ? Math.max(0, ga4Hard - siteTagHard) : 0;
    const overallRate = ga4Connected
      ? (ga4Hard > 0
          ? `${Math.round((siteTagHard / ga4Hard) * 1000) / 10}%`
          : (siteTagHard > 0 ? "100%" : "n/a"))
      : "n/a";

    // ── Diagnosis (human-readable findings) ─────────────────────
    const diagnosis: string[] = [];
    if (!ga4Connected) {
      diagnosis.push(ga4Error ?? "GA4 not connected — comparison skipped.");
    } else {
      for (const g of gaps) {
        if (g.ga4 === 0 && g.siteTag === 0) continue;
        if (g.missing > 0 && g.ga4 > 0 && g.missing / g.ga4 > 0.3) {
          diagnosis.push(
            `${g.missing} ${g.label.toLowerCase()} conversions missing — likely a gtag event name not yet in CONVERSION_MAP. Capture rate ${g.captureRate}.`
          );
        } else if (g.siteTag > g.ga4 && g.ga4 > 0) {
          const pct = Math.round((g.siteTag - g.ga4) / g.ga4 * 1000) / 10;
          diagnosis.push(
            `${g.label}: Site Tag (${g.siteTag}) > GA4 (${g.ga4}) by ${pct}% — likely plugin / postMessage paths catching what GA4 misses. Healthy.`
          );
        }
      }
      if (siteTagUnknown > 0) {
        diagnosis.push(
          `${siteTagUnknown} SiteConversion rows have an unrecognised conversionType — add the source to src/lib/conversion-taxonomy.ts.`
        );
      }
      if (ga4OtherEvents > 0) {
        diagnosis.push(
          `${ga4OtherEvents} GA4 events of interest didn't map to a canonical type — review GA4 event names that don't appear in CONVERSION_TYPES.sources.`
        );
      }
      if (diagnosis.length === 0) {
        diagnosis.push("No significant gaps — Site Tag and GA4 are in agreement within tolerance.");
      }
    }

    // Prepend an install-date note to the diagnosis list when the
    // window is a partial overlap. Operators see the caveat FIRST,
    // before the per-type findings.
    if (partialOverlap && installedAt) {
      const fullAvail = new Date(installedAt.getTime() + window.days * 86_400_000);
      diagnosis.unshift(
        `Note: Site Tag has been active for ${effectiveDays} day${effectiveDays === 1 ? "" : "s"} (since ${installedAt.toISOString().slice(0, 10)}). Full ${window.days}-day comparison available after ${fullAvail.toISOString().slice(0, 10)}.`,
      );
    }
    // If Site Tag is capturing MORE conversions than GA4 within the
    // overlap window, surface that as a WIN rather than burying it
    // in the generic gaps loop.
    if (ga4Connected && ga4Hard > 0 && siteTagHard > ga4Hard) {
      const pct = Math.round(((siteTagHard - ga4Hard) / ga4Hard) * 1000) / 10;
      diagnosis.unshift(
        `Site Tag is capturing ${pct}% more conversions than GA4 — gtag interception + plugin paths are catching conversions GA4 misses.`,
      );
    }

    const result = {
      scope: {
        brandId,
        range,
        days: window.days,
        sinceISO: window.sinceISO,
        // Effective window metadata — the comparison actually
        // aggregated over max(rangeStart, installedAt) → now.
        effectiveSinceISO: since.toISOString(),
        effectiveDays,
        installedAt: installedAt?.toISOString() ?? null,
        partialOverlap,
      },
      ga4: {
        connected: ga4Connected,
        propertyId: ga4PropertyId,
        error: ga4Error,
        rawCounts: ga4Counts,
        byCanonical: ga4ByCanonical,
        totalConversions: ga4Hard,
        otherEvents: ga4OtherEvents,
      },
      siteTag: {
        byCanonical: siteTagByCanonical,
        totalConversions: siteTagHard,
        unknown: siteTagUnknown,
        unknownTypes: Object.keys(unknownTypes).length > 0 ? unknownTypes : undefined,
      },
      gaps: {
        perType: gaps,
        overall: {
          ga4: ga4Hard,
          siteTag: siteTagHard,
          missing: overallMissing,
          captureRate: overallRate,
          // Daily rates over the EFFECTIVE (overlap) window so the UI
          // can render "2,529/day vs 3,680/day — 68.7% capture" instead
          // of the misleading raw-total view.
          siteTagPerDay: Math.round((siteTagHard / effectiveDays) * 10) / 10,
          ga4PerDay: Math.round((ga4Hard / effectiveDays) * 10) / 10,
        },
      },
      diagnosis,
      generatedAt: new Date().toISOString(),
    };

    console.info("[conversion-reconciliation]", {
      brandId, range, ga4Hard, siteTagHard,
      overallMissing, overallRate,
    });

    return result;
  }));
}
