/**
 * GET /api/admin/analytics/data-moat
 *
 * Cumulative first-party data advantage. Surfaces the data assets
 * meSEO has collected that no competitor can replicate (the moat
 * argument for investors / pricing decisions). Sources every
 * metric from existing tables — TrackedEvent / TrackedSession /
 * SiteConversion / TechnicalAudit / GscQuery / AiInteraction /
 * SiteEvent.
 *
 * Returns:
 *   volumes:    { trackedEvents, trackedSessions, siteConversions,
 *                  pagesCrawled, keywordsTracked, aiInteractions,
 *                  growthPerDay: { ... } }
 *   growth:     { eventsByDay: [{date, count}], cumulative: [{date, total}],
 *                  doubleDays }
 *   perBrand:   [{ brandId, name, events, sessions, conversions,
 *                  pagesCrawled, keywords, daysOfData, richnessScore }]
 *   moat:       { firstPartyEventsGa4Cant, bookingFunnelInteractions,
 *                  pluginsDetected, brandsWithPlugins, realUserCwv }
 *
 * Superadmin-only. maxDuration 30s. Per-section try/catch so one
 * failing query zeroes only its section.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSuperadmin, withAdminTiming } from "@/lib/admin-auth";

export const dynamic = "force-dynamic";
export const maxDuration = 30;

const DAY_MS = 86_400_000;

export async function GET() {
  const denied = await requireSuperadmin();
  if (denied) return denied;

  try {
    return NextResponse.json(await withAdminTiming("analytics.data-moat", async () => {
      const now = Date.now();
      const ninetyDaysAgo = new Date(now - 90 * DAY_MS);
      const sevenDaysAgo = new Date(now - 7 * DAY_MS);

      // ── Volumes (each independent, allSettled) ─────────────
      const counts = await Promise.allSettled([
        prisma.trackedEvent.count(),
        prisma.trackedSession.count(),
        // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
        prisma.siteConversion.count(),
        prisma.pageRecord.count(),
        // Distinct keywords across all GscQuery rows. Use groupBy
        // by query so a single keyword tracked across 3 brands
        // counts once. Cap at 100K for memory safety.
        prisma.gscQuery.groupBy({
          by: ["query"],
          orderBy: { query: "asc" },
          take: 100_000,
        }).then((r) => r.length),
        prisma.aiInteraction.count(),
        // Last-7-days deltas for growth-per-day computation.
        prisma.trackedEvent.count({ where: { timestamp: { gte: sevenDaysAgo } } }),
        prisma.trackedSession.count({ where: { startedAt: { gte: sevenDaysAgo } } }),
        // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
        prisma.siteConversion.count({ where: { timestamp: { gte: sevenDaysAgo } } }),
        prisma.aiInteraction.count({ where: { timestamp: { gte: sevenDaysAgo } } }),
      ]);
      const settled = (i: number) => counts[i].status === "fulfilled" ? (counts[i] as PromiseFulfilledResult<number>).value : 0;
      const volumes = {
        trackedEvents:    settled(0),
        trackedSessions:  settled(1),
        siteConversions:  settled(2),
        pagesCrawled:     settled(3),
        keywordsTracked:  settled(4),
        aiInteractions:   settled(5),
        growthPerDay: {
          trackedEvents:   Math.round(settled(6) / 7),
          trackedSessions: Math.round(settled(7) / 7),
          siteConversions: Math.round(settled(8) / 7),
          aiInteractions:  Math.round(settled(9) / 7),
        },
      };

      // ── Growth — events per day (last 90d) + cumulative ────
      let eventsByDay: Array<{ date: string; count: number }> = [];
      let cumulative: Array<{ date: string; total: number }> = [];
      let doubleDays: number | null = null;
      try {
        // Pull timestamps over the window and bucket client-side.
        // Bounded at 200K — large enough for plausible monthly
        // platform volume.
        const rows = await prisma.trackedEvent.findMany({
          where: { timestamp: { gte: ninetyDaysAgo } },
          select: { timestamp: true },
          take: 200_000,
        });
        const map = new Map<string, number>();
        for (const r of rows) {
          const k = r.timestamp.toISOString().slice(0, 10);
          map.set(k, (map.get(k) ?? 0) + 1);
        }
        // Backfill missing days so the chart is gap-free.
        const dayCount = 90;
        const ordered: Array<{ date: string; count: number }> = [];
        for (let i = dayCount - 1; i >= 0; i--) {
          const d = new Date(now - i * DAY_MS).toISOString().slice(0, 10);
          ordered.push({ date: d, count: map.get(d) ?? 0 });
        }
        eventsByDay = ordered;
        // Cumulative: running total prepended with the all-time
        // count BEFORE the 90-day window so the chart shows the
        // full curve, not just the last-90d slice.
        const eventsBefore = (volumes.trackedEvents - rows.length) || 0;
        let running = Math.max(0, eventsBefore);
        cumulative = ordered.map((p) => {
          running += p.count;
          return { date: p.date, total: running };
        });
        // "Doubling" estimate — at the current 7-day daily-rate
        // average, how many days until the lifetime count doubles?
        // Useful for the "data doubles every X days" headline.
        const dailyRate = volumes.growthPerDay.trackedEvents;
        if (dailyRate > 0 && volumes.trackedEvents > 0) {
          doubleDays = Math.ceil(volumes.trackedEvents / dailyRate);
        }
      } catch (err) {
        console.warn("[data-moat] growth queries failed", err);
      }

      // ── Per-brand depth ────────────────────────────────────
      // For each brand: events + sessions + conversions + pages +
      // keywords + days of data. richnessScore = log-scaled
      // composite to surface the data-richest brands first.
      let perBrand: Array<{
        brandId: string; name: string; events: number; sessions: number;
        conversions: number; pagesCrawled: number; keywords: number;
        daysOfData: number; richnessScore: number;
      }> = [];
      try {
        const brands = await prisma.brand.findMany({
          select: { id: true, name: true },
          take: 200,
        });
        if (brands.length > 0) {
          const brandIds = brands.map((b) => b.id);
          const [
            eventsByBrand, sessionsByBrand, conversionsByBrand,
            pagesByBrand, keywordsByBrand, sitesByBrand,
          ] = await Promise.allSettled([
            prisma.trackedEvent.groupBy({
              by: ["trackedSiteId"],
              _count: true,
            }),
            prisma.trackedSession.groupBy({
              by: ["trackedSiteId"],
              _count: true,
            }),
            // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
            prisma.siteConversion.groupBy({
              by: ["brandId"],
              where: { brandId: { in: brandIds } },
              _count: true,
            }),
            prisma.pageRecord.groupBy({
              by: ["brandId"],
              where: { brandId: { in: brandIds } },
              _count: true,
            }),
            prisma.gscQuery.groupBy({
              by: ["brandId"],
              where: { brandId: { in: brandIds } },
              _count: { query: true },
            }),
            prisma.trackedSite.findMany({
              where: { brandId: { in: brandIds } },
              select: { id: true, brandId: true, createdAt: true, firstEventAt: true },
            }),
          ]);
          const get = <T>(r: PromiseSettledResult<T>, fb: T): T => r.status === "fulfilled" ? r.value : fb;
          const sites = get(sitesByBrand, [] as Array<{ id: string; brandId: string; createdAt: Date; firstEventAt: Date | null }>);
          const brandIdBySite = new Map(sites.map((s) => [s.id, s.brandId]));
          // "Installed" = when the Site Tag actually started phoning
          // home (firstEventAt), not when the row was inserted
          // (createdAt). The difference shows up as an overstated
          // "days of data" in the per-brand metric card.
          const installedAtByBrand = new Map(sites.map((s) => [s.brandId, s.firstEventAt ?? s.createdAt]));
          const sumByBrand = (rows: Array<{ trackedSiteId: string; _count: number }>): Map<string, number> => {
            const out = new Map<string, number>();
            for (const r of rows) {
              const bId = brandIdBySite.get(r.trackedSiteId);
              if (!bId) continue;
              out.set(bId, (out.get(bId) ?? 0) + r._count);
            }
            return out;
          };
          const eventsBy   = sumByBrand(get(eventsByBrand,   [] as Array<{ trackedSiteId: string; _count: number }>));
          const sessionsBy = sumByBrand(get(sessionsByBrand, [] as Array<{ trackedSiteId: string; _count: number }>));
          const conversionsBy = new Map(
            get(conversionsByBrand, [] as Array<{ brandId: string; _count: number }>).map((r) => [r.brandId, r._count]),
          );
          const pagesBy = new Map(
            get(pagesByBrand, [] as Array<{ brandId: string; _count: number }>).map((r) => [r.brandId, r._count]),
          );
          const keywordsBy = new Map(
            get(keywordsByBrand, [] as Array<{ brandId: string; _count: { query: number } }>).map((r) => [r.brandId, r._count.query]),
          );

          perBrand = brands.map((b) => {
            const installed = installedAtByBrand.get(b.id);
            const daysOfData = installed
              ? Math.max(0, Math.floor((now - installed.getTime()) / DAY_MS))
              : 0;
            const events = eventsBy.get(b.id) ?? 0;
            const sessions = sessionsBy.get(b.id) ?? 0;
            const conversions = conversionsBy.get(b.id) ?? 0;
            const pages = pagesBy.get(b.id) ?? 0;
            const keywords = keywordsBy.get(b.id) ?? 0;
            // richnessScore — log-scaled composite. Each component
            // saturates around its "good" anchor so a single
            // outlier doesn't dominate the rank. Scaled 0-100.
            const score = Math.min(100, Math.round(
              (Math.log10(events + 1) / Math.log10(100_000 + 1)) * 30
              + (Math.log10(sessions + 1) / Math.log10(10_000 + 1)) * 20
              + (Math.log10(conversions + 1) / Math.log10(1_000 + 1)) * 20
              + (Math.log10(pages + 1) / Math.log10(500 + 1)) * 15
              + (Math.log10(keywords + 1) / Math.log10(2_000 + 1)) * 10
              + Math.min(5, daysOfData / 30 * 5),
            ));
            return {
              brandId: b.id, name: b.name,
              events, sessions, conversions, pagesCrawled: pages, keywords,
              daysOfData, richnessScore: score,
            };
          })
          .filter((b) => b.events + b.sessions + b.conversions + b.pagesCrawled > 0)
          .sort((a, b) => b.richnessScore - a.richnessScore)
          .slice(0, 50);
        }
      } catch (err) {
        console.warn("[data-moat] perBrand failed", err);
      }

      // ── Moat metrics ───────────────────────────────────────
      // Numbers that quantify why the data is hard to replicate.
      // GA4 captures aggregate analytics but does NOT see:
      //   • our gtag-intercepted conversions (they fire AFTER
      //     gtag does, so GA4's own numbers don't include the
      //     plugin / postMessage paths we surface)
      //   • booking funnel step-by-step interactions
      //   • plugin auto-detection signals
      // Surfacing these counts makes the differentiation
      // concrete for investor / pricing conversations.
      let firstPartyEventsGa4Cant = 0;
      let bookingFunnelInteractions = 0;
      let pluginsDetected = 0;
      let brandsWithPlugins = 0;
      let realUserCwv = 0;
      try {
        const [gtagInterceptCount, bookingCount, pluginCount, pluginBrands, cwvCount] = await Promise.allSettled([
          // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
          prisma.siteConversion.count({
            where: { detectionMethod: { contains: "t.js:" } },
          }),
          prisma.trackedEvent.count({
            where: { eventType: { in: ["booking_widget_opened", "booking_step", "booking_submit", "booking_confirmed"] } },
          }),
          // Distinct plugin_key extracted from plugin_detected
          // metadata. Pull rows + dedupe in-process — groupBy on
          // a JSON column isn't supported.
          prisma.trackedEvent.findMany({
            where: { eventType: "plugin_detected" },
            select: { metadata: true },
            take: 5_000,
          }).then((rows) => {
            const set = new Set<string>();
            for (const r of rows) {
              const m = (r.metadata ?? {}) as Record<string, unknown>;
              // Handle new consolidated payload { plugins: [...] } and legacy flat shape.
              if (Array.isArray(m.plugins)) {
                for (const p of m.plugins as unknown[]) {
                  if (typeof p !== "object" || p === null) continue;
                  const pm = p as Record<string, unknown>;
                  const k = typeof pm.plugin_key === "string" ? pm.plugin_key : (typeof pm.plugin === "string" ? pm.plugin : null);
                  if (k) set.add(k.toLowerCase());
                }
              } else {
                const k = typeof m.plugin_key === "string" ? m.plugin_key : (typeof m.plugin === "string" ? m.plugin : null);
                if (k) set.add(k.toLowerCase());
              }
            }
            return set.size;
          }),
          prisma.trackedEvent.groupBy({
            by: ["trackedSiteId"],
            where: { eventType: "plugin_detected" },
          }).then((r) => r.length),
          prisma.realUserMetric.count().catch(() => 0),
        ]);
        firstPartyEventsGa4Cant = gtagInterceptCount.status === "fulfilled" ? gtagInterceptCount.value : 0;
        bookingFunnelInteractions = bookingCount.status === "fulfilled" ? bookingCount.value : 0;
        pluginsDetected = pluginCount.status === "fulfilled" ? pluginCount.value : 0;
        brandsWithPlugins = pluginBrands.status === "fulfilled" ? pluginBrands.value : 0;
        realUserCwv = cwvCount.status === "fulfilled" ? cwvCount.value : 0;
      } catch (err) {
        console.warn("[data-moat] moat metrics failed", err);
      }

      return {
        scope: { generatedAt: new Date().toISOString() },
        volumes,
        growth: { eventsByDay, cumulative, doubleDays },
        perBrand,
        moat: {
          firstPartyEventsGa4Cant,
          bookingFunnelInteractions,
          pluginsDetected,
          brandsWithPlugins,
          realUserCwv,
        },
      };
    }));
  } catch (err) {
    const message = err instanceof Error ? err.message : "unknown error";
    console.error("[admin/analytics/data-moat] failed", message);
    return NextResponse.json(
      { error: "Data moat aggregation failed.", detail: message },
      { status: 500 },
    );
  }
}
