/**
 * Visitor Profile + Household Cluster Builder
 * ────────────────────────────────────────────
 * Rebuilds the VisitorProfile and HouseholdCluster read-cache tables
 * from the source-of-truth tables (TrackedSession, SiteConversion).
 *
 * Both tables are pure aggregations — they can be dropped and rebuilt
 * at any time without data loss. The builder is idempotent (upserts
 * by brandId + visitorId / subnet + city).
 *
 * Called by:
 *   - POST /api/paid-media/attribution (manual trigger)
 *   - /api/cron/paid-media-rebuild (scheduled)
 */

import { prisma } from "@/lib/prisma";
import { classifyChannel } from "./attribution";

// ─── Visitor Profile Builder ────────────────────────────────────

export interface ProfileBuildResult {
  profilesCreated: number;
  profilesUpdated: number;
  errors: number;
}

export async function rebuildVisitorProfiles(
  brandId: string,
  opts: { since?: Date } = {},
): Promise<ProfileBuildResult> {
  const since = opts.since ?? new Date(Date.now() - 90 * 86_400_000);
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return { profilesCreated: 0, profilesUpdated: 0, errors: 0 };

  // Cap at 10k sessions per brand per rebuild pass. Larger brands
  // process across multiple cron runs (the upsert is idempotent so
  // each pass picks up where the last left off as new sessions
  // arrive). Previously 100k — caused the cron to OOM/timeout on
  // brands with 30d+ history.
  const SESSION_BATCH = 10_000;
  const sessions = await prisma.trackedSession.findMany({
    where: { trackedSiteId: site.id, startedAt: { gte: since } },
    select: {
      sessionId: true, startedAt: true, lastSeenAt: true, pageCount: true,
      device: true, browser: true, os: true, ipAddress: true,
      utmSource: true, utmMedium: true, utmCampaign: true,
      referrer: true, gclid: true, fbclid: true, msclkid: true,
      ttclid: true, li_fat_id: true, dclid: true, wbraid: true, gbraid: true,
    },
    orderBy: { startedAt: "asc" },
    take: SESSION_BATCH,
  });

  // intentionally unfiltered: internal service; uses raw counts intentionally
  const conversions = await prisma.siteConversion.findMany({
    where: { brandId, timestamp: { gte: since } },
    select: {
      visitorId: true, conversionType: true, pageUrl: true,
      conversionValue: true, timestamp: true,
    },
    take: SESSION_BATCH,
  });

  // Group sessions by sessionId (which is the visitor's localStorage UUID
  // for session_start events, or the per-tab UUID for others). The
  // TrackedSession.sessionId IS the visitor identifier in the current
  // t.js implementation.
  const byVisitor = new Map<string, typeof sessions>();
  for (const s of sessions) {
    const arr = byVisitor.get(s.sessionId) ?? [];
    arr.push(s);
    byVisitor.set(s.sessionId, arr);
  }

  const convByVisitor = new Map<string, typeof conversions>();
  for (const c of conversions) {
    const arr = convByVisitor.get(c.visitorId) ?? [];
    arr.push(c);
    convByVisitor.set(c.visitorId, arr);
  }

  const allVisitorIds = new Set([...byVisitor.keys(), ...convByVisitor.keys()]);
  const result: ProfileBuildResult = { profilesCreated: 0, profilesUpdated: 0, errors: 0 };

  for (const visitorId of allVisitorIds) {
    try {
      const vSessions = byVisitor.get(visitorId) ?? [];
      const vConversions = convByVisitor.get(visitorId) ?? [];

      const devices = new Map<string, { device: string | null; browser: string | null; os: string | null }>();
      const ips = new Set<string>();
      const channels: Array<{ channel: string; source: string | null; medium: string | null; date: string }> = [];
      let totalPageViews = 0;
      let firstSeen = new Date();
      let lastSeen = new Date(0);

      for (const s of vSessions) {
        if (s.startedAt < firstSeen) firstSeen = s.startedAt;
        if (s.lastSeenAt > lastSeen) lastSeen = s.lastSeenAt;
        totalPageViews += s.pageCount;
        const devKey = `${s.device ?? "?"}|${s.browser ?? "?"}|${s.os ?? "?"}`;
        if (!devices.has(devKey)) devices.set(devKey, { device: s.device, browser: s.browser, os: s.os });
        if (s.ipAddress) ips.add(s.ipAddress);
        channels.push({
          channel: classifyChannel(s),
          source: s.utmSource,
          medium: s.utmMedium,
          date: s.startedAt.toISOString().split("T")[0],
        });
      }

      const convHistory = vConversions.map((c) => ({
        type: c.conversionType,
        pageUrl: c.pageUrl,
        date: c.timestamp.toISOString().split("T")[0],
        value: c.conversionValue,
      }));

      const topPageCounts = new Map<string, number>();
      // We don't have per-page breakdown here — just use session entry
      // pages as a proxy for "most visited pages"
      for (const s of vSessions) {
        // sessionId-keyed sessions don't carry per-page arrays; use
        // the session's entryPage from TrackedSession if available.
        // For now, skip top-pages (it requires a TrackedEvent join
        // that's too expensive for a bulk rebuild).
      }

      await prisma.visitorProfile.upsert({
        where: { brandId_visitorId: { brandId, visitorId } },
        create: {
          brandId,
          visitorId,
          firstSeenAt: firstSeen,
          lastSeenAt: lastSeen,
          totalSessions: vSessions.length,
          totalPageViews,
          totalConversions: vConversions.length,
          devices: [...devices.values()] as object,
          ipAddresses: [...ips] as object,
          channelHistory: channels as object,
          conversionHistory: convHistory as object,
          topPages: [] as object,
        },
        update: {
          lastSeenAt: lastSeen,
          totalSessions: vSessions.length,
          totalPageViews,
          totalConversions: vConversions.length,
          devices: [...devices.values()] as object,
          ipAddresses: [...ips] as object,
          channelHistory: channels as object,
          conversionHistory: convHistory as object,
        },
      });
      result.profilesCreated++;
    } catch (err) {
      result.errors++;
      console.warn("[visitor-profiles] upsert failed", {
        visitorId,
        err: err instanceof Error ? err.message : String(err),
      });
    }
  }

  console.info("[visitor-profiles] rebuild complete", { brandId, ...result });
  return result;
}

// ─── Household Cluster Builder ──────────────────────────────────

export interface ClusterBuildResult {
  clustersCreated: number;
  clustersUpdated: number;
  errors: number;
}

export async function rebuildHouseholdClusters(
  brandId: string,
  opts: { since?: Date } = {},
): Promise<ClusterBuildResult> {
  const since = opts.since ?? new Date(Date.now() - 90 * 86_400_000);
  const profiles = await prisma.visitorProfile.findMany({
    where: { brandId, lastSeenAt: { gte: since } },
    select: {
      visitorId: true, ipAddresses: true, totalSessions: true,
      totalConversions: true, devices: true,
    },
    take: 10_000,
  });

  // Group visitors by /24 subnet (first 3 octets). Each subnet × city
  // combination becomes one cluster.
  type ClusterKey = string; // "subnet|city"
  interface ClusterData {
    ipSubnet: string;
    city: string | null;
    region: string | null;
    visitorIds: Set<string>;
    deviceSet: Set<string>;
    sessionCount: number;
    conversionCount: number;
    firstSeen: Date;
    lastSeen: Date;
  }

  const clusters = new Map<ClusterKey, ClusterData>();

  // We need city/region from TrackedSession. Load a compact mapping
  // of IP → city/region from the same window.
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  const ipGeo = new Map<string, { city: string | null; region: string | null }>();
  if (site) {
    const geoRows = await prisma.trackedSession.findMany({
      where: { trackedSiteId: site.id, startedAt: { gte: since }, ipAddress: { not: null } },
      select: { ipAddress: true, city: true, region: true },
      distinct: ["ipAddress"],
      take: 10_000,
    });
    for (const r of geoRows) {
      if (r.ipAddress) ipGeo.set(r.ipAddress, { city: r.city, region: r.region });
    }
  }

  for (const profile of profiles) {
    const ips = Array.isArray(profile.ipAddresses) ? profile.ipAddresses as string[] : [];
    const devices = Array.isArray(profile.devices) ? profile.devices as Array<{ device?: string }> : [];

    for (const ip of ips) {
      const parts = ip.split(".");
      if (parts.length < 3) continue;
      const subnet = parts.slice(0, 3).join(".");
      const geo = ipGeo.get(ip);
      const city = geo?.city ?? null;
      const key = `${subnet}|${city ?? "?"}`;

      let cluster = clusters.get(key);
      if (!cluster) {
        cluster = {
          ipSubnet: subnet,
          city,
          region: geo?.region ?? null,
          visitorIds: new Set(),
          deviceSet: new Set(),
          sessionCount: 0,
          conversionCount: 0,
          firstSeen: new Date(),
          lastSeen: new Date(0),
        };
        clusters.set(key, cluster);
      }
      cluster.visitorIds.add(profile.visitorId);
      cluster.sessionCount += profile.totalSessions;
      cluster.conversionCount += profile.totalConversions;
      for (const d of devices) {
        if (d.device) cluster.deviceSet.add(d.device);
      }
    }
  }

  const result: ClusterBuildResult = { clustersCreated: 0, clustersUpdated: 0, errors: 0 };

  for (const cluster of clusters.values()) {
    if (cluster.visitorIds.size < 1) continue;
    const confidence = cluster.visitorIds.size >= 3 && cluster.deviceSet.size >= 2
      ? "high"
      : cluster.visitorIds.size >= 2
        ? "medium"
        : "low";
    try {
      await prisma.householdCluster.upsert({
        where: {
          brandId_ipSubnet_city: {
            brandId,
            ipSubnet: cluster.ipSubnet,
            city: cluster.city ?? "",
          },
        },
        create: {
          brandId,
          ipSubnet: cluster.ipSubnet,
          city: cluster.city,
          region: cluster.region,
          visitorIds: [...cluster.visitorIds] as object,
          deviceCount: cluster.deviceSet.size,
          sessionCount: cluster.sessionCount,
          conversionCount: cluster.conversionCount,
          confidence,
        },
        update: {
          visitorIds: [...cluster.visitorIds] as object,
          deviceCount: cluster.deviceSet.size,
          sessionCount: cluster.sessionCount,
          conversionCount: cluster.conversionCount,
          confidence,
          region: cluster.region,
          lastSeenAt: new Date(),
        },
      });
      result.clustersCreated++;
    } catch (err) {
      result.errors++;
      console.warn("[household-clusters] upsert failed", {
        subnet: cluster.ipSubnet,
        err: err instanceof Error ? err.message : String(err),
      });
    }
  }

  console.info("[household-clusters] rebuild complete", { brandId, ...result });
  return result;
}

// ─── Channel Group Backfill ─────────────────────────────────────
// Stamps channelGroup on TrackedSession rows that were created before
// Phase 1 shipped (i.e. rows where channelGroup is null). Runs once
// per brand; subsequent sessions get channelGroup at write time.

export async function backfillChannelGroups(
  brandId: string,
  opts: { batchSize?: number; force?: boolean } = {},
): Promise<{ updated: number; errors: number }> {
  const batchSize = opts.batchSize ?? 5000;
  const force = opts.force ?? false;
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return { updated: 0, errors: 0 };

  // force=true re-classifies ALL sessions (used after fixing the
  // classifier). Default only processes null channelGroup rows.
  const rows = await prisma.trackedSession.findMany({
    where: force
      ? { trackedSiteId: site.id }
      : { trackedSiteId: site.id, channelGroup: null },
    select: {
      id: true, utmSource: true, utmMedium: true, referrer: true,
      gclid: true, fbclid: true, msclkid: true, ttclid: true,
      li_fat_id: true, dclid: true, wbraid: true, gbraid: true,
    },
    take: batchSize,
  });

  let updated = 0;
  let errors = 0;
  for (const row of rows) {
    try {
      const channel = classifyChannel(row);
      await prisma.trackedSession.update({
        where: { id: row.id },
        data: { channelGroup: channel },
      });
      updated++;
    } catch {
      errors++;
    }
  }

  console.info("[channel-groups] backfill complete", { brandId, updated, errors, remaining: rows.length === batchSize ? "more" : "done" });
  return { updated, errors };
}
