/**
 * Shared Conversion Metrics
 * ─────────────────────────────
 * Single source of truth for the "how many conversions did this brand
 * have?" question. EVERY surface that shows a conversion number or
 * breakdown should call getConversionMetrics() — the Overview tab, the
 * Conversions tab, the Conversion Funnel tab, and the admin dashboard.
 *
 * Why this exists
 * ───────────────
 * Before this helper, each surface wrote its own query against one of
 * three tables (TrackedEvent, SiteConversion, SiteEvent) with slightly
 * different filters. Operators saw the same brand + same window
 * rendered as "10 form submits" on one page and "341 form submits" on
 * another — a 34× drift driven by which table the surface happened to
 * query.
 *
 * This module standardises on the SiteConversion table because:
 *   1. It uses the 30-second conversion resolver for dedup (same
 *      session + same page + same canonical type = one conversion).
 *   2. It carries source-authority ranking (gtag > plugin > native).
 *   3. It's purpose-built for conversions and receives writes from
 *      every detection path (t.js, form-phone.js, plugin postMessage).
 *
 * Engagement metrics (intent signals without a corresponding
 * SiteConversion row — appointment_intent, booking_widget_opened,
 * form_start, product_view) stay TrackedEvent-backed; see
 * site-tag-analytics.ts for those. This helper covers the "did the
 * user actually convert?" numbers only.
 */

import { prisma } from "@/lib/prisma";
import {
  canonicalConversionType,
  CONVERSION_TYPES,
  type CanonicalConversionType,
} from "@/lib/conversion-taxonomy";
import { countedConversionWhere } from "@/lib/conversions/counted-where";

/** Union of every stored SiteConversion.conversionType value we want to
 *  recognise — Tier 1 + Tier 2 canonical names, plus legacy storage
 *  names still in the table. canonicalConversionType() maps both
 *  directions; this list is just the SQL filter input.
 *
 *  2026-04 production audit confirmed all 7 stored values for the
 *  canary brand (form_submit=341, phone_number_click=184, phone_call=62,
 *  appointment_attempted=29, booking_attempt=8, appointment_booked=2,
 *  form_submission=1 — 627 raw rows) are included in this list. If a
 *  future audit reveals a new conversionType that's NOT in this array,
 *  the `[conversion-metrics:drift]` warning log below will fire on the
 *  first request for that brand — add the new type here immediately
 *  or the dashboard will silently under-count conversions. */
const RECOGNISED_TYPES = [
  // Tier 1 canonical
  "appointment_booked", "form_submitted", "phone_call", "email_contact",
  "sms_contact", "purchase",
  // Tier 2 canonical
  "appointment_attempted", "form_started", "appointment_intent",
  "chat_initiated", "newsletter_signup", "add_to_cart", "checkout_started",
  "file_download", "waitlist_signup",
  // Legacy / pre-resolver
  "form_submit", "form_submission",
  "phone_number_click", "click_to_call",
  "calendly", "booking_confirmed", "booking_attempt",
  "email_click", "sms_click",
];

/** Collapse legacy + canonical conversion-type names to one UI key per
 *  real-world action. Mirrors the mapper in site-tag-analytics.ts so
 *  any surface consuming this helper ends up with the same byType
 *  bucket names (click_to_call, form_submit, booking_confirmed,
 *  purchase, email_click, sms_click, booking_submit, add_to_cart, ...).
 */
function uiKey(raw: string): string {
  if (raw === "appointment_booked")    return "booking_confirmed";
  if (raw === "form_submitted")        return "form_submit";
  if (raw === "phone_call")            return "click_to_call";
  if (raw === "email_contact")         return "email_click";
  if (raw === "sms_contact")           return "sms_click";
  if (raw === "sms_submit")            return "sms_click";
  if (raw === "appointment_attempted") return "booking_submit";
  if (raw === "form_submission")       return "form_submit";
  if (raw === "booking_attempt")       return "booking_submit";
  if (raw === "phone_number_click")    return "click_to_call";
  return raw;
}

export interface ConversionMetrics {
  /** Unique conversions after dedup — unique (session, page, canonical) tuples. */
  total: number;
  /** Raw SiteConversion row count (pre-dedup). Always ≥ total. */
  interactions: number;
  /** Deduped conversion count by UI-canonical key. Sums to `total`. */
  byType: Record<string, number>;
  /** Raw interactions by UI-canonical key. Sums to `interactions`. */
  byTypeInteractions: Record<string, number>;
  /** Tier 1 (hard) conversions — sum of byType entries where tier === 1. */
  hardConversions: number;
  /** Tier 2 (soft) conversions — sum of byType entries where tier === 2. */
  softConversions: number;
  /** Session count over the same window (drives the conversion rate). */
  sessions: number;
  /** Conversion rate as a percentage, (total / sessions) * 100, rounded to 2dp.
   *  0 when sessions === 0 (never NaN). */
  rate: number;
  /** The window's start timestamp — echoed back for logging / caching. */
  since: Date;
  /** Server clock at the time of the query — stable snapshot for the response. */
  now: Date;
}

/**
 * Fetch the canonical conversion metrics for a brand over a time window.
 *
 * Use this EVERYWHERE a conversion number is shown. Surfaces that
 * previously hit TrackedEvent directly for "form_submit count" or
 * "click_to_call count" should switch to this helper so every page
 * agrees on the number.
 *
 * The helper resolves the brand's TrackedSite once and reuses it for
 * the session query. Callers that already have siteId can pass it via
 * `sessionsTrackedSiteId` to skip the lookup.
 */
export async function getConversionMetrics(
  brandId: string,
  since: Date,
  opts: { sessionsTrackedSiteId?: string } = {},
): Promise<ConversionMetrics> {
  // Resolve the brand's TrackedSite if not provided — sessions live on
  // TrackedEvent, scoped by trackedSiteId rather than brandId.
  let trackedSiteId = opts.sessionsTrackedSiteId ?? null;
  if (!trackedSiteId) {
    const site = await prisma.trackedSite.findUnique({
      where: { brandId },
      select: { id: true },
    }).catch(() => null);
    trackedSiteId = site?.id ?? null;
  }

  const now = new Date();
  const [convRows, sessionCount, unfilteredCount] = await Promise.all([
    prisma.siteConversion.findMany({
      where: {
        ...countedConversionWhere(brandId),
        conversionType: { in: RECOGNISED_TYPES },
        timestamp: { gte: since, lte: now },
      },
      select: { sessionId: true, pageUrl: true, conversionType: true },
      take: 200_000,
    }),
    // Sessions come from TrackedSession.count, not from counting
    // TrackedEvent rows with eventType="session_start". The two numbers
    // drift by ~10% (audit: 16,571 TrackedSession vs 15,067 session_start
    // events) because: (a) some sessions never emit session_start
    // (the event is fire-and-forget; a dropped beacon leaves the
    // session row without the event), and (b) a session that
    // re-starts after the 30-min idle timeout writes a new session_start
    // event but the TrackedSession upsert may or may not create a new
    // row depending on the sessionId cookie state. TrackedSession is
    // the authoritative unique-visitor count — every other surface in
    // the platform (getTrafficSummary, getConversionSummary,
    // platform-metrics, site-tag/confidence, brand-radar) uses it, so
    // the shared helper aligns here too.
    trackedSiteId
      ? prisma.trackedSession.count({
          where: {
            trackedSiteId,
            startedAt: { gte: since, lte: now },
          },
        }).catch(() => 0)
      : Promise.resolve(0),
    // Unfiltered raw-count of SiteConversion rows for the brand +
    // window. Used purely as a drift guard below — when this diverges
    // from the filtered convRows.length, a new conversionType has
    // shown up in production that RECOGNISED_TYPES doesn't cover,
    // meaning the dashboard is silently under-counting. Surfaces the
    // missing types so the list can be updated before operators notice.
    prisma.siteConversion.count({
      // intentionally unfiltered: drift guard
      where: { brandId, timestamp: { gte: since, lte: now } },
    }).catch(() => -1),
  ]);

  // Drift guard — if the unfiltered count exceeds the filtered rows,
  // query the unknown types once so they land in the log. Safe: the
  // groupBy is a single aggregate, only fires on drift, and we cap
  // the `NOT IN` list at 64 to avoid pathological SQL.
  if (unfilteredCount > 0 && unfilteredCount > convRows.length) {
    try {
      const missingTypes = await prisma.siteConversion.groupBy({
        // intentionally unfiltered: drift guard
        by: ["conversionType"],
        where: {
          brandId,
          timestamp: { gte: since, lte: now },
          conversionType: { notIn: RECOGNISED_TYPES },
        },
        _count: true,
      });
      console.warn(
        "[conversion-metrics:drift] Unrecognised SiteConversion types dropped from rollup",
        JSON.stringify({
          brandId,
          since: since.toISOString(),
          unfilteredCount,
          filteredCount: convRows.length,
          dropped: unfilteredCount - convRows.length,
          missingTypes: Object.fromEntries(missingTypes.map((r) => [
            r.conversionType,
            typeof r._count === "number" ? r._count : 0,
          ])),
        }),
      );
    } catch { /* best effort — the warning is optional */ }
  }

  // Bucket into byType + byTypeInteractions. Dedup per
  // (sessionId, pageUrl, canonical) — same rule the resolver uses —
  // so a visitor hammering the phone-call button nine times in one
  // session counts as one conversion (nine interactions).
  const byType: Record<string, number> = {};
  const byTypeInteractions: Record<string, number> = {};
  let hardConversions = 0;
  let softConversions = 0;
  const seenKeys = new Set<string>();
  for (const row of convRows) {
    const canon = canonicalConversionType(row.conversionType);
    if (!canon) continue;
    const key = uiKey(row.conversionType);
    byTypeInteractions[key] = (byTypeInteractions[key] ?? 0) + 1;
    const dedupKey = `${row.sessionId || "anon"}|${row.pageUrl}|${canon}`;
    if (seenKeys.has(dedupKey)) continue;
    seenKeys.add(dedupKey);
    byType[key] = (byType[key] ?? 0) + 1;
    const tier = CONVERSION_TYPES[canon as CanonicalConversionType].tier;
    if (tier === 1) hardConversions++;
    else if (tier === 2) softConversions++;
  }
  const total = hardConversions + softConversions;
  const interactions = Object.values(byTypeInteractions).reduce((a, b) => a + b, 0);
  const rate = sessionCount > 0
    ? Math.round((total / sessionCount) * 10_000) / 100
    : 0;

  return {
    total,
    interactions,
    byType,
    byTypeInteractions,
    hardConversions,
    softConversions,
    sessions: sessionCount,
    rate,
    since,
    now,
  };
}

/** Compact log-payload shape. Use this to log "what did this surface
 *  render?" breadcrumbs so conversion-number drift between pages can
 *  be diagnosed from one grep over the Vercel logs. */
export function summariseForLog(metrics: ConversionMetrics): Record<string, unknown> {
  return {
    total: metrics.total,
    interactions: metrics.interactions,
    hardConversions: metrics.hardConversions,
    softConversions: metrics.softConversions,
    sessions: metrics.sessions,
    rate: metrics.rate,
    byType: metrics.byType,
    since: metrics.since.toISOString(),
    now: metrics.now.toISOString(),
  };
}
