/**
 * CANONICAL PAID MEDIA METRICS — SINGLE SOURCE OF TRUTH
 *
 * EVERY paid media surface (overview tab, attribution tab, visitor
 * journeys, campaigns, clusters, reconciliation, admin cross-brand)
 * MUST use these functions. No ad-hoc queries.
 *
 * Three channel categories:
 *   PAID    — Paid Search, Paid Social, Display, OTT/CTV
 *   ORGANIC — Organic Search, Organic Social, Direct, Referral, Email
 *   ASSISTED — multi-touch paths where both paid + organic contributed
 *
 * Session channel is resolved from TrackedSession.channelGroup when
 * populated; falls back to classifyChannel() inline when null (pre-
 * backfill sessions). If > 50% of sessions lack channelGroup, a
 * fire-and-forget backfill is kicked off so subsequent requests are
 * faster.
 */

import { prisma } from "@/lib/prisma";
import { classifyChannel, classifyChannelDetailed } from "./attribution";
import type { ChannelDetail } from "./attribution";
import { backfillChannelGroups } from "./visitor-profiles";
import { countedConversionWhere } from "@/lib/conversions/counted-where";

// Re-export for consumers that want sub-channel detail.
export { classifyChannelDetailed, type ChannelDetail } from "./attribution";

// ─── Channel category sets ─────────────────────────────────────

const PAID_CHANNELS = new Set([
  "Paid Search", "Paid Social", "Display", "OTT/CTV",
]);
const ORGANIC_CHANNELS = new Set([
  "Organic Search", "Organic Social", "Direct", "Referral", "Email", "Affiliate",
]);

function isPaid(ch: string): boolean { return PAID_CHANNELS.has(ch); }
function isOrganic(ch: string): boolean { return ORGANIC_CHANNELS.has(ch); }

// ─── Internal: resolve sessions with inline channel fallback ────

interface SessionRow {
  sessionId: string;
  channel: string;
}

async function resolveSessionChannels(
  brandId: string,
  since: Date,
): Promise<{ rows: SessionRow[]; backfillTriggered: boolean }> {
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return { rows: [], backfillTriggered: false };

  const sessions = await prisma.trackedSession.findMany({
    where: { trackedSiteId: site.id, startedAt: { gte: since } },
    select: {
      sessionId: true, channelGroup: 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: 50_000,
  }).catch(() => []);

  let nullCount = 0;
  const rows: SessionRow[] = sessions.map((s) => {
    if (!s.channelGroup) nullCount++;
    return {
      sessionId: s.sessionId,
      channel: s.channelGroup || classifyChannel(s),
    };
  });

  let backfillTriggered = false;
  if (sessions.length > 0 && nullCount / sessions.length > 0.5) {
    void backfillChannelGroups(brandId).catch(() => {});
    backfillTriggered = true;
  }

  return { rows, backfillTriggered };
}

// ─── Paid Channel Metrics ───────────────────────────────────────

export interface PaidChannelMetrics {
  sessions: number;
  conversions: number;
  spend: number;
  roas: number;
  cpc: number;
  cpl: number;
  byChannel: Record<string, { sessions: number; conversions: number; spend: number }>;
}

export async function getPaidChannelMetrics(
  brandId: string,
  since: Date,
): Promise<PaidChannelMetrics> {
  try {
    const [{ rows }, attrPaths, campaignData] = await Promise.all([
      resolveSessionChannels(brandId, since),
      prisma.attributionPath.findMany({
        where: { brandId, createdAt: { gte: since } },
        select: { convertingChannel: true },
      }).catch(() => []),
      prisma.adCampaignData.findMany({
        where: { brandId, date: { gte: since } },
        select: { platform: true, spend: true, clicks: true },
      }).catch(() => []),
    ]);

    const P2C: Record<string, string> = {
      google_ads: "Paid Search", meta: "Paid Social", linkedin: "Paid Social",
      tiktok: "Paid Social", microsoft_ads: "Paid Search", the_trade_desk: "Display",
    };

    const byChannel: Record<string, { sessions: number; conversions: number; spend: number }> = {};
    const ensure = (ch: string) => { if (!byChannel[ch]) byChannel[ch] = { sessions: 0, conversions: 0, spend: 0 }; };

    let totalSessions = 0;
    for (const r of rows) {
      if (!isPaid(r.channel)) continue;
      ensure(r.channel);
      byChannel[r.channel].sessions++;
      totalSessions++;
    }

    let totalConversions = 0;
    for (const p of attrPaths) {
      const ch = p.convertingChannel ?? "Direct";
      if (!isPaid(ch)) continue;
      ensure(ch);
      byChannel[ch].conversions++;
      totalConversions++;
    }

    let totalSpend = 0;
    let totalClicks = 0;
    for (const c of campaignData) {
      const ch = P2C[c.platform] || "Paid Search";
      ensure(ch);
      byChannel[ch].spend += c.spend;
      totalSpend += c.spend;
      totalClicks += c.clicks;
    }

    return {
      sessions: totalSessions,
      conversions: totalConversions,
      spend: Math.round(totalSpend * 100) / 100,
      roas: totalSpend > 0 ? Math.round((totalConversions / totalSpend) * 100) / 100 : 0,
      cpc: totalClicks > 0 ? Math.round((totalSpend / totalClicks) * 100) / 100 : 0,
      cpl: totalConversions > 0 ? Math.round((totalSpend / totalConversions) * 100) / 100 : 0,
      byChannel,
    };
  } catch {
    return { sessions: 0, conversions: 0, spend: 0, roas: 0, cpc: 0, cpl: 0, byChannel: {} };
  }
}

// ─── Organic Channel Metrics ────────────────────────────────────

export interface OrganicChannelMetrics {
  sessions: number;
  conversions: number;
  byChannel: Record<string, { sessions: number; conversions: number }>;
}

export async function getOrganicChannelMetrics(
  brandId: string,
  since: Date,
): Promise<OrganicChannelMetrics> {
  try {
    const [{ rows }, attrPaths] = await Promise.all([
      resolveSessionChannels(brandId, since),
      prisma.attributionPath.findMany({
        where: { brandId, createdAt: { gte: since } },
        select: { convertingChannel: true },
      }).catch(() => []),
    ]);

    const byChannel: Record<string, { sessions: number; conversions: number }> = {};
    const ensure = (ch: string) => { if (!byChannel[ch]) byChannel[ch] = { sessions: 0, conversions: 0 }; };

    let totalSessions = 0;
    for (const r of rows) {
      if (!isOrganic(r.channel)) continue;
      ensure(r.channel);
      byChannel[r.channel].sessions++;
      totalSessions++;
    }

    let totalConversions = 0;
    for (const p of attrPaths) {
      const ch = p.convertingChannel ?? "Direct";
      if (!isOrganic(ch)) continue;
      ensure(ch);
      byChannel[ch].conversions++;
      totalConversions++;
    }

    return { sessions: totalSessions, conversions: totalConversions, byChannel };
  } catch {
    return { sessions: 0, conversions: 0, byChannel: {} };
  }
}

// ─── Assisted Conversions ───────────────────────────────────────

export interface AssistedConversions {
  total: number;
  paidAssistedOrganic: number;
  organicAssistedPaid: number;
  avgTouchpoints: number;
}

export async function getAssistedConversions(
  brandId: string,
  since: Date,
): Promise<AssistedConversions> {
  try {
    // Primary path: AttributionPath rows written by the cron builder.
    // These have touchpointCount >= 2 when the builder correctly
    // assembled multi-session journeys.
    const paths = await prisma.attributionPath.findMany({
      where: { brandId, createdAt: { gte: since }, touchpointCount: { gte: 2 } },
      select: { touchpoints: true, convertingChannel: true, touchpointCount: true },
      take: 10_000,
    }).catch(() => []);

    let total = 0;
    let paidAssistedOrganic = 0;
    let organicAssistedPaid = 0;
    let touchpointSum = 0;

    for (const p of paths) {
      const tps = Array.isArray(p.touchpoints) ? p.touchpoints as Array<{ channel?: string }> : [];
      const channels = tps.map((t) => t.channel ?? "Other");
      const hasPaid = channels.some(isPaid);
      const hasOrganic = channels.some(isOrganic);
      if (!hasPaid || !hasOrganic) continue;

      total++;
      touchpointSum += p.touchpointCount;
      const converting = p.convertingChannel ?? "Direct";
      if (isOrganic(converting)) paidAssistedOrganic++;
      else if (isPaid(converting)) organicAssistedPaid++;
    }

    if (total > 0) {
      return {
        total,
        paidAssistedOrganic,
        organicAssistedPaid,
        avgTouchpoints: Math.round((touchpointSum / total) * 10) / 10,
      };
    }

    // Fallback: derive assists directly from SiteConversion + TrackedSession.
    // Used when the cron builder hasn't run or produced empty touchpoint arrays.
    // Looks for visitors with 2+ conversion events that span both paid and
    // organic channels (SiteConversion.sessionId → TrackedSession.channelGroup).
    return await computeAssistedFromConversions(brandId, since);
  } catch {
    return { total: 0, paidAssistedOrganic: 0, organicAssistedPaid: 0, avgTouchpoints: 0 };
  }
}

async function computeAssistedFromConversions(
  brandId: string,
  since: Date,
): Promise<AssistedConversions> {
  try {
    const conversions = await prisma.siteConversion.findMany({
      where: countedConversionWhere(brandId, { since }),
      select: { visitorId: true, sessionId: true, timestamp: true },
      orderBy: { timestamp: "asc" },
      take: 50_000,
    }).catch(() => []);

    if (conversions.length === 0) return { total: 0, paidAssistedOrganic: 0, organicAssistedPaid: 0, avgTouchpoints: 0 };

    const uniqueSessionIds = [...new Set(conversions.map((c) => c.sessionId).filter(Boolean))];
    const sessionRows = await prisma.trackedSession.findMany({
      where: { sessionId: { in: uniqueSessionIds } },
      select: {
        sessionId: true, channelGroup: 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,
      },
    }).catch(() => []);

    const channelBySession = new Map<string, string>();
    for (const s of sessionRows) {
      channelBySession.set(s.sessionId, s.channelGroup || classifyChannel(s));
    }

    // Group conversions by visitorId, ordered chronologically (already sorted above)
    const visitorConvs = new Map<string, Array<{ channel: string }>>();
    for (const conv of conversions) {
      const channel = channelBySession.get(conv.sessionId) ?? "Direct";
      const arr = visitorConvs.get(conv.visitorId) ?? [];
      arr.push({ channel });
      visitorConvs.set(conv.visitorId, arr);
    }

    let total = 0;
    let paidAssistedOrganic = 0;
    let organicAssistedPaid = 0;

    for (const [, convs] of visitorConvs) {
      if (convs.length < 2) continue;
      const hasPaid = convs.some((c) => isPaid(c.channel));
      const hasOrganic = convs.some((c) => isOrganic(c.channel));
      if (!hasPaid || !hasOrganic) continue;

      total++;
      const lastChannel = convs[convs.length - 1].channel;
      if (isOrganic(lastChannel)) paidAssistedOrganic++;
      else if (isPaid(lastChannel)) organicAssistedPaid++;
    }

    return { total, paidAssistedOrganic, organicAssistedPaid, avgTouchpoints: total > 0 ? 2 : 0 };
  } catch {
    return { total: 0, paidAssistedOrganic: 0, organicAssistedPaid: 0, avgTouchpoints: 0 };
  }
}

// ─── Channel Summary ────────────────────────────────────────────

export interface ChannelSummary {
  paid: PaidChannelMetrics;
  organic: OrganicChannelMetrics;
  assisted: AssistedConversions;
  totalConversions: number;
  paidShareOfConversions: number;
  organicShareOfConversions: number;
  assistedShareOfConversions: number;
  channelBackfillTriggered: boolean;
}

export async function getChannelSummary(
  brandId: string,
  since: Date,
): Promise<ChannelSummary> {
  const [paid, organic, assisted, totalConversions] = await Promise.all([
    getPaidChannelMetrics(brandId, since),
    getOrganicChannelMetrics(brandId, since),
    getAssistedConversions(brandId, since),
    prisma.siteConversion.count({
      where: countedConversionWhere(brandId, { since }),
    }).catch(() => 0),
  ]);

  const total = totalConversions || 1;
  const { backfillTriggered } = await resolveSessionChannels(brandId, since);

  return {
    paid,
    organic,
    assisted,
    totalConversions,
    paidShareOfConversions: Math.round((paid.conversions / total) * 1000) / 10,
    organicShareOfConversions: Math.round((organic.conversions / total) * 1000) / 10,
    assistedShareOfConversions: Math.round((assisted.total / total) * 1000) / 10,
    channelBackfillTriggered: backfillTriggered,
  };
}

// ─── Visitor Profile Summary ────────────────────────────────────

export interface VisitorProfileSummary {
  total: number;
  returning: number;
  converted: number;
  avgSessionsPerVisitor: number;
}

export async function getVisitorProfileSummary(
  brandId: string,
  since: Date,
): Promise<VisitorProfileSummary> {
  try {
    const [total, returning, converted, sessAgg] = await Promise.all([
      prisma.visitorProfile.count({
        where: { brandId, lastSeenAt: { gte: since } },
      }),
      prisma.visitorProfile.count({
        where: { brandId, lastSeenAt: { gte: since }, totalSessions: { gte: 2 } },
      }),
      prisma.visitorProfile.count({
        where: { brandId, lastSeenAt: { gte: since }, totalConversions: { gte: 1 } },
      }),
      prisma.visitorProfile.aggregate({
        where: { brandId, lastSeenAt: { gte: since } },
        _avg: { totalSessions: true },
      }),
    ]);
    return {
      total,
      returning,
      converted,
      avgSessionsPerVisitor: Math.round((sessAgg._avg.totalSessions ?? 0) * 10) / 10,
    };
  } catch {
    return { total: 0, returning: 0, converted: 0, avgSessionsPerVisitor: 0 };
  }
}

// ─── Household Cluster Summary ──────────────────────────────────

export interface HouseholdClusterSummary {
  total: number;
  multiDevice: number;
  converting: number;
  avgVisitorsPerCluster: number;
}

export async function getHouseholdClusterSummary(
  brandId: string,
  since: Date,
): Promise<HouseholdClusterSummary> {
  try {
    const clusters = await prisma.householdCluster.findMany({
      where: { brandId, lastSeenAt: { gte: since } },
      select: { deviceCount: true, conversionCount: true, visitorIds: true },
      take: 10_000,
    });
    const total = clusters.length;
    const multiDevice = clusters.filter((c) => c.deviceCount > 1).length;
    const converting = clusters.filter((c) => c.conversionCount > 0).length;
    const totalVisitors = clusters.reduce((s, c) => s + (Array.isArray(c.visitorIds) ? (c.visitorIds as string[]).length : 0), 0);
    return {
      total,
      multiDevice,
      converting,
      avgVisitorsPerCluster: total > 0 ? Math.round((totalVisitors / total) * 10) / 10 : 0,
    };
  } catch {
    return { total: 0, multiDevice: 0, converting: 0, avgVisitorsPerCluster: 0 };
  }
}

// ─── Ad Platform Status ─────────────────────────────────────────

const ALL_PLATFORMS = ["google_ads", "meta", "linkedin", "tiktok", "microsoft_ads", "the_trade_desk"];

export interface AdPlatformStatus {
  connected: string[];
  pending: string[];
  available: string[];
}

export async function getAdPlatformStatus(
  brandId: string,
): Promise<AdPlatformStatus> {
  try {
    const integrations = await prisma.adPlatformIntegration.findMany({
      where: { brandId },
      select: { platform: true, status: true },
    });
    const connected = integrations.filter((i) => i.status === "connected").map((i) => i.platform);
    const pending = integrations.filter((i) => i.status === "pending" || i.status === "error").map((i) => i.platform);
    const usedSet = new Set(integrations.map((i) => i.platform));
    const available = ALL_PLATFORMS.filter((p) => !usedSet.has(p));
    return { connected, pending, available };
  } catch {
    return { connected: [], pending: [], available: ALL_PLATFORMS };
  }
}
