/**
 * meSEO Site Tag — Data Reconciliation Service
 * -----------------------------------------------
 * Reconciles events from multiple sources into canonical events.
 *
 * How it works:
 *   1. When a TrackedEvent arrives from Site Tag, a SourceEvent is created
 *      with source="site_tag" and a CanonicalEvent is created for it.
 *   2. When events come from other sources (GA4, GTM, GSC, CRM), they
 *      create SourceEvents and try to match an existing CanonicalEvent
 *      by eventType + pageUrl + timestamp (within a 5-minute window).
 *   3. If a match is found: link to existing CanonicalEvent, mark dupe if
 *      same source already linked. If no match: create new CanonicalEvent.
 *   4. getReconciliationSummary() computes all stats for the frontend.
 */

import { prisma } from "@/lib/prisma";

// ─── Types ───────────────────────────────────────────────────────────────────

export type EventSource = "site_tag" | "ga4" | "gtm" | "gsc" | "crm";

export interface ReconciliationSummary {
  totalEvents: number;
  duplicatesRemoved: number;
  reconciledEvents: number;
  unmatchedEvents: number;
  confidenceScore: number;
  lastReconciledAt: string;
  bySource: Array<{
    source: string;
    label: string;
    events: number;
    matchRate: number;
    status: string;
  }>;
  reconciledStats: {
    matched: number;
    matchedPct: number;
    unmatched: number;
    unmatchedPct: number;
    duplicates: number;
    duplicatesPct: number;
  };
  eventTable: Array<{
    eventType: string;
    sources: string[];
    canonical: string;
    confidence: number;
    journey: string;
    count: number;
  }>;
}

// ─── Ingest a source event and reconcile ─────────────────────────────────────

const MATCH_WINDOW_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Ingest an event from any source and reconcile it against existing
 * canonical events. This is the core reconciliation logic.
 */
export async function ingestSourceEvent(
  trackedSiteId: string,
  source: EventSource,
  eventType: string,
  pageUrl: string,
  timestamp: Date,
  rawData: Record<string, unknown> = {}
): Promise<{ sourceEventId: string; canonicalEventId: string; isDuplicate: boolean }> {
  // Look for an existing canonical event that matches
  const windowStart = new Date(timestamp.getTime() - MATCH_WINDOW_MS);
  const windowEnd = new Date(timestamp.getTime() + MATCH_WINDOW_MS);

  const matchingCanonical = await prisma.canonicalEvent.findFirst({
    where: {
      trackedSiteId,
      eventType,
      pageUrl: { contains: extractPath(pageUrl) },
      timestamp: { gte: windowStart, lte: windowEnd },
    },
    include: { sourceEvents: { select: { source: true } } },
  });

  let canonicalEventId: string;
  let isDuplicate = false;

  if (matchingCanonical) {
    canonicalEventId = matchingCanonical.id;

    // Check if this source already contributed to this canonical event
    const alreadyHasSource = matchingCanonical.sourceEvents.some(
      (se) => se.source === source
    );
    isDuplicate = alreadyHasSource;

    // Boost confidence when multiple sources agree (max 100)
    if (!alreadyHasSource) {
      const sourceCount = matchingCanonical.sourceEvents.length + 1;
      const newConfidence = Math.min(100, 75 + sourceCount * 8);
      await prisma.canonicalEvent.update({
        where: { id: canonicalEventId },
        data: {
          confidence: newConfidence,
          status: sourceCount >= 2 ? "verified" : matchingCanonical.status,
        },
      });
    }
  } else {
    // No match — create a new canonical event
    const canonical = await prisma.canonicalEvent.create({
      data: {
        trackedSiteId,
        eventType,
        pageUrl: extractPath(pageUrl),
        timestamp,
        confidence: source === "site_tag" ? 85 : 75, // Machine Enhancing gets higher baseline
        status: "inferred",
      },
    });
    canonicalEventId = canonical.id;
  }

  // Create the source event
  const sourceEvent = await prisma.sourceEvent.create({
    data: {
      trackedSiteId,
      source,
      eventType,
      pageUrl,
      timestamp,
      isDuplicate,
      rawData: JSON.parse(JSON.stringify(rawData)),
      canonicalEventId,
    },
  });

  return {
    sourceEventId: sourceEvent.id,
    canonicalEventId,
    isDuplicate,
  };
}

// ─── Auto-reconcile from existing TrackedEvents ──────────────────────────────

/**
 * Backfill: take all TrackedEvents that don't have SourceEvents yet
 * and create SourceEvent + CanonicalEvent rows for them.
 * Call this once or on a schedule.
 */
export async function reconcileTrackedEvents(trackedSiteId: string): Promise<number> {
  // Get all tracked events that haven't been reconciled yet
  // We check by looking for source events with matching timestamp + pageUrl
  const events = await prisma.trackedEvent.findMany({
    where: { trackedSiteId },
    orderBy: { timestamp: "asc" },
    take: 500, // Process in batches
  });

  let processed = 0;
  for (const event of events) {
    // Check if a source event already exists for this
    const existing = await prisma.sourceEvent.findFirst({
      where: {
        trackedSiteId,
        source: "site_tag",
        eventType: event.eventType,
        timestamp: event.timestamp,
        pageUrl: event.pageUrl,
      },
    });

    if (!existing) {
      await ingestSourceEvent(
        trackedSiteId,
        "site_tag",
        event.eventType,
        event.pageUrl,
        event.timestamp,
        (event.metadata as Record<string, unknown>) ?? {}
      );
      processed++;
    }
  }

  return processed;
}

// ─── Get reconciliation summary ──────────────────────────────────────────────

const SOURCE_LABELS: Record<string, string> = {
  site_tag: "meSEO Site Tag",
  ga4: "GA4 Key Events",
  gtm: "GTM / gtag",
  gsc: "Google Search Console",
  crm: "CRM",
};

export async function getReconciliationSummary(
  trackedSiteId: string
): Promise<ReconciliationSummary> {
  // All queries are bounded to the last 48h -- this is a "is my tag working
  // right now" summary, not a lifetime audit. The 48h window keeps each query
  // under the (trackedSiteId, timestamp) index rather than full-table scanning
  // millions of rows for high-volume brands.
  const window48h = new Date(Date.now() - 48 * 60 * 60 * 1000);

  // Total source events in window
  const totalEvents = await prisma.sourceEvent.count({
    where: { trackedSiteId, timestamp: { gte: window48h } },
  });

  // Duplicates in window
  const duplicatesRemoved = await prisma.sourceEvent.count({
    where: { trackedSiteId, isDuplicate: true, timestamp: { gte: window48h } },
  });

  // Canonical events by status in window
  const canonicalCounts = await prisma.canonicalEvent.groupBy({
    by: ["status"],
    where: { trackedSiteId, timestamp: { gte: window48h } },
    _count: true,
  });

  const verified = canonicalCounts.find((c) => c.status === "verified")?._count ?? 0;
  const inferred = canonicalCounts.find((c) => c.status === "inferred")?._count ?? 0;
  const unmatched = canonicalCounts.find((c) => c.status === "unmatched")?._count ?? 0;
  const totalCanonical = verified + inferred + unmatched;
  const reconciledEvents = verified + inferred;
  const unmatchedEvents = unmatched;

  // Average confidence in window
  const avgConf = await prisma.canonicalEvent.aggregate({
    where: { trackedSiteId, timestamp: { gte: window48h } },
    _avg: { confidence: true },
  });
  const confidenceScore = Math.round(avgConf._avg.confidence ?? 0);

  // By source in window
  const sourceCounts = await prisma.sourceEvent.groupBy({
    by: ["source"],
    where: { trackedSiteId, timestamp: { gte: window48h } },
    _count: true,
  });

  // Match rate per source in window
  const bySource = await Promise.all(
    sourceCounts.map(async (sc) => {
      const matched = await prisma.sourceEvent.count({
        where: {
          trackedSiteId,
          source: sc.source,
          isDuplicate: false,
          timestamp: { gte: window48h },
          canonicalEvent: { status: { in: ["verified", "inferred"] } },
        },
      });
      const total = sc._count;
      return {
        source: sc.source,
        label: SOURCE_LABELS[sc.source] ?? sc.source,
        events: total,
        matchRate: total > 0 ? Math.round((matched / total) * 100) : 0,
        status: sc.source === "site_tag" ? "active" : "connected",
      };
    })
  );

  // Sort: site_tag first, then by event count
  bySource.sort((a, b) => {
    if (a.source === "site_tag") return -1;
    if (b.source === "site_tag") return 1;
    return b.events - a.events;
  });

  // Reconciliation stats
  const nonDupeTotal = totalEvents - duplicatesRemoved;
  const reconciledStats = {
    matched: reconciledEvents,
    matchedPct: totalCanonical > 0 ? Math.round((reconciledEvents / totalCanonical) * 1000) / 10 : 0,
    unmatched: unmatchedEvents,
    unmatchedPct: totalCanonical > 0 ? Math.round((unmatchedEvents / totalCanonical) * 1000) / 10 : 0,
    duplicates: duplicatesRemoved,
    duplicatesPct: nonDupeTotal > 0 ? Math.round((duplicatesRemoved / totalEvents) * 1000) / 10 : 0,
  };

  // Event table: group canonical events by type in window
  const eventGroups = await prisma.canonicalEvent.groupBy({
    by: ["eventType", "status"],
    where: { trackedSiteId, timestamp: { gte: window48h } },
    _count: true,
    _avg: { confidence: true },
  });

  // Collapse by eventType and gather sources
  const eventMap = new Map<string, { count: number; confidence: number; status: string; sources: Set<string>; journey: string }>();
  for (const g of eventGroups) {
    const existing = eventMap.get(g.eventType);
    if (existing) {
      existing.count += g._count;
      existing.confidence = Math.round((existing.confidence + (g._avg.confidence ?? 0)) / 2);
    } else {
      eventMap.set(g.eventType, {
        count: g._count,
        confidence: Math.round(g._avg.confidence ?? 0),
        status: g.status,
        sources: new Set<string>(),
        journey: getJourneyNote(g.eventType),
      });
    }
  }

  // Get sources for each event type in window
  const sourcesByType = await prisma.sourceEvent.groupBy({
    by: ["eventType", "source"],
    where: { trackedSiteId, isDuplicate: false, timestamp: { gte: window48h } },
  });
  for (const st of sourcesByType) {
    const entry = eventMap.get(st.eventType);
    if (entry) entry.sources.add(SOURCE_LABELS[st.source] ?? st.source);
  }

  const eventTable = Array.from(eventMap.entries())
    .map(([eventType, data]) => ({
      eventType,
      sources: Array.from(data.sources),
      canonical: data.sources.size >= 2 ? "verified" : data.sources.size === 1 ? "single-source" : data.status,
      confidence: data.confidence,
      journey: data.journey,
      count: data.count,
    }))
    .sort((a, b) => b.count - a.count);

  // Most recent source event in window -- bound on createdAt matches the orderBy
  const lastEvent = await prisma.sourceEvent.findFirst({
    where: { trackedSiteId, createdAt: { gte: window48h } },
    orderBy: { createdAt: "desc" },
    select: { createdAt: true },
  });

  return {
    totalEvents,
    duplicatesRemoved,
    reconciledEvents,
    unmatchedEvents,
    confidenceScore,
    lastReconciledAt: lastEvent?.createdAt.toISOString() ?? new Date().toISOString(),
    bySource,
    reconciledStats,
    eventTable,
  };
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function extractPath(fullUrl: string): string {
  try { return new URL(fullUrl).pathname; } catch { return fullUrl; }
}

function getJourneyNote(eventType: string): string {
  const map: Record<string, string> = {
    page_view: "Session → Page → Conversion path",
    session_start: "Entry → Pages viewed → Exit or convert",
    form_submit: "Session → Form start → Submit → CRM lead",
    click_to_call: "Session → CTA click → Phone call → CRM deal",
    outbound_click: "Session → External link → Return or exit",
    form_start: "Field focus → Engagement signal only",
    sms_submit: "SMS link click → Mobile intent signal",
  };
  return map[eventType] ?? "Event tracked";
}
