"use client";

import { useState, useEffect, useCallback, useRef } from "react";
import { useSearchParams } from "next/navigation";
import { useBrand } from "@/context/brand-context";
import {
  BarChart3, TrendingUp, Target, Share2, MousePointer,
  Activity, Video, AlertCircle, Info, RefreshCw, Bot, AlertTriangle, Layers, Puzzle,
} from "lucide-react";
import { ConversionSettingsModal, type ConversionTypeRow } from "./conversion-settings-modal";
import type { ConversionTier } from "@/lib/conversions/tiers";
import { getDefaultTier } from "@/lib/conversions/tiers";
import { OverviewTab, type ReconciliationData } from "./overview-tab";
import { ConversionsTab } from "./conversions-tab";
import { EmbeddedConversionsTab } from "./embedded-conversions-tab";
import { FunnelTab } from "./funnel-tab";
import { AttributionTab } from "./attribution-tab";
import { HeatmapsTab } from "./heatmaps-tab";
import { UxTab } from "./ux-tab";
import { VideoChatTab } from "./video-chat-tab";
import { AiAttributionTab } from "./ai-attribution-tab";
import { IntegrationsTab } from "./integrations-tab";
import { SetupPage } from "./setup-page";
import { DetectedIntegrationsPanel } from "@/components/site-tag/DetectedIntegrationsPanel";
import { isAbortError } from "@/lib/utils/is-abort-error";

// ─── Tab definitions ────────────────────────────────────────────────────────────────────

const TABS = [
  { key: "overview",              label: "Overview",             icon: BarChart3 },
  { key: "conversions",           label: "Conversions",          icon: TrendingUp },
  { key: "embedded-conversions",  label: "Embedded Conversions", icon: Layers },
  { key: "integrations",          label: "Integrations",         icon: Puzzle },
  { key: "funnel",                label: "Conversion Funnel",    icon: Target },
  { key: "attribution", label: "Attribution",       icon: Share2 },
  { key: "ai",          label: "AI Attribution",    icon: Bot },
  { key: "heatmaps",    label: "Heatmaps",          icon: MousePointer },
  { key: "ux",          label: "UX Insights",       icon: Activity },
  { key: "video",       label: "Video / Chat",      icon: Video },
] as const;

type TabKey = (typeof TABS)[number]["key"];

// ─── Shared types ────────────────────────────────────────────────────────────────────

type PageStatus = "loading" | "no_tag" | "partial" | "ready" | "not_connected" | "error";

interface TagStatus {
  installed: boolean;
  status: string | null;
  totalEvents: number;
  /** Real lifetime COUNT(*) when the analytics API's count query
   *  resolved inside its 5s race window; null when the count timed
   *  out. Downstream consumers rendering a "total lifetime" number
   *  should prefer this field. */
  lifetimeEventsExact?: number | null;
  lastEventAt: string | null;
}

interface GA4Data {
  dataOrigin: "conversion_event" | "metric_snapshot" | "none";
  totals: { conversions: number; revenue: number; avgValue: number; sessions: number; conversionRate: number; siteTagConversions?: number };
  changes: {
    conversions: { pct: number; direction: string };
    revenue: { pct: number; direction: string };
    conversionRate: { pct: number; direction: string };
    sessions: { pct: number; direction: string };
  };
  byType: Array<{ type: string; count: number; revenue: number; change: { pct: number; direction: string } }>;
  byChannel: Array<{ channel: string; count: number; revenue: number; change: { pct: number; direction: string } }>;
  topPages: Array<{ name: string; count: number; revenue: number }>;
  dailyTrend: Array<{ date: string; count: number | null; revenue: number; siteTagCount?: number }>;
}

interface TrafficData {
  pageViews: number;
  sessions: number;
  pagesPerSession: number;
  topPages: Array<{ url: string; views: number }>;
  byReferrer: Array<{ source: string; views: number }>;
  byUtmSource: Array<{ source: string; views: number }>;
}

interface ConvSummary {
  totalConversions: number;
  totalInteractions?: number;
  byType: Record<string, number>;
  byTypeInteractions?: Record<string, number>;
  conversionRate: number;
  topConvertingPages: Array<{ url: string; conversions: number }>;
  byUtmSource: Array<{ source: string; conversions: number }>;
}

interface ConversionTiers {
  counted: number;
  byTier: Record<ConversionTier | "unknown", { total: number; counted: number }>;
  byType: Record<string, { count: number; tier: ConversionTier | "unknown"; counted: boolean; label: string }>;
}

// ─── Page ─────────────────────────────────────────────────────────────────────────

const VALID_TABS = new Set<string>(TABS.map((t) => t.key));

export default function SiteTagAnalyticsPage() {
  const { brand } = useBrand();
  const searchParams = useSearchParams();
  const initialTab = searchParams.get("tab");
  const [tab, setTab] = useState<TabKey>(
    initialTab && VALID_TABS.has(initialTab) ? (initialTab as TabKey) : "overview",
  );
  const [range, setRange] = useState<"1d" | "7d" | "28d" | "30d" | "90d">("28d");

  useEffect(() => {
    const paramTab = searchParams.get("tab");
    if (paramTab && VALID_TABS.has(paramTab) && paramTab !== tab) {
      setTab(paramTab as TabKey);
    }
  }, [searchParams]);
  const [status, setStatus] = useState<PageStatus>("loading");
  const [errorMsg, setErrorMsg] = useState<string | null>(null);

  // Data from both sources
  const [ga4Data, setGa4Data] = useState<GA4Data | null>(null);
  const [trafficData, setTrafficData] = useState<TrafficData | null>(null);
  const [convData, setConvData] = useState<ConvSummary | null>(null);
  const [trendsData, setTrendsData] = useState<Array<{ hour: number; count: number }>>([]);
  const [dailyPageViews, setDailyPageViews] = useState<Array<{ date: string; pageViews: number; sessions: number }>>([]);
  const [recentEvents, setRecentEvents] = useState<Array<{ id: string; eventType: string; pageUrl: string; timestamp: string; synthetic?: boolean }>>([]);
  const [eventsToday, setEventsToday] = useState(0);
  const [eventsThisWeek, setEventsThisWeek] = useState(0);
  const [activePagesCount, setActivePagesCount] = useState(0);
  const [todayByType, setTodayByType] = useState<Array<{ type: string; count: number }>>([]);
  const [pluginsDetected, setPluginsDetected] = useState<Array<{
    name: string; key: string; firstSeenAt: string; lastSeenAt: string;
    trackingLevel: "full" | "limited"; trackingNote: string;
  }>>([]);
  const [formFunnel, setFormFunnel] = useState<{
    intent: number; started: number; submitted: number; confirmed: number;
    dropoffStarted: number | null; dropoffSubmitted: number | null; dropoffConfirmed: number | null;
  } | undefined>(undefined);
  const [outboundClicks, setOutboundClicks] = useState<{
    totalClicks: number;
    topDestinations: Array<{ domain: string; url: string; text: string; count: number; sourcePage?: string | null }>;
  }>({ totalClicks: 0, topDestinations: [] });
  const [customEventsCount, setCustomEventsCount] = useState(0);
  const [reconciliation, setReconciliation] = useState<ReconciliationData | null>(null);
  const [conversionTiers, setConversionTiers] = useState<ConversionTiers | null>(null);
  const [convSettingsOpen, setConvSettingsOpen] = useState(false);
  const [convUpdating, setConvUpdating] = useState(false);
  const [sitePlatform, setSitePlatform] = useState<{
    name: string | null; version: string | null; confidence: string | null;
    category: string | null; detectedAt: string | null; hosting: string | null; hostingDetectedAt: string | null;
  } | null>(null);
  const [hasGa4, setHasGa4] = useState(false);
  const [hasTag, setHasTag] = useState(false);
  const [tagStatus, setTagStatus] = useState<TagStatus | null>(null);
  const [siteTagAge, setSiteTagAge] = useState(0);
  const [siteTagInstalledAt, setSiteTagInstalledAt] = useState<string | null>(null);
  // TrackedSite row-creation timestamp — when the user deployed the
  // Site Tag. Distinct from siteTagInstalledAt (first captured event)
  // and used by the overview history bar to render a "waiting for
  // first event" state when the tag is deployed but no events have
  // been captured yet.
  const [siteTagDeployedAt, setSiteTagDeployedAt] = useState<string | null>(null);
  const [tagApiError, setTagApiError] = useState<string | null>(null);
  // Server signalled the analytics race hit its 12s budget and is
  // returning what it has. Renders a soft "still loading" banner
  // instead of the red error banner. Cleared on the next non-partial
  // poll so a partial response can heal in place without manual retry.
  const [tagPartial, setTagPartial] = useState(false);
  const [refreshing, setRefreshing] = useState(false);
  const [isLive, setIsLive] = useState(true);
  const [lastUpdatedAt, setLastUpdatedAt] = useState<number | null>(null);
  const [pollingStopped, setPollingStopped] = useState(false);
  const [pollingRestartKey, setPollingRestartKey] = useState(0);
  const [healthAlert, setHealthAlert] = useState<{
    id: string; dropPct: number; expectedRate: number; actualRate: number;
    firstDetectedAt: string; consecutiveHoursDegraded: number;
  } | null>(null);
  // Forces the "Last updated: Xs ago" / "Last Event: Xs ago" displays
  // to re-render every second so the relative timestamps stay accurate
  // without hitting the network. Previously ran at 5s, which made the
  // Xs-ago label jump in 5-second steps and visually reinforced the
  // "is polling actually running?" uncertainty. 1s is cheap — it
  // triggers a React re-render with no DOM diff cost beyond the text
  // nodes that actually carry the relative-time values.
  const [, setTick] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setTick((n) => n + 1), 1000);
    return () => clearInterval(t);
  }, []);
  // Grace-period timer: error banner only fires after 500ms so a
  // successful auto-retry can clear the pending timer before it shows.
  const errorBannerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const loadingRef = useRef(false);
  const initialLoadedRef = useRef(false);
  // Separate in-flight guard for the lightweight /pulse loop so a
  // slow pulse response never stacks up concurrent requests each
  // holding one of the 5 Neon connections. Distinct from loadingRef
  // (which guards the heavy /analytics bundle) so the two loops
  // don't starve each other when one is mid-flight.
  const pulseInFlightRef = useRef(false);
  // Tracks the range value used in the last loadData() call. Used to
  // gate data-state clearing: only clear on range change (not on every
  // 30s poll), so tiles don't flash to 0 during the fetch window.
  const lastFetchedRangeRef = useRef<string | null>(null);

  const loadData = useCallback(async () => {
    // Don't fire against a placeholder brand. The BrandProvider starts
    // with { id: "" } and resolves the real active brand asynchronously
    // via /api/me → /api/brands. If loadData ran on mount before the
    // resolution, the request went out as ?brandId= (empty) and the
    // server correctly returned 400 — which then rendered as
    // "Error loading analytics data — Server error (HTTP 400)"
    // for the brief window before brand.id settled. useCallback
    // re-creates loadData when brand.id changes, which re-fires the
    // mount useEffect, so the real fetch still runs once the brand
    // is known. Nothing to show the user in the meantime; the
    // full-page loading spinner (set just below) covers the gap.
    if (!brand.id) return;
    // Stack-prevention: the page polls every 30s. Without this guard,
    // a slow request (the route used to hang for the full 300s before
    // we added a 12s server deadline) caused subsequent polls to pile
    // up and saturate the function. loadingRef short-circuits new
    // calls until the in-flight one resolves.
    if (loadingRef.current) return;
    loadingRef.current = true;

    // Data states clear only on range change (or initial load).
    // On polling ticks the range is the same, so we skip the clear and
    // let tiles update in place when the response arrives — preventing
    // the 5–12s window where tiles flash to 0 on every 30s heavy poll.
    // Error/loading state contract (below) always resets per loadData call.
    // See contract block at the "mutual-exclusion contract" comment below.
    const rangeChanged = range !== lastFetchedRangeRef.current;
    if (rangeChanged) {
      setGa4Data(null);
      setTrafficData(null);
      setConvData(null);
      setTrendsData([]);
      setDailyPageViews([]);
      setRecentEvents([]);
      setEventsThisWeek(0);
      setActivePagesCount(0);
      setTodayByType([]);
      setOutboundClicks({ totalClicks: 0, topDestinations: [] });
      setPluginsDetected([]);
      setFormFunnel(undefined);
      setConversionTiers(null);
      setReconciliation(null);
    }

    // ── Error/loading state mutual-exclusion contract ────────────────────
    // Five state variables govern error and loading state:
    //   status      – PageStatus ("loading"|"ready"|"partial"|"not_connected"|"error")
    //   errorMsg    – human-readable message shown on the full-page error screen
    //   tagApiError – non-null string shown in the red inline banner; blocks
    //                 both blue banners below (guarded at lines 766 and 781)
    //   tagPartial  – true only when the analytics API returned { partial: true }
    //                 and the tag fetch otherwise succeeded (no HTTP error)
    //   refreshing  – true while a fetch is in flight; drives the spinner UI
    //
    // Valid combinations (enforced here, at the top of every loadData() call):
    //   tagPartial=true  → tagApiError must be null   (partial ≠ error)
    //   tagApiError≠null → tagPartial must be false   (error overrides partial)
    //   status="error"   → errorMsg must be non-null  (error screen needs text)
    //
    // This block is the single enforcement point. All five vars are reset
    // together so no prior call's state can leak into the next fetch.
    //
    // NOTE: if this state machine grows further (e.g. adding a "degraded"
    // mode), consider refactoring to a discriminated union rather than
    // adding a sixth boolean.
    setTagPartial(false);

    // Only show the full-page spinner on the very first load. Subsequent
    // polls and manual refreshes update data in place.
    if (!initialLoadedRef.current) setStatus("loading");
    setErrorMsg(null);
    if (errorBannerTimerRef.current) clearTimeout(errorBannerTimerRef.current);
    setTagApiError(null);
    setRefreshing(true);
    // Stamp the range BEFORE the fetch so a concurrent poll that slips
    // past loadingRef sees the correct "last fetched" range and doesn't
    // double-clear on the next call.
    lastFetchedRangeRef.current = range;

    // 15s client timeout matches the server's 12s handler deadline +
    // a 3s network buffer. After that we abandon and let the user
    // see the timeout banner instead of an infinite spinner — the
    // server will still finish in the background and the next poll
    // picks up the cached response.
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 65_000);

    try {
      // Fetch data sources in parallel. /api/site-tag is the source of
      // truth for the Data Reconciliation block and the Custom Events count,
      // so it rides along with the primary analytics query.
      //
      // `live=true` + `_t=<ms>` + `cache: "no-store"` defeat the browser
      // HTTP cache. Without this, the analytics route's
      // `Cache-Control: private, max-age=30` meant a 5s poll returned
      // the same cached body for 30 seconds — users saw "Last Event"
      // stuck until max-age expired even though new events were
      // landing in the DB every few seconds. The server also reads
      // `live=true` and downgrades to no-store on its side so any
      // intermediate Vercel CDN / service-worker layer also bypasses.
      const fetchStart = Date.now();
      const cacheBust = `live=true&_t=${Date.now()}`;
      const [ga4Res, tagRes, siteRes] = await Promise.allSettled([
        fetch(`/api/conversion-intelligence?brandId=${brand.id}&range=${range}`, { signal: controller.signal }),
        fetch(`/api/site-tag/analytics?brandId=${brand.id}&range=${range}&${cacheBust}`, { cache: "no-store", signal: controller.signal }),
        fetch(`/api/site-tag?brandId=${brand.id}`, { cache: "no-store", signal: controller.signal }),
      ]);
      console.info("[SiteTagAnalytics] fetch settled", {
        elapsedMs: Date.now() - fetchStart,
        ga4: ga4Res.status === "fulfilled" ? { ok: ga4Res.value.ok, status: ga4Res.value.status } : { rejected: String(ga4Res.reason) },
        tag: tagRes.status === "fulfilled" ? { ok: tagRes.value.ok, status: tagRes.value.status } : { rejected: String(tagRes.reason) },
        site: siteRes.status === "fulfilled" ? { ok: siteRes.value.ok, status: siteRes.value.status } : { rejected: String(siteRes.reason) },
      });

      // Process /api/site-tag — used for reconciliation + custom events count
      if (siteRes.status === "fulfilled" && siteRes.value.ok) {
        try {
          const siteData = await siteRes.value.json();
          setReconciliation(siteData.reconciliation ?? null);
          setCustomEventsCount((siteData.customEventsList ?? []).length);
        } catch {
          setReconciliation(null);
          setCustomEventsCount(0);
        }
      } else {
        setReconciliation(null);
        setCustomEventsCount(0);
      }

      // Process GA4 data — wrap the JSON parse so a malformed response
      // doesn't bubble to the outer catch and tank the whole page.
      // Previously an unwrapped .json() throw here would leave the
      // page on `status="error"` even when the Site Tag analytics
      // call (the actual source of truth for this view) succeeded.
      //
      // Process GA4 conversion data from conv-intel.
      // hasGa4 is NOT set here — it's read from the tag analytics response
      // (tagResult.sources.ga4) so both this page and /api/conversion-intelligence
      // derive status from the same getConnectedSources() call, keeping
      // the two endpoints in sync without a separate status-only fetch.
      let ga4Ok = false;
      if (ga4Res.status === "fulfilled" && ga4Res.value.ok) {
        try {
          const result = await ga4Res.value.json();
          // ga4Ok is used below (after the tag parse) to gate setGa4Data.
          // We intentionally do NOT call setHasGa4 here; see tag block.
          ga4Ok = !!(result.sources?.ga4);
          // Only populate the GA4 data block when GA4 is truly connected.
          // When the API has data but sources.ga4 is false (GSC-only
          // brands get a data payload driven by GSC conversions), we
          // explicitly set null so the GA4 cards don't render with
          // GSC-backed numbers labelled as GA4.
          if (ga4Ok && result.data) {
            setGa4Data(result.data);
          } else {
            setGa4Data(null);
          }
        } catch (err) {
          if (!isAbortError(err)) console.error("[SiteTagAnalytics] GA4 JSON parse failed", err);
          setGa4Data(null);
        }
      } else {
        setGa4Data(null);
      }

      // Process Site Tag data
      let tagInstalled = false;
      let tagMeta: TagStatus | null = null;
      let apiError: string | null = null;
      // Captured from the payload for the [POLL] received log at the
      // bottom of the try block — state setters are async, so reading
      // the state variable after setEventsToday() would give the prior
      // poll's value. Capture at parse time instead.
      let loggedEventsToday: number | null = null;
      let loggedPartial = false;

      if (tagRes.status === "fulfilled" && tagRes.value.ok) {
        // Same defensive parse — a bad body here is the most likely
        // path to "Failed to Load" given the user's report (API
        // responds 200 fast but page errors anyway).
        let tagResult: Record<string, unknown> | null = null;
        try {
          tagResult = await tagRes.value.json();
          console.info("[SiteTagAnalytics] tag payload keys", { keys: tagResult ? Object.keys(tagResult) : null });
        } catch (err) {
          if (!isAbortError(err)) console.error("[SiteTagAnalytics] tag JSON parse failed", err);
          apiError = "Server returned malformed data — try again in a moment.";
          setTagApiError(apiError);
        }

        if (tagResult) {
          tagMeta = (tagResult.tagStatus as TagStatus | undefined) ?? null;
          setTagStatus(tagMeta);
          loggedEventsToday = (tagResult.eventsToday as number | undefined) ?? null;
          loggedPartial = tagResult.partial === true;
          // partial=true ⇒ the server's analytics race hit its 12s deadline
          // and returned what it had (always tagStatus, often nothing else).
          // installedAt is then null because the bounded sub-query timed out.
          // Don't overwrite a previously-known install date with null — that
          // would make the data-maturity banner regress every time the race
          // misses, even on a brand the previous poll resolved cleanly.
          const isPartial = tagResult.partial === true;
          setTagPartial(isPartial);
          if (!isPartial || tagResult.installedAt) {
            setSiteTagAge((tagResult.siteTagAge as number | undefined) ?? 0);
            setSiteTagInstalledAt((tagResult.installedAt as string | null | undefined) ?? null);
          }
          // siteTagDeployedAt is independent of the analytics race —
          // it comes from the always-loaded TrackedSite row, so it's
          // safe to update on every poll regardless of partial state.
          if (tagResult.siteTagDeployedAt !== undefined) {
            setSiteTagDeployedAt((tagResult.siteTagDeployedAt as string | null | undefined) ?? null);
          }

          // hasGa4 is set here (not in the conv-intel block above) so both
          // routes share the same getConnectedSources() call as the single
          // source of truth. On a partial response sources may be absent;
          // fall back to the ga4Ok we captured from conv-intel so a deadline
          // hit doesn't flip the GA4 cards off for one poll.
          const tagSources = tagResult.sources as { ga4?: boolean; gsc?: boolean } | undefined;
          setHasGa4(tagSources ? !!tagSources.ga4 : ga4Ok);

          // API may return 200 with an error field (top-level catch) — surface
          // the error but still render the dashboard shell rather than setup.
          if (tagResult.error) {
            apiError = String(tagResult.error);
            setTagApiError(apiError);
            // Treat this as "installed" so the dashboard renders. The user sees
            // empty cards + an error banner instead of the misleading setup page.
            tagInstalled = true;
            setHasTag(true);
            setTrafficData((tagResult.traffic as Parameters<typeof setTrafficData>[0]) ?? null);
            setConvData((tagResult.conversions as Parameters<typeof setConvData>[0]) ?? null);
            setTrendsData((tagResult.trends as Parameters<typeof setTrendsData>[0]) ?? []);
            setDailyPageViews((tagResult.dailyPageViews as Parameters<typeof setDailyPageViews>[0]) ?? []);
            setRecentEvents((tagResult.recentEvents as Parameters<typeof setRecentEvents>[0]) ?? []);
            setEventsToday((tagResult.eventsToday as number | undefined) ?? 0);
            setEventsThisWeek((tagResult.eventsThisWeek as number | undefined) ?? 0);
            setActivePagesCount((tagResult.activePagesCount as number | undefined) ?? 0);
            setTodayByType((tagResult.todayByType as Parameters<typeof setTodayByType>[0]) ?? []);
            setOutboundClicks((tagResult.outboundClicks as Parameters<typeof setOutboundClicks>[0]) ?? { totalClicks: 0, topDestinations: [] });
            setPluginsDetected((tagResult.pluginsDetected as Parameters<typeof setPluginsDetected>[0]) ?? []);
            setFormFunnel((tagResult.formFunnel as Parameters<typeof setFormFunnel>[0]) ?? undefined);
          } else {
            setTagApiError(null);
            // Show the dashboard when the TrackedSite record exists.
            if (tagMeta?.installed) {
              tagInstalled = true;
              setHasTag(true);
              // On a partial response, only assign sections that actually
              // came back with data. The server emits empty-shape defaults
              // (pageViews:0, etc.) when the analytics race misses, so a
              // blanket assignment here would clobber the previous poll's
              // good data with zeros and make the page flicker to "—" /
              // "0" between polls. Each section is gated on its presence
              // independently so a future server change that resolves
              // some sections but not others (e.g. traffic ok, trends
              // pending) shows the resolved ones live and lets the rest
              // catch up on the next poll.
              // Cast to the value types directly (not Parameters<setX>[0])
              // because the latter resolves to SetStateAction, which is the
              // value-or-updater union and can't be narrowed for property
              // access without an extra typeof guard.
              const traffic = tagResult.traffic as TrafficData | null | undefined;
              const conversions = tagResult.conversions as ConvSummary | null | undefined;
              const trends = tagResult.trends as Array<{ hour: number; count: number }> | undefined;
              const daily = tagResult.dailyPageViews as Array<{ date: string; pageViews: number; sessions: number }> | undefined;
              const recent = tagResult.recentEvents as Array<{ id: string; eventType: string; pageUrl: string; timestamp: string; synthetic?: boolean }> | undefined;
              const outbound = tagResult.outboundClicks as { totalClicks: number; topDestinations: Array<{ domain: string; url: string; text: string; count: number; sourcePage?: string | null }> } | undefined;
              const today = tagResult.todayByType as Array<{ type: string; count: number }> | undefined;
              const hasAnyTraffic = !!(traffic && (traffic.pageViews || traffic.sessions || (traffic.topPages?.length ?? 0) > 0));
              const hasAnyConversions = !!(conversions && (conversions.totalConversions || Object.keys(conversions.byType ?? {}).length > 0));

              if (!isPartial || hasAnyTraffic) setTrafficData(traffic ?? null);
              if (!isPartial || hasAnyConversions) setConvData(conversions ?? null);
              if (!isPartial || (trends?.length ?? 0) > 0) setTrendsData(trends ?? []);
              if (!isPartial || (daily?.length ?? 0) > 0) setDailyPageViews(daily ?? []);
              if (!isPartial || (recent?.length ?? 0) > 0) setRecentEvents(recent ?? []);
              if (!isPartial || (tagResult.eventsToday as number | undefined)) setEventsToday((tagResult.eventsToday as number | undefined) ?? 0);
              if (!isPartial || (tagResult.eventsThisWeek as number | undefined)) setEventsThisWeek((tagResult.eventsThisWeek as number | undefined) ?? 0);
              if (!isPartial || (tagResult.activePagesCount as number | undefined)) setActivePagesCount((tagResult.activePagesCount as number | undefined) ?? 0);
              if (!isPartial || (today?.length ?? 0) > 0) setTodayByType(today ?? []);
              if (!isPartial || (outbound?.totalClicks ?? 0) > 0 || (outbound?.topDestinations?.length ?? 0) > 0) {
                setOutboundClicks(outbound ?? { totalClicks: 0, topDestinations: [] });
              }
              const plugins = tagResult.pluginsDetected as Parameters<typeof setPluginsDetected>[0] | undefined;
              if (!isPartial || (plugins?.length ?? 0) > 0) setPluginsDetected(plugins ?? []);
              const funnel = tagResult.formFunnel as Parameters<typeof setFormFunnel>[0] | undefined;
              if (!isPartial || funnel) setFormFunnel(funnel ?? undefined);
              const tiers = tagResult.conversionTiers as ConversionTiers | null | undefined;
              if (!isPartial || tiers) setConversionTiers(tiers ?? null);
              if (tagResult.sitePlatform !== undefined) setSitePlatform(tagResult.sitePlatform as typeof sitePlatform ?? null);
            } else {
              setHasTag(false);
              setTrafficData(null);
              setConvData(null);
              setTrendsData([]);
              setDailyPageViews([]);
              setRecentEvents([]);
              setEventsToday(0);
              setEventsThisWeek(0);
              setActivePagesCount(0);
              setTodayByType([]);
              setOutboundClicks({ totalClicks: 0, topDestinations: [] });
              setPluginsDetected([]);
              setFormFunnel(undefined);
              setConversionTiers(null);
            }
          }
        } else if (apiError) {
          // JSON parse failed — render the shell but PRESERVE prior
          // state so we don't visibly regress a populated dashboard to
          // all zeros just because one bad body came back. If the
          // first-ever fetch parsed badly, state is still in its
          // initial zero/null form anyway, so this is a no-op there
          // but protects every subsequent poll.
          tagInstalled = true;
          setHasTag(true);
        }
      } else {
        // Non-ok HTTP response (400, 401, 403, 404, 500, network error).
        // Show dashboard shell with an error banner — but preserve any
        // previously-loaded state so a transient 400 (e.g. brand.id
        // empty for one tick) doesn't wipe the visible dashboard back
        // to zeros. The next successful poll will overwrite.
        const status = tagRes.status === "fulfilled" ? tagRes.value.status : -1;
        if (tagRes.status === "rejected") {
          apiError = "Network error loading analytics data";
        } else if (status === 400) {
          // Most common cause is a transient empty brandId mid-navigation.
          // The LIVE poll fires again in ≤5s and should recover on its own.
          apiError = "Request rejected by server — retrying…";
        } else if (status === 401) {
          apiError = "Session expired — please sign in again to view live analytics.";
        } else if (status === 403) {
          apiError = "You don't have access to this brand. Contact your workspace owner.";
        } else if (status === 404) {
          apiError = "Brand not found. It may have been deleted or moved.";
        } else {
          apiError = `Server error (HTTP ${status >= 0 ? status : "?"})`;
        }
        // Debounce the error banner by 500ms so a successful auto-retry
        // can clear this timer before the banner ever appears. Permanent
        // errors (401, 403, 404) will still show after the brief delay.
        errorBannerTimerRef.current = setTimeout(() => setTagApiError(apiError), 500);
        setTagPartial(false);
        // Still render the dashboard shell — a 400/500 is NOT "tag not
        // installed". Do NOT clobber tagStatus/trafficData/convData/etc
        // to null/zero here; a previous poll's data is better than a
        // flash of zeros while the next poll recovers.
        tagInstalled = true;
        setHasTag(true);
      }

      // Determine page status
      if (!tagInstalled) {
        // Only reached when the API explicitly says installed: false
        setStatus("not_connected");
      } else if (apiError) {
        setStatus("ready"); // dashboard with error banner
      } else if (tagMeta && tagMeta.totalEvents > 0 && tagMeta.totalEvents < 10) {
        // Tag installed with very few events → dashboard + info banner
        setStatus("partial");
      } else {
        setStatus("ready");
      }
      initialLoadedRef.current = true;
      // Always stamp lastUpdatedAt on a completed poll — even partial
      // responses count. The "Last updated: Xs ago" label is a
      // freshness indicator for the attempt, not for the completeness
      // of the payload; the tagPartial flag + "Still loading" banner
      // separately communicate when a response came back short.
      setLastUpdatedAt(Date.now());
      // Mirror the poll tick log so the pair shows up adjacent in the
      // console: [POLL] tick ... fetching → [POLL] received ...
      // Lets the user eyeball that every tick actually produced a
      // response and see the eventsToday number update live.
      console.info(
        "[POLL] received data, events today:",
        loggedEventsToday ?? "n/a",
        "| partial:",
        loggedPartial,
        "| lastEventAt:",
        tagMeta?.lastEventAt ?? "—",
      );
    } catch (err) {
      // Outer-catch is the defensive last resort. Most failure modes are
      // already isolated above (per-fetch .json() try/catch, .allSettled
      // around the network calls), so reaching this branch means
      // something genuinely unexpected threw. Log the full error so the
      // browser console shows the cause — previously this just set
      // "Network error" with no context.
      if (!isAbortError(err)) console.error("[SiteTagAnalytics] outer catch fired", err);
      if (err instanceof DOMException && err.name === "AbortError") {
        // Client-timeout abort. The next poll picks up the cached
        // response so we don't loop on a single bad request.
        if (!initialLoadedRef.current) {
          setStatus("error");
          setErrorMsg("Loading is taking longer than expected. The server is still working — try refreshing in a moment.");
        }
      } else if (!initialLoadedRef.current) {
        setStatus("error");
        setErrorMsg(err instanceof Error ? err.message : "Network error");
      }
    } finally {
      clearTimeout(timeoutId);
      loadingRef.current = false;
      setRefreshing(false);
    }
  }, [brand.id, range]);

  useEffect(() => { loadData(); }, [loadData]);

  // Manual refresh from the top-right button — loadData manages setRefreshing.
  // Also resets any polling stop conditions so both loops restart cleanly.
  function handleRefresh() {
    setPollingStopped(false);
    setPollingRestartKey((k) => k + 1);
    loadData();
  }

  // Live polling — split into two loops to avoid exhausting the Neon
  // connection pool. Previous single-loop design at 5s hit the heavy
  // /api/site-tag/analytics bundle (8+ grouping queries) 12 times per
  // minute per open tab, which blew through the default
  // connection_limit=5 and surfaced as:
  //   "Timed out fetching a new connection from the connection pool"
  //
  // New architecture:
  //   • /api/site-tag/analytics/pulse every 5s
  //       Two indexed queries (lastEventAt + eventsToday count).
  //       <10ms typical, one connection held for <1ms. Updates the
  //       "Last Event" / "Events Today" / "Last Updated" displays so
  //       the LIVE badge stays honest.
  //   • /api/site-tag/analytics (full) every 30s
  //       The heavy bundle. Refreshes charts, tables, top pages,
  //       attribution. 30s matches the server's max-age for non-live
  //       requests so cache pressure stays reasonable.
  //
  // Both loops:
  //   • Gate on brand.id and document.visibilityState="visible"
  //   • Guard with an in-flight ref so a slow response never stacks
  //     a second concurrent request (each concurrent request = one
  //     more held connection)
  //   • Back off on HTTP 503 { reason: "pool_pressure" } by doubling
  //     their own interval up to a ceiling — prevents pool-exhaustion
  //     from cascading across dozens of open tabs
  useEffect(() => {
    if (!brand.id) return;
    let cancelled = false;
    // Exponential-ish backoff state. Each loop owns its own current
    // interval so a pulse-side pool hit doesn't throttle the heavy
    // loop and vice versa. Reset to baseline on the first success.
    let pulseInterval = 5_000;
    let heavyInterval = 30_000;
    const PULSE_MAX = 60_000;
    const HEAVY_MAX = 120_000;
    let pulseTimer: ReturnType<typeof setTimeout> | null = null;
    let heavyTimer: ReturnType<typeof setTimeout> | null = null;
    let pulseFailCount = 0;
    let stopPulse = false;
    let heavyFailCount = 0;
    let stopHeavy = false;

    async function pulse() {
      if (cancelled) return;
      if (typeof document !== "undefined" && document.visibilityState !== "visible") {
        pulseTimer = setTimeout(pulse, pulseInterval);
        return;
      }
      if (pulseInFlightRef.current) {
        pulseTimer = setTimeout(pulse, pulseInterval);
        return;
      }
      pulseInFlightRef.current = true;
      const t0 = Date.now();
      try {
        console.info("[POLL pulse] tick at", new Date().toLocaleTimeString(), "fetching...");
        const res = await fetch(`/api/site-tag/analytics/pulse?brandId=${brand.id}&_t=${Date.now()}`, {
          cache: "no-store",
        });
        if (res.ok) {
          const body = await res.json().catch(() => null) as { installed?: boolean; lastEventAt?: string | null; eventsToday?: number } | null;
          if (body && !cancelled) {
            pulseInterval = 5_000;
            pulseFailCount = 0;
            // Merge-update the tagStatus object so the "Last Event"
            // relative timestamp reflects the latest timestamp from
            // the DB. Keeps the heavy poll's richer fields (status,
            // totalEvents) intact.
            setTagStatus((prev) => prev
              ? { ...prev, lastEventAt: body.lastEventAt ?? prev.lastEventAt }
              : { installed: !!body.installed, status: null, totalEvents: 0, lastEventAt: body.lastEventAt ?? null }
            );
            if (typeof body.eventsToday === "number") setEventsToday(body.eventsToday);
            setLastUpdatedAt(Date.now());
            console.info("[POLL pulse] received eventsToday:", body.eventsToday, "lastEventAt:", body.lastEventAt);
          }
        } else if (res.status >= 500 || res.status === 429) {
          // Any 5xx (pool pressure, db timeout, internal error) or rate limit:
          // back off exponentially so a slow DB period doesn't pile up requests.
          pulseInterval = Math.min(pulseInterval * 2, PULSE_MAX);
          pulseFailCount++;
          console.warn(`[POLL pulse] server error ${res.status}, backing off to ${pulseInterval}ms (failure #${pulseFailCount})`);
          if (pulseFailCount >= 5) {
            stopPulse = true;
            console.warn("[POLL pulse] 5 consecutive failures, stopping pulse loop");
            setPollingStopped(true);
          }
        } else {
          pulseFailCount++;
          console.warn(`[POLL pulse] error ${res.status}, failure #${pulseFailCount}`);
          if (pulseFailCount >= 5) {
            stopPulse = true;
            console.warn("[POLL pulse] 5 consecutive failures, stopping pulse loop");
            setPollingStopped(true);
          }
        }
      } catch (err) {
        // Network-level failure (timeout, DNS, etc.) -- back off same as 5xx
        pulseInterval = Math.min(pulseInterval * 2, PULSE_MAX);
        pulseFailCount++;
        console.warn(`[POLL pulse] network error, backing off to ${pulseInterval}ms`, err);
        if (pulseFailCount >= 5) {
          stopPulse = true;
          console.warn("[POLL pulse] 5 consecutive failures (network), stopping pulse loop");
          setPollingStopped(true);
        }
      } finally {
        pulseInFlightRef.current = false;
        console.info(`[POLL pulse] ${Date.now() - t0}ms (next in ${stopPulse ? "stopped" : `${pulseInterval}ms`})`);
        if (!cancelled && !stopPulse) pulseTimer = setTimeout(pulse, pulseInterval);
      }
    }

    async function heavy() {
      if (cancelled) return;
      if (typeof document !== "undefined" && document.visibilityState !== "visible") {
        heavyTimer = setTimeout(heavy, heavyInterval);
        return;
      }
      if (loadingRef.current) {
        // loadData is already running (either from mount, manual
        // refresh, or an overlapping heavy tick). Skip this tick;
        // loadData clears its own lock in finally.
        heavyTimer = setTimeout(heavy, heavyInterval);
        return;
      }
      console.info("[POLL heavy] tick at", new Date().toLocaleTimeString(), "fetching analytics bundle...");
      try {
        await loadData();
        // loadData's error path doesn't throw on HTTP errors (they become
        // tagApiError banners), so we only count thrown errors as failures.
        heavyInterval = 30_000;
        heavyFailCount = 0;
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        heavyFailCount++;
        if (/connection pool|pool_timeout|timed out fetching|P1017|P2024|idle.in.transaction|Server has closed the connection/i.test(msg)) {
          heavyInterval = Math.min(heavyInterval * 2, HEAVY_MAX);
          console.warn("[POLL heavy] connection pool pressure, backing off to", heavyInterval, "ms");
        } else {
          console.warn(`[POLL heavy] failure #${heavyFailCount}`, msg);
        }
        if (heavyFailCount >= 3) {
          stopHeavy = true;
          console.warn("[POLL heavy] 3 consecutive failures, stopping heavy loop");
          setPollingStopped(true);
        }
      } finally {
        if (!cancelled && !stopHeavy) heavyTimer = setTimeout(heavy, heavyInterval);
      }
    }

    function onVisibilityChange() {
      setIsLive(document.visibilityState === "visible");
      if (document.visibilityState === "visible") {
        // Fire an immediate catch-up on return, rather than waiting
        // for the next scheduled tick. Both loops self-schedule on
        // completion so this won't double-fire.
        if (pulseTimer) { clearTimeout(pulseTimer); pulseTimer = null; }
        pulse();
      }
    }

    // Kick off both loops. Pulse fires immediately so the first LIVE
    // render gets fresh lastEventAt; heavy waits for its full interval
    // because the initial loadData() mount effect has already run.
    pulse();
    heavyTimer = setTimeout(heavy, heavyInterval);
    document.addEventListener("visibilitychange", onVisibilityChange);
    return () => {
      cancelled = true;
      if (pulseTimer) clearTimeout(pulseTimer);
      if (heavyTimer) clearTimeout(heavyTimer);
      document.removeEventListener("visibilitychange", onVisibilityChange);
    };
  }, [brand.id, loadData, pollingRestartKey]);

  // ── Health alert poll (every 30 min) ──
  useEffect(() => {
    if (!brand.id) return;
    let cancelled = false;
    const check = async () => {
      try {
        const res = await fetch(`/api/brands/${brand.id}/health-alert`);
        if (!cancelled && res.ok) {
          const json = await res.json() as { alert: typeof healthAlert };
          setHealthAlert(json.alert ?? null);
        }
      } catch { /* silently ignore */ }
    };
    check();
    const t = setInterval(check, 30 * 60 * 1000);
    return () => { cancelled = true; clearInterval(t); };
  }, [brand.id]);

  // ── No tag installed, no GA4 ──
  if (status === "not_connected") {
    return <SetupPage />;
  }

  // ── Error ──
  if (status === "error") {
    return (
      <div className="space-y-6">
        <PageHeader brand={brand} range={range} setRange={setRange} />
        <div className="rounded-xl border border-red-200 bg-red-50 p-8 text-center">
          <AlertCircle className="w-12 h-12 text-red-300 mx-auto mb-3" />
          <h2 className="text-lg font-bold text-slate-900 mb-1">Failed to Load</h2>
          <p className="text-sm text-slate-500 max-w-md mx-auto mb-4">{errorMsg || "Could not fetch analytics data."}</p>
          <button onClick={loadData} className="px-5 py-2.5 bg-brand-600 text-white text-sm font-semibold rounded-xl hover:bg-brand-700 transition-colors">
            Retry
          </button>
        </div>
      </div>
    );
  }

  // ── Loading ──
  if (status === "loading") {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="w-8 h-8 border-4 border-brand-100 border-t-brand-600 rounded-full animate-spin" />
      </div>
    );
  }

  // ── Ready / Partial ──
  return (
    <div className="space-y-6">
      {/* Ingest degraded banner — shown when the health monitor has
          flagged this brand as "down" for 2+ consecutive hours */}
      {healthAlert && (
        <div className="flex items-start gap-3 rounded-xl border border-red-300 bg-red-50 px-4 py-3.5">
          <AlertTriangle className="w-4 h-4 text-red-600 mt-0.5 shrink-0" />
          <div className="flex-1 min-w-0">
            <p className="text-sm font-bold text-red-900 leading-snug">
              ⚠️ Ingest issue detected — events have dropped {Math.round(healthAlert.dropPct * 100)}% from baseline
            </p>
            <p className="text-xs text-red-700 mt-0.5">
              Expected ~{Math.round(healthAlert.expectedRate).toLocaleString()} events/hr · received {Math.round(healthAlert.actualRate).toLocaleString()} ·
              sustained for {healthAlert.consecutiveHoursDegraded}+ hour{healthAlert.consecutiveHoursDegraded !== 1 ? "s" : ""} ·
              started {(() => {
                const h = Math.floor((Date.now() - new Date(healthAlert.firstDetectedAt).getTime()) / 3_600_000);
                return h < 1 ? "less than 1h" : `${h}h`;
              })()} ago.
              {" "}Check that the Site Tag is still installed on your site.
            </p>
          </div>
        </div>
      )}

      <PageHeader
        brand={brand}
        range={range}
        setRange={setRange}
        isLive={isLive}
        refreshing={refreshing}
        onRefresh={handleRefresh}
        lastUpdatedAt={lastUpdatedAt}
      />

      {/* API error banner. Session-expired gets a sign-in CTA instead
          of a Retry button — retrying with a dead session will just
          loop the same 401. Other errors keep the Retry path. */}
      {tagApiError && (() => {
        const isSessionExpired = tagApiError.startsWith("Session expired");
        return (
          <div className="flex items-start gap-2.5 rounded-xl border border-red-200 bg-red-50 px-4 py-3">
            <AlertCircle className="w-4 h-4 text-red-600 mt-0.5 shrink-0" />
            <div className="flex-1">
              <p className="text-sm font-semibold text-red-900 leading-relaxed">
                {isSessionExpired ? "Session expired" : "Error loading analytics data"}
              </p>
              <p className="text-xs text-red-700 mt-0.5">
                {isSessionExpired ? (
                  <>
                    {tagApiError}{" "}
                    <a href="/sign-in" className="underline font-semibold">Sign in</a>
                  </>
                ) : (
                  <>
                    {tagApiError}. Showing cached/empty data.{" "}
                    <button onClick={loadData} className="underline font-semibold">Retry</button>
                  </>
                )}
              </p>
            </div>
          </div>
        );
      })()}

      {/* Partial install banner */}
      {status === "partial" && !tagApiError && (
        <div className="flex items-start gap-2.5 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3">
          <Info className="w-4 h-4 text-blue-600 mt-0.5 shrink-0" />
          <p className="text-sm text-blue-900 leading-relaxed">
            Site Tag recently installed — data is being collected. Check back in 24 hours for meaningful analytics.
          </p>
        </div>
      )}

      {/* Partial-response banner — server hit its 12s analytics-race
          deadline and returned what it had. Shown as info, not error,
          because previously-loaded sections still display their last
          good values; only the empty cards are actually pending. The
          spinning RefreshCw icon makes "we're still working on it"
          obvious without needing a busy spinner over the whole page. */}
      {tagPartial && !tagApiError && status !== "partial" && (
        <div className="flex items-start gap-2.5 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3">
          <RefreshCw className="w-4 h-4 text-blue-600 mt-0.5 shrink-0 animate-spin" />
          <div className="flex-1">
            <p className="text-sm font-semibold text-blue-900 leading-relaxed">
              Still loading analytics
            </p>
            <p className="text-xs text-blue-800 mt-0.5">
              Some sections are taking longer than usual. Showing the latest available data — the rest will fill in on the next refresh.
            </p>
          </div>
        </div>
      )}

      {pollingStopped && !tagApiError && (
        <div className="flex items-start gap-2.5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
          <AlertCircle className="w-4 h-4 text-amber-600 mt-0.5 shrink-0" />
          <div className="flex-1">
            <p className="text-sm font-semibold text-amber-900 leading-relaxed">Live updates paused</p>
            <p className="text-xs text-amber-800 mt-0.5">
              Analytics could not be refreshed after several attempts. Showing last known data.{" "}
              <button onClick={handleRefresh} className="underline font-semibold">Retry</button>
            </p>
          </div>
        </div>
      )}

      {/* Tab Bar */}
      <div className="flex items-center gap-1 bg-slate-100 rounded-xl p-1 overflow-x-auto">
        {TABS.map(({ key, label, icon: Icon }) => (
          <button
            key={key}
            onClick={() => setTab(key)}
            className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-colors ${
              tab === key
                ? "bg-white text-slate-900 shadow-sm"
                : "text-slate-500 hover:text-slate-700"
            }`}
          >
            <Icon className="w-3.5 h-3.5" />
            {label}
          </button>
        ))}
      </div>

      {/* Tab Content */}
      {tab === "overview" && (
        <>
          <OverviewTab
            ga4={ga4Data}
            traffic={trafficData}
            conversions={convData}
            hasTag={hasTag}
            hasGa4={hasGa4}
            tagStatus={tagStatus}
            dailyPageViews={dailyPageViews}
            recentEvents={recentEvents}
            eventsToday={eventsToday}
            eventsThisWeek={eventsThisWeek}
            activePagesCount={activePagesCount}
            outboundClicks={outboundClicks}
            todayByType={todayByType}
            customEventsCount={customEventsCount}
            reconciliation={reconciliation}
            siteTagAge={siteTagAge}
            siteTagInstalledAt={siteTagInstalledAt}
            siteTagDeployedAt={siteTagDeployedAt}
            range={range}
            pluginsDetected={pluginsDetected}
            formFunnel={formFunnel}
            brandId={brand.id}
            conversionTiers={conversionTiers}
            onOpenConvSettings={() => setConvSettingsOpen(true)}
            isAnalyticsLoading={tagPartial}
            convUpdating={convUpdating}
          />
          {/* External Converters — off-domain conversion tracking, below Top Outbound Destinations */}
          <DetectedIntegrationsPanel brandId={brand.id} sections="external" />
        </>
      )}
      {tab === "conversions" && (
        <ConversionsTab
          ga4={ga4Data}
          tagConversions={convData}
          hasGa4={hasGa4}
          hasTag={hasTag}
          brandId={brand.id}
          onNavigateToEmbedded={() => setTab("embedded-conversions")}
        />
      )}
      {tab === "embedded-conversions" && <EmbeddedConversionsTab brandId={brand.id} />}
      {tab === "integrations" && <IntegrationsTab brandId={brand.id} sitePlatform={sitePlatform} />}
      {tab === "funnel" && (
        <FunnelTab
          traffic={trafficData}
          conversions={convData}
          trends={trendsData}
          hasTag={hasTag}
        />
      )}
      {tab === "attribution" && (
        <AttributionTab
          traffic={trafficData}
          conversions={convData}
          hasTag={hasTag}
        />
      )}
      {tab === "heatmaps" && <HeatmapsTab brandId={brand.id} range={range} />}
      {tab === "ux" && <UxTab brandId={brand.id} range={range} />}
      {tab === "video" && <VideoChatTab brandId={brand.id} range={range} />}
      {tab === "ai" && <AiAttributionTab brandId={brand.id} range={range} />}

      {convSettingsOpen && brand.id && (() => {
        const rows: ConversionTypeRow[] = conversionTiers
          ? Object.entries(conversionTiers.byType).map(([type, d]) => ({
              conversionType: type,
              tier: d.tier,
              isCounted: d.counted,
              displayLabel: d.label,
              isCustom: false,
              defaultTier: getDefaultTier(type),
              defaultLabel: d.label,
              count: d.count,
            }))
          : [];
        return (
          <ConversionSettingsModal
            open={convSettingsOpen}
            onClose={() => setConvSettingsOpen(false)}
            brandId={brand.id}
            rows={rows}
            sessions={trafficData?.sessions ?? 1}
            onSaved={() => {
              setConvSettingsOpen(false);
              setConvUpdating(true);
              void (async () => {
                const controller = new AbortController();
                try {
                  const res = await fetch(
                    `/api/site-tag/analytics?brandId=${brand.id}&range=${range}&live=true&_t=${Date.now()}`,
                    { cache: "no-store", signal: controller.signal },
                  );
                  if (res.ok) {
                    const data = await res.json();
                    if (data.conversionTiers) setConversionTiers(data.conversionTiers as ConversionTiers);
                  }
                } catch { /* best effort */ } finally {
                  setConvUpdating(false);
                }
              })();
            }}
          />
        );
      })()}
    </div>
  );
}

// ─── Shared Components ────────────────────────────────────────────────────────────────────────

function PageHeader(props: {
  brand: { domain: string };
  range: string;
  setRange: (r: "1d" | "7d" | "28d" | "30d" | "90d") => void;
  isLive?: boolean;
  refreshing?: boolean;
  onRefresh?: () => void;
  lastUpdatedAt?: number | null;
}) {
  const lastUpdatedLabel = (() => {
    if (!props.lastUpdatedAt) return null;
    const diff = Date.now() - props.lastUpdatedAt;
    const secs = Math.floor(diff / 1000);
    if (secs < 5) return "Last updated: just now";
    if (secs < 60) return `Last updated: ${secs}s ago`;
    const mins = Math.floor(secs / 60);
    if (mins < 60) return `Last updated: ${mins}m ago`;
    const hours = Math.floor(mins / 60);
    return `Last updated: ${hours}h ago`;
  })();

  return (
    <div className="flex items-start justify-between gap-4">
      <div className="flex items-start gap-3 min-w-0">
        <div>
          <div className="flex items-center gap-2">
            <h1 className="text-2xl font-bold text-slate-900">Site Tag Analytics</h1>
            {props.isLive && (
              <span className="inline-flex items-center gap-1 text-[10px] font-bold text-green-600 bg-green-50 border border-green-200 rounded-full px-2 py-0.5">
                <span className="relative flex h-1.5 w-1.5">
                  <span className="absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75 animate-ping" />
                  <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-500" />
                </span>
                LIVE
              </span>
            )}
          </div>
          <p className="text-slate-500 text-sm mt-1">
            Unified analytics for <span className="font-medium text-slate-700">{props.brand.domain}</span>
            {lastUpdatedLabel && (
              <span className="text-slate-400 ml-2">· {lastUpdatedLabel}</span>
            )}
          </p>
        </div>
      </div>
      <div className="flex items-center gap-2 shrink-0">
        <div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
          {(["1d", "7d", "28d", "30d", "90d"] as const).map((r) => (
            <button key={r} onClick={() => props.setRange(r)}
              disabled={props.refreshing}
              className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors disabled:opacity-50 ${
                props.range === r ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-700"
              }`}
            >
              {r === "1d" ? "Today" : r === "7d" ? "7d" : r === "28d" ? "28d" : r === "30d" ? "30d" : "90d"}
            </button>
          ))}
        </div>
        {props.onRefresh && (
          <button
            onClick={props.onRefresh}
            disabled={props.refreshing}
            className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold text-slate-600 bg-white border border-slate-200 hover:bg-slate-50 disabled:opacity-50 transition-colors"
            title="Refresh all data"
          >
            <RefreshCw className={`w-3.5 h-3.5 ${props.refreshing ? "animate-spin" : ""}`} />
            Refresh
          </button>
        )}
      </div>
    </div>
  );
}
