/**
 * /api/site-tag — meSEO Site Tag Management
 * -------------------------------------------
 * GET  ?brandId=xxx           — get the tracked site info + snippet
 * POST { brandId, domain }    — create a tracked site for a brand
 *
 * Both require authentication. The user must be a member of the org
 * that owns the brand.
 */

import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import {
  createTrackedSite,
  getTrackedSite,
  getRecentEvents,
  toggleSiteStatus,
  regenerateTrackingKey,
  addAllowedDomain,
  removeAllowedDomain,
  getSiteTagInstalledAt,
  siteTagAgeDays,
  getSiteTagMaturity,
} from "@/lib/services/site-tag";
import {
  getReconciliationSummary,
  reconcileTrackedEvents,
} from "@/lib/services/site-tag-reconciliation";

export const dynamic = "force-dynamic";

export async function GET(req: Request) {
  // [PERF] timing — Site Tag page polls this every 30s, so keeping it
  // fast matters for the whole product. Logs major phases + total.
  const perfStart = Date.now();

  const t_auth = Date.now();
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const { searchParams } = new URL(req.url);
  const brandId = searchParams.get("brandId");
  const debugTiming = searchParams.get("debugTiming") === "1";
  if (debugTiming) console.log(`[timing] getCurrentUser ${Date.now() - t_auth}ms`);
  if (!brandId) return NextResponse.json({ error: "brandId required" }, { status: 400 });

  // Permission check
  const t_brandLookup = Date.now();
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (debugTiming) console.log(`[timing] brandLookup ${Date.now() - t_brandLookup}ms`);
  if (!brand || !brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  // Get the tracked site (or null if not created yet / table not migrated)
  const t_getTrackedSite = Date.now();
  let site;
  try {
    site = await getTrackedSite(brandId);
  } catch {
    site = null;
  }
  if (debugTiming) console.log(`[timing] getTrackedSite ${Date.now() - t_getTrackedSite}ms`);
  if (!site) {
    return NextResponse.json({
      site: null, recentEvents: [], eventsToday: 0, eventBreakdown: {},
      eventsPerHour: [], sessionsToday: 0, health: null, domainStatus: [],
      customEventsList: [], pluginsDetected: [], reconciliation: null, analytics: null,
      siteTagAge: 0, installedAt: null,
    });
  }

  // Recent events (last 15)
  const t_recentEvents = Date.now();
  const recentEvents = await getRecentEvents(brandId, 15);
  if (debugTiming) console.log(`[timing] recentEvents ${Date.now() - t_recentEvents}ms`);

  // Today's event count + breakdown by type
  const todayStart = new Date();
  todayStart.setHours(0, 0, 0, 0);
  // UTC midnight -- matches the SiteTagDailyRollup date key written by the cron.
  const todayStartUtc = new Date();
  todayStartUtc.setUTCHours(0, 0, 0, 0);

  let eventsToday = 0;
  const eventBreakdown: Record<string, number> = {};

  let sessionsToday = 0;
  let eventsPerHour: number[] = new Array(24).fill(0);

  if (site) {
    // Fast path: read today's rollup row for eventsToday, eventBreakdown, sessionsToday.
    // Avoids the TrackedEvent count + groupBy that timeout for high-volume brands.
    // Falls back to raw queries when the row is absent (new brand, cron lag) or
    // eventCountsByType is null.
    const t_todayRollup = Date.now();
    let todayRollupLoaded = false;
    try {
      const todayRollup = await prisma.siteTagDailyRollup.findFirst({
        where: { brandId, date: todayStartUtc },
        select: { sessions: true, eventCountsByType: true },
      });
      if (todayRollup !== null && todayRollup.eventCountsByType !== null) {
        const ect = todayRollup.eventCountsByType as Record<string, number>;
        for (const [type, count] of Object.entries(ect)) {
          eventBreakdown[type] = count;
          eventsToday += count;
        }
        sessionsToday = todayRollup.sessions;
        todayRollupLoaded = true;
      }
    } catch { /* rollup unavailable -- fall through to live queries */ }

    if (!todayRollupLoaded) {
      const [todayCount, todayBreakdown, sessions] = await Promise.all([
        prisma.trackedEvent.count({
          where: { trackedSiteId: site.id, timestamp: { gte: todayStart } },
        }),
        prisma.trackedEvent.groupBy({
          by: ["eventType"],
          where: { trackedSiteId: site.id, timestamp: { gte: todayStart } },
          _count: { eventType: true },
        }),
        prisma.trackedSession.count({
          where: { trackedSiteId: site.id, startedAt: { gte: todayStart } },
        }),
      ]);
      eventsToday = todayCount;
      for (const e of todayBreakdown) {
        eventBreakdown[e.eventType] = e._count.eventType;
      }
      sessionsToday = sessions;
    }

    if (debugTiming) console.log(`[timing] todayRollup ${Date.now() - t_todayRollup}ms`);

    // Bounded sample for the per-hour sparkline -- most recent 1000 events.
    // Accurate enough for a visual trend; not the total count.
    const t_hourlySample = Date.now();
    const hourlySample = await prisma.trackedEvent.findMany({
      where: { trackedSiteId: site.id, timestamp: { gte: todayStart } },
      select: { timestamp: true },
      orderBy: { timestamp: "desc" },
      take: 1000,
    });
    for (const e of hourlySample) {
      const hour = new Date(e.timestamp).getHours();
      eventsPerHour[hour]++;
    }
    if (debugTiming) console.log(`[timing] hourlySample ${Date.now() - t_hourlySample}ms`);
  }

  // Trim eventsPerHour to current hour + 1 so the chart isn't all zeros on the right
  const currentHour = new Date().getHours();
  eventsPerHour = eventsPerHour.slice(0, currentHour + 1);

  // Per-domain status: check which domains have actually sent events.
  // Previously ran 2 serial queries per domain (2N round-trips). Now
  // fans all out in one Promise.all — for a 3-domain brand that's 6
  // queries in parallel instead of 6 sequential, dropping the floor
  // on this block from ~300ms to ~50ms.
  let domainStatus: Array<{ domain: string; status: "verified" | "pending" | "mismatch"; eventsToday: number; lastEventAt: string | null }> = [];
  if (site) {
    const perDomainStart = Date.now();
    domainStatus = await Promise.all(
      site.allowedDomains.map(async (domain) => {
        const [domainEvents, lastDomainEvent] = await Promise.all([
          prisma.trackedEvent.count({
            where: {
              trackedSiteId: site.id,
              pageUrl: { startsWith: "https://" + domain + "/" },
              timestamp: { gte: todayStart },
              synthetic: false,
            },
          }),
          // Bound to 30 days -- without this the findFirst has no date filter
          // and scans the full TrackedEvent table for every allowed domain.
          // A domain with no event in 30 days is inactive; null lastEventAt
          // is the correct signal.
          prisma.trackedEvent.findFirst({
            where: {
              trackedSiteId: site.id,
              pageUrl: { startsWith: "https://" + domain + "/" },
              synthetic: false,
              timestamp: { gte: new Date(Date.now() - 30 * 86_400_000) },
            },
            orderBy: { timestamp: "desc" },
            select: { timestamp: true },
          }),
        ]);
        const hasEvents = !!lastDomainEvent;
        return {
          domain,
          status: (hasEvents ? "verified" : "pending") as "verified" | "pending" | "mismatch",
          eventsToday: domainEvents,
          lastEventAt: lastDomainEvent?.timestamp.toISOString() ?? null,
        };
      }),
    );
    console.info(`[PERF] site-tag domain-status (${site.allowedDomains.length} domains) ${Date.now() - perDomainStart}ms`);
    if (debugTiming) console.log(`[timing] domainStatus ${Date.now() - perDomainStart}ms`);
  }

  // Build health diagnostics
  const health = {
    tagInstalled: eventsToday > 0 || !!site.lastEventAt,
    tagActive: site.status === "active",
    domainsConfigured: site.allowedDomains.length > 0,
    receivingEvents: eventsToday > 0,
    sessionsToday,
    lastEventAge: site.lastEventAt
      ? Math.floor((Date.now() - new Date(site.lastEventAt).getTime()) / 60000)
      : null, // minutes since last event, or null
  };

  // Custom events: distinct event names that aren't standard types.
  // The standard list now includes the form / booking / plugin
  // event types added in the t.js v2 upgrade -- without this the
  // setup page's "Custom Events" list was showing booking_confirmed,
  // appointment_intent etc. as if a brand had set them up manually.
  let customEventsList: Array<{ name: string; count: number; lastFired: string }> = [];
  if (site) {
    const t_customEvents = Date.now();
    try {
      const standardTypes = [
        "page_view", "session_start", "form_submit", "click_to_call", "sms_submit",
        "form_start", "outbound_click", "appointment_intent", "form_intent",
        "form_iframe_present", "booking_widget_opened", "booking_step",
        "booking_submit", "booking_confirmed", "plugin_detected",
        // gtag-intercepted types (Site Tag v2.1)
        "custom_event", "purchase", "checkout_started", "add_to_cart",
      ];
      const standardSet = new Set(standardTypes);
      const since30d = new Date(Date.now() - 30 * 86_400_000);

      // Fast path: sum eventCountsByType across the last 30 rollup rows in memory.
      // Avoids the raw 90d TrackedEvent groupBy. The rollup covers 30 days
      // which matches what is pre-aggregated; fall back to the raw groupBy only
      // when no rollup rows exist (new brand or cron lag).
      let customRollupLoaded = false;
      try {
        const rollupRows30d = await prisma.siteTagDailyRollup.findMany({
          where: { brandId, date: { gte: since30d } },
          select: { date: true, eventCountsByType: true },
          orderBy: { date: "desc" },
        });
        if (rollupRows30d.length > 0) {
          const typeTotals: Record<string, number> = {};
          const typeLastDate: Record<string, Date> = {};
          for (const row of rollupRows30d) {
            if (!row.eventCountsByType) continue;
            const ect = row.eventCountsByType as Record<string, number>;
            for (const [type, count] of Object.entries(ect)) {
              if (count <= 0) continue;
              typeTotals[type] = (typeTotals[type] ?? 0) + count;
              // rows are desc-ordered -- first occurrence per type is the most recent date
              if (!typeLastDate[type]) typeLastDate[type] = row.date;
            }
          }
          customEventsList = Object.entries(typeTotals)
            .filter(([type]) => !standardSet.has(type))
            .map(([name, count]) => ({
              name,
              count,
              lastFired: (typeLastDate[name] ?? new Date()).toISOString(),
            }))
            .sort((a, b) => b.count - a.count);
          customRollupLoaded = true;
        }
      } catch { /* fall through to live query */ }

      if (!customRollupLoaded) {
        const since90d = new Date(Date.now() - 90 * 86_400_000);
        const customs = await prisma.trackedEvent.groupBy({
          by: ["eventType"],
          where: { trackedSiteId: site.id, eventType: { notIn: standardTypes }, timestamp: { gte: since90d } },
          _count: true,
          _max: { timestamp: true },
        });
        customEventsList = customs.map(c => ({
          name: c.eventType,
          count: c._count,
          lastFired: c._max.timestamp?.toISOString() ?? new Date().toISOString(),
        })).sort((a, b) => b.count - a.count);
      }
    } catch { /* skip */ }
    if (debugTiming) console.log(`[timing] customEventsList ${Date.now() - t_customEvents}ms`);
  }

  // Detected booking / form plugins — surfaces "we see you're using
  // <plugin>" on the setup page so the brand has confidence their
  // specific tech stack is being tracked. Reads plugin_detected events
  // from the last 30 days and dedupes by plugin_key.
  let pluginsDetected: Array<{
    name: string; key: string; category: string; firstSeenAt: string; lastSeenAt: string;
    trackingLevel: "full" | "limited"; trackingNote: string;
  }> = [];
  if (site) {
    const t_plugins = Date.now();
    try {
      const since = new Date(Date.now() - 30 * 86_400_000);
      const rows = await prisma.trackedEvent.findMany({
        where: { trackedSiteId: site.id, eventType: "plugin_detected", timestamp: { gte: since } },
        orderBy: { timestamp: "desc" },
        select: { timestamp: true, metadata: true },
        take: 200,
      });
      const byKey = new Map<string, { firstSeen: Date; lastSeen: Date; name: string }>();
      for (const row of rows) {
        const meta = (row.metadata ?? {}) as Record<string, unknown>;
        // Handle new consolidated payload { plugins: [...] } and legacy flat shape.
        const pluginEntries: Array<{ key: string; name: string }> = [];
        if (Array.isArray(meta.plugins)) {
          for (const p of meta.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.toLowerCase() : null;
            if (!k) continue;
            pluginEntries.push({ key: k, name: typeof pm.plugin === "string" ? pm.plugin : k });
          }
        } else {
          const key = (typeof meta.plugin_key === "string" ? meta.plugin_key : null)?.toLowerCase();
          const name = typeof meta.plugin === "string" ? meta.plugin : null;
          if (key) pluginEntries.push({ key, name: name ?? key });
        }
        for (const { key, name } of pluginEntries) {
          const existing = byKey.get(key);
          if (existing) {
            // rows are desc; later iterations carry an earlier timestamp,
            // so update firstSeen to the older one.
            existing.firstSeen = row.timestamp;
          } else {
            byKey.set(key, { firstSeen: row.timestamp, lastSeen: row.timestamp, name });
          }
        }
      }
      // Tracking-capability map mirrors src/lib/services/site-tag-analytics.ts.
      // Duplicated here (rather than imported) to keep this route's
      // bundle small and avoid pulling the analytics service into the
      // setup-page request path.
      const CAP: Record<string, { name: string; trackingLevel: "full" | "limited"; trackingNote: string; category?: string }> = {
        // Booking / form plugins
        amelia:        { name: "Amelia",            trackingLevel: "full",    trackingNote: "Tracks every booking step, AJAX submission, and confirmation panel." },
        calendly:      { name: "Calendly",          trackingLevel: "limited", trackingNote: "Iframe-based — open / datetime-selected / scheduled signals via postMessage." },
        nexhealth:     { name: "NexHealth",         trackingLevel: "limited", trackingNote: "Iframe-based — widget opens + confirmation messages when exposed." },
        acuity:        { name: "Acuity Scheduling", trackingLevel: "limited", trackingNote: "Iframe-based — scheduled signal via postMessage." },
        jotform:       { name: "JotForm",           trackingLevel: "limited", trackingNote: "Iframe-based — submission-completed event via postMessage." },
        typeform:      { name: "Typeform",          trackingLevel: "limited", trackingNote: "Iframe-based — form-submit event via postMessage." },
        wpforms:       { name: "WPForms",           trackingLevel: "full",    trackingNote: "Tracks both native form submits and AJAX-mode submissions." },
        gravityforms:  { name: "Gravity Forms",     trackingLevel: "full",    trackingNote: "Tracks the gform_confirmation_loaded DOM event and AJAX submissions." },
        contactform7:  { name: "Contact Form 7",    trackingLevel: "full",    trackingNote: "Tracks the wpcf7mailsent confirmation event." },
        hubspot:       { name: "HubSpot Forms",     trackingLevel: "limited", trackingNote: "Iframe-based — onFormSubmitted event via postMessage." },
        // Chat widgets — full tracking for vendors with SDK event hooks
        chat_intercom:    { name: "Intercom",          category: "chat", trackingLevel: "full",    trackingNote: "Chat open/hide events via Intercom SDK hooks." },
        chat_drift:       { name: "Drift",             category: "chat", trackingLevel: "full",    trackingNote: "Chat open + message events via Drift SDK hooks." },
        chat_tidio:       { name: "Tidio",             category: "chat", trackingLevel: "full",    trackingNote: "Chat open + message events via Tidio SDK hooks." },
        chat_tawkto:      { name: "Tawk.to",           category: "chat", trackingLevel: "full",    trackingNote: "Chat start + message events via Tawk_API hooks." },
        chat_livechat:    { name: "LiveChat",           category: "chat", trackingLevel: "limited", trackingNote: "Detected via LC_API global or script src." },
        chat_crisp:       { name: "Crisp",             category: "chat", trackingLevel: "limited", trackingNote: "Detected via $crisp global or script src." },
        chat_olark:       { name: "Olark",             category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.olark global or script src." },
        chat_hubspot_chat:{ name: "HubSpot Chat",      category: "chat", trackingLevel: "limited", trackingNote: "Detected via HubSpotConversations global or script src." },
        chat_zendesk:     { name: "Zendesk Chat",      category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.zE global or script src." },
        chat_fb_messenger:{ name: "Facebook Messenger", category: "chat", trackingLevel: "limited", trackingNote: "Detected via FB.CustomerChat global or script src." },
        chat_smartsupp:   { name: "Smartsupp",         category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.smartsupp global or script src." },
        chat_jivochat:    { name: "JivoChat",           category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.jivo_api global or script src." },
        chat_purechat:    { name: "Pure Chat",         category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.purechatApi global or script src." },
        chat_userlike:    { name: "Userlike",          category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.userlikeStart global or script src." },
        chat_frontchat:   { name: "Front Chat",        category: "chat", trackingLevel: "limited", trackingNote: "Detected via window.FrontChat global or script src." },
      };
      pluginsDetected = Array.from(byKey.entries()).map(([key, info]) => {
        const cap = CAP[key];
        return {
          key,
          name: cap?.name ?? info.name,
          category: cap?.category ?? "booking",
          firstSeenAt: info.firstSeen.toISOString(),
          lastSeenAt: info.lastSeen.toISOString(),
          trackingLevel: cap?.trackingLevel ?? "limited",
          trackingNote: cap?.trackingNote ?? "Custom plugin — basic detection only.",
        };
      }).sort((a, b) => a.name.localeCompare(b.name));
    } catch { /* skip -- plugin detection is optional */ }
    if (debugTiming) console.log(`[timing] pluginsDetected ${Date.now() - t_plugins}ms`);
  }

  // Reconciliation: fire-and-forget the reconcile pass so N+1 SourceEvent
  // lookups don't block this request path. Summary reads from already-
  // reconciled CanonicalEvent/SourceEvent rows and is fast.
  let reconciliation = null;
  if (site) {
    const t_reconciliation = Date.now();
    try {
      void reconcileTrackedEvents(site.id).catch(() => {});
      reconciliation = await getReconciliationSummary(site.id);
    } catch { /* reconciliation is optional -- don't block the page */ }
    if (debugTiming) console.log(`[timing] reconciliation ${Date.now() - t_reconciliation}ms`);
  }

  // Derive install date from firstEventAt for the "deployed, waiting" state,
  // then overlay with rollup-based maturity for the "capturing since" banner.
  // getSiteTagMaturity runs in parallel with getSiteTagInstalledAt so neither
  // adds serial latency to the other.
  const t_maturity = Date.now();
  const [installedAt, maturity] = await Promise.all([
    getSiteTagInstalledAt(brandId).catch(() => null),
    getSiteTagMaturity(brandId).catch(() => ({ activeDays: 0, captureStart: null })),
  ]);
  if (debugTiming) console.log(`[timing] installedAt+maturity ${Date.now() - t_maturity}ms`);

  const siteTagAge = maturity.activeDays > 0 ? maturity.activeDays : siteTagAgeDays(installedAt);
  const maturityInstalledAt = maturity.captureStart ?? installedAt;

  console.info(`[PERF] site-tag GET total ${Date.now() - perfStart}ms`);
  return NextResponse.json({
    site,
    recentEvents,
    eventsToday,
    eventBreakdown,
    eventsPerHour,
    sessionsToday,
    health,
    domainStatus,
    customEventsList,
    pluginsDetected,
    reconciliation,
    analytics: null,
    siteTagAge,
    installedAt: maturityInstalledAt?.toISOString() ?? null,
  });
}

export async function POST(req: Request) {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const body = await req.json();
  const { brandId } = body as { brandId: string };

  if (!brandId) return NextResponse.json({ error: "brandId required" }, { status: 400 });

  // Permission check
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (!brand || !brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  // Create the tracked site
  const site = await createTrackedSite(brand.orgId, brandId, brand.domain);

  return NextResponse.json({ site });
}

/**
 * PATCH /api/site-tag
 * Actions: toggle_status, regenerate_key, add_domain, remove_domain
 */
export async function PATCH(req: Request) {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const body = await req.json();
  const { brandId, action, domain } = body as {
    brandId: string;
    action: "toggle_status" | "regenerate_key" | "add_domain" | "remove_domain";
    domain?: string;
  };

  if (!brandId || !action) {
    return NextResponse.json({ error: "brandId and action required" }, { status: 400 });
  }

  // Permission check
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (!brand || !brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  switch (action) {
    case "toggle_status": {
      const site = await toggleSiteStatus(brandId);
      return NextResponse.json({ site });
    }
    case "regenerate_key": {
      const result = await regenerateTrackingKey(brandId);
      if (!result) return NextResponse.json({ error: "Site not found" }, { status: 404 });
      return NextResponse.json({ ok: true, message: "Tracking key regenerated" });
    }
    case "add_domain": {
      if (!domain) return NextResponse.json({ error: "domain required" }, { status: 400 });
      // Strip protocol only — preserve path so entries like quiz.pbdquiz.com/sf/2fc04c39 are stored as-is.
      const normalized = domain.trim().replace(/^https?:\/\//i, "").toLowerCase();
      const hostname = normalized.split("/")[0].split(":")[0];
      if (!hostname.includes(".") || hostname.length < 4 || normalized.length > 2048 || /\s/.test(hostname)) {
        return NextResponse.json({ error: "Invalid domain" }, { status: 400 });
      }
      const site = await addAllowedDomain(brandId, normalized);
      return NextResponse.json({ site });
    }
    case "remove_domain": {
      if (!domain) return NextResponse.json({ error: "domain required" }, { status: 400 });
      const site = await removeAllowedDomain(brandId, domain);
      return NextResponse.json({ site });
    }
    default:
      return NextResponse.json({ error: "Unknown action" }, { status: 400 });
  }
}
