import { prisma } from "@/lib/prisma";
import { getDateRange, getComparisonRange, DateRange, CompareMode } from "./date-utils";
import { ACTIVE_CONVERSION_FILTER } from "@/lib/conversions/active-filter";

export type ConversionType = "phone" | "form" | "order" | "booking";
export type Channel = "organic" | "ai_search" | "social" | "gbp" | "direct";

/**
 * Fetches conversion intelligence data for a brand.
 *
 * Data sources, in order of preference:
 *   1. ConversionEvent table — per-event granularity (type, channel, landing
 *      page, keyword). Populated by manual seed or future event-level syncs.
 *   2. MetricSnapshot table — daily aggregated totals written by the GA4
 *      sync (ga4-sync.ts writes sessions, conversions, revenue per day).
 *      This is the same data the Dashboard reads, so the two pages stay
 *      in sync when the ConversionEvent table is empty.
 *
 * When ConversionEvent has rows for the period, we use it for totals,
 * daily trend, and breakdowns. When it's empty but MetricSnapshot has GA4
 * rows, we synthesise totals + daily trend from the snapshots and mark
 * `dataOrigin: "metric_snapshot"` so the UI can explain the source.
 */
export async function getConversionData(
  brandId: string,
  range: DateRange = "30d",
  compare: CompareMode = "previous",
  filters?: { type?: ConversionType; channel?: Channel; sourceFilter?: string[] }
) {
  const { start, end } = getDateRange(range);
  const comp = getComparisonRange(start, end, compare);

  // Build where clause — only include events from real sources when filter is provided
  const where: Record<string, unknown> = { brandId, date: { gte: start, lte: end } };
  if (filters?.type) where.type = filters.type;
  if (filters?.channel) where.channel = filters.channel;
  if (filters?.sourceFilter) where.source = { in: filters.sourceFilter };

  const compWhere = { ...where, date: { gte: comp.start, lte: comp.end } };

  // Fetch conversion events
  const currentEvents = await prisma.conversionEvent.findMany({
    where,
    orderBy: { date: "asc" },
  });

  const previousEvents = await prisma.conversionEvent.findMany({
    where: compWhere,
    orderBy: { date: "asc" },
  });

  // ── MetricSnapshot fallback (GA4 aggregated daily totals) ──
  // Always fetch these — we use them for the conversion-rate card and,
  // if ConversionEvent is empty, as the source of truth for totals.
  const snapshotSourceFilter = filters?.sourceFilter ?? ["ga4", "gsc"];
  const [currentSnapshots, previousSnapshots] = await Promise.all([
    prisma.metricSnapshot.findMany({
      where: { brandId, date: { gte: start, lte: end }, source: { in: snapshotSourceFilter } },
      orderBy: { date: "asc" },
    }),
    prisma.metricSnapshot.findMany({
      where: { brandId, date: { gte: comp.start, lte: comp.end }, source: { in: snapshotSourceFilter } },
      orderBy: { date: "asc" },
    }),
  ]);

  const snapshotTotals = (rows: typeof currentSnapshots) => {
    let conversions = 0, revenue = 0, sessions = 0;
    for (const r of rows) {
      conversions += r.conversions;
      revenue += r.revenue;
      sessions += r.sessions;
    }
    return { conversions, revenue, sessions };
  };
  const snapshotCurrent = snapshotTotals(currentSnapshots);
  const snapshotPrevious = snapshotTotals(previousSnapshots);

  // Fetch channel metrics — filter by source when sourceFilter is provided
  const channelWhere: Record<string, unknown> = { brandId, date: { gte: start, lte: end } };
  const prevChannelWhere: Record<string, unknown> = { brandId, date: { gte: comp.start, lte: comp.end } };
  if (filters?.sourceFilter) {
    channelWhere.source = { in: filters.sourceFilter };
    prevChannelWhere.source = { in: filters.sourceFilter };
  }

  const channelMetrics = await prisma.channelMetric.findMany({ where: channelWhere, orderBy: { date: "asc" } });
  const prevChannelMetrics = await prisma.channelMetric.findMany({ where: prevChannelWhere, orderBy: { date: "asc" } });

  // Aggregate by type
  const byType = aggregateByField(currentEvents, "type");
  const prevByType = aggregateByField(previousEvents, "type");

  // Aggregate by channel
  const byChannel = aggregateByField(currentEvents, "channel");
  const prevByChannel = aggregateByField(previousEvents, "channel");

  // Channel metrics summary
  const channelSummary = summarizeChannelMetrics(channelMetrics);
  const prevChannelSummary = summarizeChannelMetrics(prevChannelMetrics);

  // ── Totals: prefer ConversionEvent, fall back to MetricSnapshot ──
  // When ConversionEvent is empty but MetricSnapshot has GA4 rows, we use
  // the snapshot sum so the page shows the same aggregate number the
  // Dashboard displays (e.g. 1,435 conversions for a brand with GA4 sync).
  const hasEventData = currentEvents.length > 0;
  const dataOrigin: "conversion_event" | "metric_snapshot" | "none" =
    hasEventData ? "conversion_event"
    : snapshotCurrent.conversions > 0 ? "metric_snapshot"
    : "none";

  const totalConversions = hasEventData ? currentEvents.length : snapshotCurrent.conversions;
  const totalRevenue = hasEventData
    ? currentEvents.reduce((s, e) => s + e.value, 0)
    : snapshotCurrent.revenue;
  const prevTotalConversions = previousEvents.length > 0
    ? previousEvents.length
    : snapshotPrevious.conversions;
  const prevTotalRevenue = previousEvents.length > 0
    ? previousEvents.reduce((s, e) => s + e.value, 0)
    : snapshotPrevious.revenue;

  // ── Daily trend: prefer ConversionEvent, fall back to snapshot rows ──
  const dailyTrend = hasEventData
    ? buildDailyTrend(currentEvents, start, end)
    : buildDailyTrendFromSnapshots(currentSnapshots, start, end);

  // ── Conversion rate (always from MetricSnapshot sessions) ──
  // GA4 counts conversion EVENTS, not converting sessions — a single
  // session can fire multiple conversion events, so the raw ratio can
  // exceed 100%. We cap the displayed rate at 100% and pass the raw
  // value as `rawConversionRate` so the UI can show "X conversion
  // events across Y sessions" when it would otherwise display >100%.
  const sessions = snapshotCurrent.sessions;
  const prevSessions = snapshotPrevious.sessions;
  const rawConversionRate = sessions > 0 ? (totalConversions / sessions) * 100 : 0;
  const conversionRate = Math.min(rawConversionRate, 100);
  const prevRawRate = prevSessions > 0 ? (prevTotalConversions / prevSessions) * 100 : 0;
  const prevConversionRate = Math.min(prevRawRate, 100);

  // Top converting pages
  const topPages = getTopField(currentEvents, "landingPage", 10);

  // Top keywords
  const topKeywords = getTopField(currentEvents, "keyword", 10);

  // ── Site Tag daily conversions (from SiteConversion table) ──
  // This is always available when the Site Tag is installed, even
  // when GA4 key events = 0. Provides the "real" conversion trend
  // that the chart shows as the primary solid line.
  type SiteTagDayRow = { day: Date; count: bigint };
  const siteTagDaily = await prisma.$queryRaw<SiteTagDayRow[]>`
    SELECT DATE_TRUNC('day', "timestamp") as day, COUNT(*)::bigint as count
    FROM "SiteConversion"
    WHERE "brandId" = ${brandId}
      AND "timestamp" >= ${start}
      AND "timestamp" <= ${end}
      AND "isDuplicateOf" IS NULL
      AND "invalidatedReason" IS NULL
    GROUP BY day
    ORDER BY day ASC
  `.catch(() => [] as SiteTagDayRow[]);

  const siteTagByDay = new Map<string, number>();
  for (const r of siteTagDaily) {
    siteTagByDay.set(new Date(r.day).toISOString().split("T")[0], Number(r.count));
  }

  // Merge into dailyTrend: add siteTagCount alongside the GA4 count
  const dailyTrendWithSiteTag = dailyTrend.map((d) => ({
    ...d,
    siteTagCount: siteTagByDay.get(d.date) ?? 0,
  }));

  // GA4 freshness: find the most recent MetricSnapshot with source=ga4
  const latestGa4Snapshot = await prisma.metricSnapshot.findFirst({
    where: { brandId, source: "ga4" },
    orderBy: { date: "desc" },
    select: { date: true, updatedAt: true },
  }).catch(() => null);

  const ga4LastSyncDate = latestGa4Snapshot?.date?.toISOString().split("T")[0] ?? null;
  const ga4LastUpdatedAt = latestGa4Snapshot?.updatedAt?.toISOString() ?? null;
  const ga4StaleHours = latestGa4Snapshot?.updatedAt
    ? Math.round((Date.now() - latestGa4Snapshot.updatedAt.getTime()) / 3_600_000)
    : null;

  // Site Tag freshness: most recent SiteConversion
  const latestSiteTagRow = await prisma.siteConversion.findFirst({
    where: { brandId, ...ACTIVE_CONVERSION_FILTER },
    orderBy: { timestamp: "desc" },
    select: { timestamp: true },
  }).catch(() => null);

  const siteTagLastEventAt = latestSiteTagRow?.timestamp?.toISOString() ?? null;

  return {
    period: { start, end, range, compare },
    dataOrigin,
    totals: {
      conversions: totalConversions,
      revenue: Math.round(totalRevenue),
      avgValue: totalConversions > 0 ? Math.round(totalRevenue / totalConversions) : 0,
      sessions,
      conversionRate: Math.round(conversionRate * 100) / 100,
      siteTagConversions: siteTagDaily.reduce((s, r) => s + Number(r.count), 0),
    },
    changes: {
      conversions: calcChange(totalConversions, prevTotalConversions),
      revenue: calcChange(totalRevenue, prevTotalRevenue),
      conversionRate: calcChange(conversionRate, prevConversionRate),
      sessions: calcChange(sessions, prevSessions),
    },
    byType: Object.entries(byType).map(([type, data]) => ({
      type,
      count: data.count,
      revenue: Math.round(data.revenue),
      change: calcChange(data.count, prevByType[type]?.count ?? 0),
    })),
    byChannel: Object.entries(byChannel).map(([channel, data]) => ({
      channel,
      count: data.count,
      revenue: Math.round(data.revenue),
      change: calcChange(data.count, prevByChannel[channel]?.count ?? 0),
    })),
    channelBreakdown: Object.entries(channelSummary).map(([channel, data]) => ({
      channel,
      ...data,
      changes: {
        sessions: calcChange(data.sessions, prevChannelSummary[channel]?.sessions ?? 0),
        conversions: calcChange(data.conversions, prevChannelSummary[channel]?.conversions ?? 0),
      },
    })),
    dailyTrend: dailyTrendWithSiteTag,
    topPages,
    topKeywords,
    freshness: {
      ga4: {
        lastSyncDate: ga4LastSyncDate,
        lastUpdatedAt: ga4LastUpdatedAt,
        staleHours: ga4StaleHours,
        isStale: ga4StaleHours !== null && ga4StaleHours > 24,
      },
      siteTag: {
        lastEventAt: siteTagLastEventAt,
        isStale: latestSiteTagRow
          ? (Date.now() - latestSiteTagRow.timestamp.getTime()) > 24 * 3_600_000
          : true,
      },
    },
  };
}

// ── Helpers ──

function aggregateByField(
  events: Array<{ type: string; channel: string; value: number }>,
  field: "type" | "channel"
) {
  const result: Record<string, { count: number; revenue: number }> = {};
  for (const e of events) {
    const key = e[field];
    if (!result[key]) result[key] = { count: 0, revenue: 0 };
    result[key].count++;
    result[key].revenue += e.value;
  }
  return result;
}

function summarizeChannelMetrics(
  metrics: Array<{ channel: string; sessions: number; users: number; conversions: number; revenue: number; bounceRate: number }>
) {
  const result: Record<string, { sessions: number; users: number; conversions: number; revenue: number; bounceRate: number }> = {};
  for (const m of metrics) {
    if (!result[m.channel]) result[m.channel] = { sessions: 0, users: 0, conversions: 0, revenue: 0, bounceRate: 0 };
    result[m.channel].sessions += m.sessions;
    result[m.channel].users += m.users;
    result[m.channel].conversions += m.conversions;
    result[m.channel].revenue += m.revenue;
  }
  // Average bounce rate
  for (const ch of Object.keys(result)) {
    const count = metrics.filter((m) => m.channel === ch).length;
    result[ch].bounceRate = count > 0
      ? Math.round((metrics.filter((m) => m.channel === ch).reduce((s, m) => s + m.bounceRate, 0) / count) * 100) / 100
      : 0;
  }
  return result;
}

/**
 * Build the daily trend from MetricSnapshot rows. Each snapshot already
 * represents a day's aggregated conversions + revenue, so we just emit
 * one trend point per day (filling zeros for gaps).
 */
function buildDailyTrendFromSnapshots(
  snapshots: Array<{ date: Date; conversions: number; revenue: number }>,
  start: Date,
  end: Date,
) {
  const byDay = new Map<string, { conversions: number; revenue: number }>();
  for (const s of snapshots) {
    const key = s.date.toISOString().split("T")[0];
    const existing = byDay.get(key) ?? { conversions: 0, revenue: 0 };
    existing.conversions += s.conversions;
    existing.revenue += s.revenue;
    byDay.set(key, existing);
  }
  const trend: Array<{ date: string; count: number | null; revenue: number }> = [];
  const cursor = new Date(start);
  while (cursor <= end) {
    const dateStr = cursor.toISOString().split("T")[0];
    const row = byDay.get(dateStr);
    // When no MetricSnapshot exists for a date, emit null so the
    // chart renders a gap (or dashed segment) instead of "0
    // conversions". This prevents the false cliff-drop on recent
    // dates where GA4 hasn't processed data yet (48-72hr delay).
    trend.push({
      date: dateStr,
      count: row ? row.conversions : null,
      revenue: row ? Math.round(row.revenue) : 0,
    });
    cursor.setDate(cursor.getDate() + 1);
  }
  return trend;
}

function buildDailyTrend(
  events: Array<{ date: Date; value: number }>,
  start: Date,
  end: Date
) {
  const trend: Array<{ date: string; count: number; revenue: number }> = [];
  const cursor = new Date(start);
  while (cursor <= end) {
    const dateStr = cursor.toISOString().split("T")[0];
    const dayEvents = events.filter((e) => e.date.toISOString().split("T")[0] === dateStr);
    trend.push({
      date: dateStr,
      count: dayEvents.length,
      revenue: Math.round(dayEvents.reduce((s, e) => s + e.value, 0)),
    });
    cursor.setDate(cursor.getDate() + 1);
  }
  return trend;
}

function getTopField(
  events: Array<Record<string, unknown>>,
  field: string,
  limit: number
) {
  const counts: Record<string, { count: number; revenue: number }> = {};
  for (const e of events) {
    const val = e[field] as string;
    if (!val) continue;
    if (!counts[val]) counts[val] = { count: 0, revenue: 0 };
    counts[val].count++;
    counts[val].revenue += (e.value as number) || 0;
  }
  return Object.entries(counts)
    .sort((a, b) => b[1].count - a[1].count)
    .slice(0, limit)
    .map(([name, data]) => ({ name, ...data }));
}

function calcChange(current: number, previous: number) {
  // No baseline → suppress the change badge rather than fabricating "↑ 100%".
  if (previous === 0) return null;
  const pct = Math.round(((current - previous) / previous) * 100);
  return { pct, direction: pct > 0 ? "up" as const : pct < 0 ? "down" as const : "flat" as const };
}
