"use client";

import { CSSProperties, Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
  getSuperadminOrgs,
  getLagoOpsBillingOverview,
  getLagoPaymentFailures,
  retryLagoBillingSync,
  syncLagoCustomer,
  getLagoCustomer,
  getLlmCostOsaCycles,
  OrgCycleWindow,
  OsaCycleCostRow,
  LagoCustomerProfile,
  isAuthenticated,
  listAmOrgs,
  OrgOverview,
  LagoOpsBillingOverview,
  LagoPaymentFailure,
  LagoSubscription,
} from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import TanStackTable from "@/components/ui/TanStackTable";
import type { ColumnDef } from "@tanstack/react-table";
import PageHeader from "@/components/ui/page-header";
import { DateField } from "@/components/ui/date-field";
import Button from "@/components/ui/button";
import Badge from "@/components/ui/badge";
import StatCard from "@/components/ops/StatCard";
import { AlertBanner } from "@/components/ops/AlertBanner";
import { SearchableSelect } from "@/components/ui/SearchableSelect";
import { PageLoadingSkeleton } from "@/components/shared/Skeleton";
import { RefreshCw, ExternalLink, RotateCcw } from "lucide-react";
import { Tooltip } from "@/components/shared/Tooltip";
import { formatNumber } from "@/lib/format-number";
import { formatDate } from "@/lib/format-date";

type BadgeVariant = "default" | "success" | "warning" | "danger" | "info" | "accent";

/**
 * `Figma/super-admin/UI-SPEC.md` §4 puts the two tables behind one heading as
 * tabs, each carrying the count of rows that need attention — not the row
 * total, which for Subscriptions is every subscription on the platform.
 */
type BillingTab = "payments" | "subscriptions";

const BILLING_TABS: { id: BillingTab; label: string }[] = [
  { id: "payments", label: "Payments" },
  { id: "subscriptions", label: "Subscriptions" },
];

/** The strip's captions, which the frame writes under each counter. */
const BILLING_STAT_HINTS = {
  total: "all clients",
  active: "billing normally",
  syncFailed: "need attention",
  paymentFailures: "need triage",
} as const;

const LAGO_STATUS: Record<string, { variant: BadgeVariant; label: string }> = {
  active: { variant: "success", label: "Active" },
  pending: { variant: "info", label: "Pending" },
  sync_failed: { variant: "danger", label: "Sync Failed" },
  canceled: { variant: "default", label: "Canceled" },
};

export default function SuperadminBillingPage() {
  return (
    <Suspense fallback={<PageLoadingSkeleton />}>
      <SuperadminBillingPageInner />
    </Suspense>
  );
}

function SuperadminBillingPageInner() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { platformRole } = useAuth();
  const isSuperadmin = platformRole === "superadmin";
  const isAm = platformRole === "account_manager" || platformRole === "am";
  const selectedOrgId = searchParams.get("org") ?? "";
  const fromDate = searchParams.get("from") ?? "";
  const toDate = searchParams.get("to") ?? "";
  const [orgOptions, setOrgOptions] = useState<OrgOverview[]>([]);
  const [lagoOverview, setLagoOverview] = useState<LagoOpsBillingOverview | null>(null);
  const [paymentFailures, setPaymentFailures] = useState<LagoPaymentFailure[]>([]);
  // #5005: the #1860 Org Usage telemetry + #4823 cost trends moved to the
  // client-scoped analytics view (ClientUsagePanels) — no usage state here.
  const [lagoLoading, setLagoLoading] = useState(true);
  const [paymentFailuresLoading, setPaymentFailuresLoading] = useState(true);
  /* Which client each result set describes. A loading flag cannot answer that:
     it is raised inside the fetching effect, which runs after the render that
     already carries the new `selectedOrgId` — so for one frame the old counts
     sit under the new filter with both flags still false. Stamping the data
     with its scope makes the question a render-time comparison instead. */
  const [overviewOrgId, setOverviewOrgId] = useState<string | null>(null);
  const [failuresOrgId, setFailuresOrgId] = useState<string | null>(null);
  const [retrying, setRetrying] = useState<string | null>(null);
  const [retrySyncError, setRetrySyncError] = useState<string | null>(null);
  const [retryingAll, setRetryingAll] = useState(false);
  const [activeTab, setActiveTab] = useState<BillingTab>("payments");

  /* A retry can outlive the filter that started it, and its refresh must not
     land an old org's billing under the new one. Rather than guard a read fired
     from the handler, mutations bump this and the loader effect re-reads: it
     already runs with the current `selectedOrgId` and already drops a superseded
     response through its own `cancelled` flag. */
  const [reloadNonce, setReloadNonce] = useState(0);
  const reloadBilling = useCallback(() => setReloadNonce((n) => n + 1), []);

  /* True only once the held result set was read for the client now selected.
     A same-client reload keeps its numbers up; a client switch blanks them on
     the very render that switches, with no frame in between. */
  const overviewIsCurrent = overviewOrgId === selectedOrgId;
  const failuresAreCurrent = failuresOrgId === selectedOrgId;

  /* Everything scoped to a client — counters, badges and the rows themselves —
     reads these, so no surface can be left behind on the raw flag alone. */
  const overviewPending = lagoLoading || !overviewIsCurrent;
  const failuresPending = paymentFailuresLoading || !failuresAreCurrent;
  const scopedPaymentFailures = failuresAreCurrent ? paymentFailures : [];
  const scopedSubscriptions = overviewIsCurrent
    ? (lagoOverview?.subscriptions ?? [])
    : [];
  // Per-row in-flight sync state. Keyed by the subscription's unique assignmentId
  // (the table's rowKey) — NOT orgId, which repeats across rows when one org has
  // several per-Specialist subscriptions and would spin every sibling row (#3010).
  // A Set lets independent rows load concurrently, so one row finishing doesn't
  // clear another row's spinner.
  const [syncingKeys, setSyncingKeys] = useState<Set<string>>(new Set());
  const [syncOrgError, setSyncOrgError] = useState<string | null>(null);
  const [modalSyncError, setModalSyncError] = useState<string | null>(null);
  const [customerProfile, setCustomerProfile] = useState<LagoCustomerProfile | null>(null);
  const [viewingCustomerOrgId, setViewingCustomerOrgId] = useState<string | null>(null);
  const [customerLoading, setCustomerLoading] = useState(false);
  const [customerError, setCustomerError] = useState(false);
  // #4797 follow-up: per-OSA current-billing-cycle LLM cost for the roster
  // column (each subscription row IS one OSA). Windows come per org (all
  // OSAs of one org share the anchor). SuperAdmin-only endpoint; empty for AMs.
  const [cycleWindows, setCycleWindows] = useState<Map<string, OrgCycleWindow>>(
    new Map(),
  );
  const [osaCycleCosts, setOsaCycleCosts] = useState<
    Map<string, OsaCycleCostRow>
  >(new Map());

  useEffect(() => {
    if (!isAuthenticated()) {
      router.replace("/login");
      return;
    }
    if (platformRole === null) return;
    if (!isSuperadmin && !isAm) {
      router.replace("/ops/clients");
      return;
    }
    let cancelled = false;
    const orgListPromise = isSuperadmin
      ? getSuperadminOrgs()
      : listAmOrgs().then((rows) =>
          rows.map((org) => ({
            id: org.id,
            name: org.name,
            slug: org.slug,
            plan: "free",
            ticketCount: 0,
            memberCount: 0,
            createdAt: org.createdAt,
            status: org.status,
            trialEndDate: org.trialEndDate,
          })),
        );
    const lagoOverviewPromise = getLagoOpsBillingOverview({
      orgId: selectedOrgId || null,
    });
    const paymentFailuresPromise = getLagoPaymentFailures({
      orgId: selectedOrgId || null,
    });
    orgListPromise
      .then((orgList) => {
        if (cancelled) return;
        setOrgOptions(orgList);
      })
      .catch(() => {
        if (!cancelled) setOrgOptions([]);
      });

    // Mark both panels loading at the start of each fetch (mount + whenever
    // selectedOrgId changes). This is the standard fetch-on-deps pattern other
    // Ops pages use (e.g. ops/clients/[id]); react-hooks/set-state-in-effect
    // flags the synchronous set, but resetting the spinner per fetch is the
    // intended behaviour, so disable it for just these two flags.
    /* eslint-disable react-hooks/set-state-in-effect -- intentional per-fetch loading reset */
    setLagoLoading(true);
    setPaymentFailuresLoading(true);
    /* eslint-enable react-hooks/set-state-in-effect */
    lagoOverviewPromise
      .then((data) => {
        if (!cancelled) setLagoOverview(data);
      })
      .catch(() => {
        if (!cancelled) setLagoOverview(null);
      })
      .finally(() => {
        // Stamped with the scope it was read for, including on failure: an
        // empty result for this client is still this client's answer.
        if (!cancelled) {
          setOverviewOrgId(selectedOrgId);
          setLagoLoading(false);
        }
      });
    paymentFailuresPromise
      .then((data) => {
        if (!cancelled) setPaymentFailures(data.paymentFailures);
      })
      .catch(() => {
        if (!cancelled) setPaymentFailures([]);
      })
      .finally(() => {
        if (!cancelled) {
          setFailuresOrgId(selectedOrgId);
          setPaymentFailuresLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [router, platformRole, isSuperadmin, isAm, selectedOrgId, reloadNonce]);

  // #4797 follow-up: fetch the current-cycle per-OSA LLM cost for every org
  // in the roster once the overview lands (SuperAdmin only — 403s AMs).
  useEffect(() => {
    if (!isSuperadmin || !lagoOverview) {
      setCycleWindows(new Map());
      setOsaCycleCosts(new Map());
      return;
    }
    const orgIds = Array.from(
      new Set(lagoOverview.subscriptions.map((s) => s.orgId).filter(Boolean)),
    );
    if (orgIds.length === 0) {
      setCycleWindows(new Map());
      setOsaCycleCosts(new Map());
      return;
    }
    let cancelled = false;
    getLlmCostOsaCycles(orgIds)
      .then((res) => {
        if (cancelled) return;
        setCycleWindows(new Map(res.windows.map((w) => [w.orgId, w])));
        setOsaCycleCosts(new Map(res.osas.map((o) => [o.osaId, o])));
      })
      .catch((err) => {
        // Degrade to "—" cells, but leave a trace for diagnosis — a silent
        // catch here is how an endpoint failure masquerades as "no data".
        console.warn("[ops/billing] osa-cycles cost fetch failed", err);
        if (cancelled) return;
        setCycleWindows(new Map());
        setOsaCycleCosts(new Map());
      });
    return () => {
      cancelled = true;
    };
  }, [isSuperadmin, lagoOverview]);

  function updateFilter(key: "org" | "from" | "to", value: string) {
    const next = new URLSearchParams(searchParams.toString());
    if (value) next.set(key, value);
    else next.delete(key);
    router.replace(`/ops/billing${next.toString() ? `?${next}` : ""}`);
  }

  // `loadingKey` scopes the spinner to a single UI element: the row's unique
  // assignmentId for table rows, or the orgId for the single-customer modal.
  const handleSyncOrg = useCallback(
    async (orgId: string, loadingKey: string, fromModal = false) => {
      setSyncingKeys((prev) => new Set(prev).add(loadingKey));
      setSyncOrgError(null);
      setModalSyncError(null);
      try {
        await syncLagoCustomer(orgId);
        // If called from modal, refresh the customer profile
        if (fromModal) {
          setCustomerError(false);
          const res = await getLagoCustomer(orgId);
          setCustomerProfile(res.customer);
        }
      } catch {
        if (fromModal) {
          setModalSyncError("Sync failed — Lago may be unavailable. Try again later.");
        } else {
          setSyncOrgError(`Failed to sync org ${orgId} to Lago.`);
        }
      } finally {
        setSyncingKeys((prev) => {
          const next = new Set(prev);
          next.delete(loadingKey);
          return next;
        });
      }
    },
    [],
  );

  const handleViewCustomer = useCallback(async (orgId: string) => {
    setViewingCustomerOrgId(orgId);
    setCustomerProfile(null);
    setCustomerError(false);
    setCustomerLoading(true);
    try {
      const res = await getLagoCustomer(orgId);
      setCustomerProfile(res.customer); // null = not synced yet (backend returned 404)
    } catch {
      setCustomerError(true); // non-404 error — provider outage, network failure, etc.
    } finally {
      setCustomerLoading(false);
    }
  }, []);

  const handleRetrySync = useCallback(async (assignmentId: string) => {
    setRetrying(assignmentId);
    setRetrySyncError(null);
    try {
      await retryLagoBillingSync(assignmentId);
      // Success: mark as pending, decrement counter
      setLagoOverview((prev) =>
        prev
          ? {
              ...prev,
              syncFailedCount: Math.max(0, prev.syncFailedCount - 1),
              subscriptions: prev.subscriptions.map((s) =>
                s.assignmentId === assignmentId
                  ? { ...s, status: "pending" as const }
                  : s,
              ),
            }
          : prev,
      );
    } catch (err) {
      // 400 = subscription not in sync_failed state (already recovered).
      // Refresh so the table reflects actual server state and the button disappears.
      const is400 =
        (err as { status?: number })?.status === 400 ||
        /not in sync_failed/i.test((err as Error)?.message ?? "");
      if (is400) {
        setRetrySyncError(`Subscription already recovered — refreshing table.`);
        reloadBilling();
      } else {
        setRetrySyncError(`Retry failed for assignment ${assignmentId}. Check backend logs.`);
      }
    } finally {
      setRetrying(null);
    }
  }, [reloadBilling]);

  /**
   * The frames' `Retry all failed`. There is no bulk endpoint — only
   * `/ops/billing/subscriptions/{id}/retry-sync` per row — so this fans out
   * over the failed rows rather than the button promising a call that does not
   * exist. Partial failure is reported as a count, because retrying eight rows
   * and silently surfacing one error reads as "all eight worked".
   */
  const handleRetryAllFailed = useCallback(async () => {
    const failed = (lagoOverview?.subscriptions ?? []).filter(
      (sub) => sub.status === "sync_failed",
    );
    if (failed.length === 0) return;

    setRetryingAll(true);
    setRetrySyncError(null);
    const results = await Promise.allSettled(
      failed.map((sub) => retryLagoBillingSync(sub.assignmentId)),
    );
    const rejected = results.filter((r) => r.status === "rejected").length;
    if (rejected > 0) {
      setRetrySyncError(
        `${rejected} of ${failed.length} retries failed. Check backend logs, or retry the affected rows individually.`,
      );
    }
    // Re-read rather than patch: a partial run leaves rows in three different
    // states and the server is the only thing that knows which is which.
    reloadBilling();
    setRetryingAll(false);
  }, [lagoOverview, reloadBilling]);

  const paymentFailureColumns = useMemo<ColumnDef<LagoPaymentFailure, unknown>[]>(() => [
    {
      id: "orgName",
      header: "Org",
      enableSorting: true,
      accessorFn: (f) => f.orgName,
      cell: ({ row: { original: failure } }) => (
        <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
          <span style={{ fontSize: 14, color: "var(--text-primary)", fontWeight: 600 }}>
            {failure.orgName}
          </span>
          <span style={{ fontSize: 12, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
            {failure.orgId}
          </span>
        </div>
      ),
    },
    {
      id: "accountManagerName",
      header: "AM",
      enableSorting: true,
      accessorFn: (f) => f.accountManagerName ?? "",
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 14, color: "var(--text-secondary)" }}>
          {failure.accountManagerName ?? "Unassigned"}
        </span>
      ),
    },
    {
      id: "externalInvoiceId",
      header: "Invoice",
      enableSorting: false,
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 12, color: "var(--text-primary)", fontFamily: "var(--font-mono)" }}>
          {failure.externalInvoiceId}
        </span>
      ),
    },
    {
      id: "amountCents",
      header: "Amount",
      size: 120,
      enableSorting: true,
      accessorFn: (f) => Number(f.amountCents),
      meta: { align: "right" },
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 14, color: "var(--text-primary)" }}>
          {formatCurrency(failure.amountCents, failure.currency)}
        </span>
      ),
    },
    {
      id: "daysSinceFirstFailure",
      header: "Days Failed",
      size: 110,
      enableSorting: true,
      accessorFn: (f) => f.daysSinceFirstFailure ?? -1,
      meta: { align: "right" },
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 14, color: "var(--text-primary)" }}>
          {failure.daysSinceFirstFailure ?? "—"}
        </span>
      ),
    },
    {
      id: "dunningAttemptCount",
      header: "Attempts",
      size: 95,
      enableSorting: true,
      accessorFn: (f) => f.dunningAttemptCount,
      meta: { align: "right" },
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 14, color: "var(--text-primary)" }}>
          {failure.dunningAttemptCount}
        </span>
      ),
    },
    {
      id: "nextRetryAt",
      header: "Next Retry",
      size: 130,
      enableSorting: true,
      accessorFn: (f) => f.nextRetryAt ?? "9999-12-31", // nulls sort to bottom
      cell: ({ row: { original: failure } }) => (
        <span style={{ fontSize: 14, color: "var(--text-secondary)" }}>
          {failure.nextRetryAt ? formatDate(failure.nextRetryAt) : "—"}
        </span>
      ),
    },
    {
      id: "status",
      header: "Status",
      size: 135,
      enableSorting: false,
      cell: () => (
        <Badge variant="danger" size="sm">
          Payment Failed
        </Badge>
      ),
    },
  ], []);

  const lagoColumns = useMemo<ColumnDef<LagoSubscription, unknown>[]>(() => [
    {
      id: "orgId",
      header: "Client",
      enableSorting: true,
      accessorFn: (s) => s.orgName ?? s.orgId,
      cell: ({ row: { original: sub } }) => (
        <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
          <span style={{ fontSize: 14, color: "var(--text-primary)", fontWeight: 600 }}>
            {sub.orgName ?? sub.orgId}
          </span>
          <span style={{ fontSize: 12, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
            {sub.orgId}
          </span>
        </div>
      ),
    },
    {
      id: "specialistId",
      header: "Specialist",
      enableSorting: true,
      accessorFn: (s) => s.specialistName ?? s.specialistId,
      cell: ({ row: { original: sub } }) => (
        <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
          <span style={{ fontSize: 14, color: "var(--text-primary)", fontWeight: 600 }}>
            {sub.specialistName ?? sub.specialistId}
          </span>
          <span style={{ fontSize: 12, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
            {sub.specialistId}
          </span>
        </div>
      ),
    },
    {
      id: "rate",
      header: "Monthly Rate",
      size: 130,
      enableSorting: true,
      accessorFn: (s) => Number(s.currentRateCents),
      meta: { align: "right" },
      cell: ({ row: { original: sub } }) => {
        const cents = Number(sub.currentRateCents);
        let label = "—";
        try {
          label = new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: (sub.currency ?? "USD").toUpperCase(),
            minimumFractionDigits: 2,
          }).format(cents / 100);
        } catch {
          label = `${(cents / 100).toFixed(2)} ${sub.currency ?? ""}`;
        }
        return (
          <span style={{ fontSize: 14, color: "var(--text-primary)" }}>
            {label}
          </span>
        );
      },
    },
    // #4797 follow-up: current billing-cycle LLM cost per OSA — the row's
    // assignmentId IS the osa_id; summed over the org's OWN anchor window so
    // it reads against this month's invoice (margin precursor for #3196).
    // OSAs with a known window but no spend show $0.00; "—" only when the
    // window itself is unknown (fetch failed / org missing). SuperAdmin-only.
    ...(isSuperadmin
      ? [
          {
            id: "cycleLlmCost",
            header: "LLM Cost (cycle)",
            size: 140,
            enableSorting: true,
            accessorFn: (s: LagoSubscription) =>
              osaCycleCosts.get(s.assignmentId)?.costMicroUsd ?? 0,
            meta: { align: "right" },
            cell: ({ row: { original: sub } }: { row: { original: LagoSubscription } }) => {
              const win = cycleWindows.get(sub.orgId);
              if (!win) {
                return (
                  <span style={{ fontSize: 14, color: "var(--text-muted)" }}>—</span>
                );
              }
              const cost = osaCycleCosts.get(sub.assignmentId);
              return (
                <Tooltip
                  content={`Billing cycle ${win.cycleStart} → ${win.cycleEnd} · ${formatNumber(cost?.callCount ?? 0)} calls · this Specialist only · analytics, not billed`}
                  position="top"
                  delay={0}
                >
                  <span style={{ fontSize: 14, color: "var(--text-primary)" }}>
                    ${((cost?.costMicroUsd ?? 0) / 1_000_000).toFixed(2)}
                  </span>
                </Tooltip>
              );
            },
          } as ColumnDef<LagoSubscription, unknown>,
        ]
      : []),
    {
      id: "status",
      header: "Status",
      size: 120,
      enableSorting: true,
      // Use effective status for sorting so AM users see consistent ordering
      accessorFn: (s) => (s.status === "sync_failed" && !isSuperadmin) ? "active" : s.status,
      cell: ({ row: { original: sub } }) => {
        // Hide technical sync_failed state from AM users
        const effectiveStatus = (sub.status === "sync_failed" && !isSuperadmin) ? "active" : sub.status;
        const cfg = LAGO_STATUS[effectiveStatus] ?? {
          variant: "default" as BadgeVariant,
          label: effectiveStatus,
        };
        return (
          <Badge variant={cfg.variant} size="sm">
            {cfg.label}
          </Badge>
        );
      },
    },
    {
      id: "actions",
      header: "",
      size: 80,
      enableSorting: false,
      meta: { align: "right" },
      cell: ({ row: { original: sub } }) =>
        isSuperadmin ? (
          <div style={{ display: "flex", gap: 4, justifyContent: "flex-end" }}>
            <Tooltip content="Sync to Lago" position="top" delay={0}>
              <button
                onClick={() => handleSyncOrg(sub.orgId, sub.assignmentId)}
                disabled={syncingKeys.has(sub.assignmentId)}
                aria-label="Sync to Lago"
                style={{
                  display: "inline-flex",
                  alignItems: "center",
                  justifyContent: "center",
                  width: 28,
                  height: 28,
                  borderRadius: 6,
                  border: "1px solid var(--border)",
                  background: "var(--bg-primary)",
                  color: "var(--text-secondary)",
                  cursor: syncingKeys.has(sub.assignmentId) ? "not-allowed" : "pointer",
                  opacity: syncingKeys.has(sub.assignmentId) ? 0.5 : 1,
                  transition: "background 0.1s, color 0.1s",
                }}
                onMouseEnter={(e) => {
                  if (!syncingKeys.has(sub.assignmentId))
                    (e.currentTarget as HTMLButtonElement).style.background = "var(--bg-surface)";
                }}
                onMouseLeave={(e) => {
                  (e.currentTarget as HTMLButtonElement).style.background = "var(--bg-primary)";
                }}
              >
                <RefreshCw
                  size={14}
                  style={{
                    animation: syncingKeys.has(sub.assignmentId) ? "spin 1s linear infinite" : undefined,
                  }}
                />
              </button>
            </Tooltip>
            <Tooltip content="View customer" position="top" delay={0}>
              <button
                onClick={() => handleViewCustomer(sub.orgId)}
                aria-label="View customer"
                style={{
                  display: "inline-flex",
                  alignItems: "center",
                  justifyContent: "center",
                  width: 28,
                  height: 28,
                  borderRadius: 6,
                  border: "1px solid var(--border)",
                  background: "var(--bg-primary)",
                  color: "var(--text-secondary)",
                  cursor: "pointer",
                  transition: "background 0.1s, color 0.1s",
                }}
                onMouseEnter={(e) => {
                  (e.currentTarget as HTMLButtonElement).style.background = "var(--bg-surface)";
                }}
                onMouseLeave={(e) => {
                  (e.currentTarget as HTMLButtonElement).style.background = "var(--bg-primary)";
                }}
              >
                <ExternalLink size={14} />
              </button>
            </Tooltip>
            {sub.status === "sync_failed" && isSuperadmin && (
              <Tooltip content="Retry sync" position="top" delay={0}>
                <button
                  onClick={() => handleRetrySync(sub.assignmentId)}
                  disabled={retrying === sub.assignmentId}
                  aria-label="Retry sync"
                  style={{
                    display: "inline-flex",
                    alignItems: "center",
                    justifyContent: "center",
                    width: 28,
                    height: 28,
                    borderRadius: 6,
                    border: "1px solid var(--warning, #F59E0B)",
                    background: "var(--warning-subtle, rgba(245,158,11,0.08))",
                    color: "var(--warning, #F59E0B)",
                    cursor: retrying === sub.assignmentId ? "not-allowed" : "pointer",
                    opacity: retrying === sub.assignmentId ? 0.5 : 1,
                    transition: "background 0.1s",
                  }}
                  onMouseEnter={(e) => {
                    if (retrying !== sub.assignmentId)
                      (e.currentTarget as HTMLButtonElement).style.background =
                        "rgba(245,158,11,0.16)";
                  }}
                  onMouseLeave={(e) => {
                    (e.currentTarget as HTMLButtonElement).style.background =
                      "var(--warning-subtle, rgba(245,158,11,0.08))";
                  }}
                >
                  <RotateCcw
                    size={14}
                    style={{
                      animation: retrying === sub.assignmentId ? "spin 1s linear infinite" : undefined,
                    }}
                  />
                </button>
              </Tooltip>
            )}
          </div>
        ) : null,
    },
  ], [isSuperadmin, cycleWindows, osaCycleCosts, syncingKeys, retrying, handleSyncOrg, handleViewCustomer, handleRetrySync]);

  return (
    <div>
      <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
      <PageHeader
        title="Billing"
      />

      {/* Sync-failed alert — the frames put it directly under the heading, and
          give it the action rather than telling you to go find the rows. */}
      {isSuperadmin && overviewIsCurrent &&
        lagoOverview &&
        lagoOverview.syncFailedCount > 0 && (
          <div style={{ marginBottom: 20 }}>
            <AlertBanner
              action={
                <Button
                  variant="primary"
                  size="sm"
                  onClick={handleRetryAllFailed}
                  loading={retryingAll}
                  disabled={retryingAll}
                >
                  {retryingAll ? "Retrying…" : "Retry all failed"}
                </Button>
              }
            >
              {lagoOverview.syncFailedCount} subscription
              {lagoOverview.syncFailedCount === 1 ? "" : "s"} failed to sync —
              retry or investigate affected rows below
            </AlertBanner>
          </div>
        )}

      <BillingStatStrip
        overview={lagoOverview}
        loading={overviewPending}
        paymentFailureCount={scopedPaymentFailures.length}
        paymentFailuresLoading={failuresPending}
        isSuperadmin={isSuperadmin}
      />

      <BillingTabs
        active={activeTab}
        onSelect={setActiveTab}
        paymentFailureCount={scopedPaymentFailures.length}
        paymentFailuresLoading={failuresPending}
        syncFailedCount={overviewIsCurrent ? (lagoOverview?.syncFailedCount ?? 0) : 0}
        syncFailedLoading={overviewPending}
      />

      <RollupFilters
        orgs={orgOptions}
        selectedOrgId={selectedOrgId}
        fromDate={fromDate}
        toDate={toDate}
        onChange={updateFilter}
      />

      {retrySyncError && (
        <div
          style={{
            background: "var(--danger-subtle, rgba(239,68,68,0.08))",
            border: "1px solid var(--danger, #EF4444)",
            borderRadius: 8,
            padding: "10px 14px",
            fontSize: 14,
            color: "var(--danger, #EF4444)",
            marginBottom: 16,
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
          }}
        >
          <span>{retrySyncError}</span>
          <button
            onClick={() => setRetrySyncError(null)}
            style={{
              background: "none",
              border: "none",
              color: "inherit",
              cursor: "pointer",
              fontSize: 16,
              lineHeight: 1,
              padding: "0 4px",
            }}
            aria-label="Dismiss error"
          >
            ×
          </button>
        </div>
      )}

      {/* Payment-failure invoices — the frames' `Payments` tab. */}
      {activeTab === "payments" && (
        <div style={{ marginBottom: 32 }}>
          {/* #4848: the normal state here is empty (failures are rare), so the
              zero-state must be recessive — a muted one-liner, not the table's
              big empty card drawing the eye to a non-event. */}
          {!failuresPending && scopedPaymentFailures.length === 0 ? (
            <p style={{ fontSize: 14, color: "var(--text-muted)", margin: 0 }}>
              No payment failures.
            </p>
          ) : (
            <TanStackTable
              columns={paymentFailureColumns}
              data={scopedPaymentFailures}
              rowKey={(failure) => failure.invoiceId}
              loading={failuresPending}
              rowsPerPage={25}
              emptyTitle="No payment failures"
            />
          )}
        </div>
      )}

      {/* Customer profile modal */}
      {viewingCustomerOrgId && (
        <div
          role="presentation"
          style={{
            position: "fixed",
            inset: 0,
            background: "rgba(0,0,0,0.5)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 1000,
          }}
          onClick={() => { setViewingCustomerOrgId(null); setCustomerError(false); setModalSyncError(null); }}
          onKeyDown={(e) => e.key === "Escape" && (setViewingCustomerOrgId(null), setCustomerError(false), setModalSyncError(null))}
        >
          <div
            role="dialog"
            aria-modal="true"
            aria-labelledby="customer-dialog-title"
            onClick={(e) => e.stopPropagation()}
            style={{
              background: "var(--bg-elevated)",
              border: "1px solid var(--border)",
              borderRadius: 12,
              padding: 28,
              maxWidth: 440,
              width: "100%",
            }}
          >
            <h2
              id="customer-dialog-title"
              style={{
                fontSize: 16,
                fontWeight: 600,
                color: "var(--text-primary)",
                margin: "0 0 16px",
              }}
            >
              Lago Customer Profile
            </h2>
            {customerLoading ? (
              <div
                style={{
                  height: 80,
                  background: "var(--border)",
                  borderRadius: 8,
                }}
              />
            ) : customerError ? (
              <p
                style={{
                  fontSize: 14,
                  color: "var(--danger, #EF4444)",
                  margin: "0 0 16px",
                }}
              >
                ⚠️ Could not load customer profile — Lago may be unavailable. Try again later.
              </p>
            ) : !customerProfile ? (
              <p
                style={{
                  fontSize: 14,
                  color: "var(--text-muted)",
                  margin: "0 0 16px",
                }}
              >
                This org has not been synced to Lago yet. Use{" "}
                <strong>Sync to Lago</strong> to create the customer record.
              </p>
            ) : (
              <div
                style={{
                  display: "flex",
                  flexDirection: "column",
                  gap: 10,
                  fontSize: 14,
                  marginBottom: 16,
                }}
              >
                {[
                  ["Name", customerProfile.name],
                  ["Email", customerProfile.email],
                  ["Lago ID", customerProfile.id],
                  ["External ID (org)", customerProfile.externalId],
                  ["Payment Provider", customerProfile.paymentProvider ?? "—"],
                  ["PSP Customer ID", customerProfile.providerCustomerId ?? "—"],
                  [
                    "Created",
                    formatDate(customerProfile.createdAt),
                  ],
                ].map(([label, value]) => (
                  <div
                    key={label}
                    style={{
                      display: "flex",
                      justifyContent: "space-between",
                      gap: 12,
                    }}
                  >
                    <span style={{ color: "var(--text-muted)", flexShrink: 0 }}>
                      {label}
                    </span>
                    <span
                      style={{
                        color: "var(--text-primary)",
                        fontFamily:
                          label === "Lago ID" || label === "External ID (org)" || label === "PSP Customer ID"
                            ? "var(--font-mono)"
                            : undefined,
                        fontSize: label === "Lago ID" || label === "PSP Customer ID" ? 12 : 14,
                        textAlign: "right",
                        wordBreak: "break-all",
                      }}
                    >
                      {value}
                    </span>
                  </div>
                ))}
              </div>
            )}
            {modalSyncError && (
              <div
                style={{
                  background: "var(--danger-subtle, rgba(239,68,68,0.08))",
                  border: "1px solid var(--danger, #EF4444)",
                  borderRadius: 6,
                  padding: "8px 12px",
                  fontSize: 12,
                  color: "var(--danger, #EF4444)",
                  marginBottom: 12,
                }}
              >
                {modalSyncError}
              </div>
            )}
            <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
              {!customerLoading && (
                <Button
                  variant="secondary"
                  size="sm"
                  loading={syncingKeys.has(viewingCustomerOrgId)}
                  onClick={() => handleSyncOrg(viewingCustomerOrgId, viewingCustomerOrgId, true)}
                >
                  {customerProfile ? "Re-sync" : "Sync to Lago"}
                </Button>
              )}
              <Button
                variant="secondary"
                size="md"
                onClick={() => { setViewingCustomerOrgId(null); setCustomerError(false); setModalSyncError(null); }}
              >
                Close
              </Button>
            </div>
          </div>
        </div>
      )}

      {syncOrgError && (
        <div
          style={{
            background: "var(--danger-subtle, rgba(239,68,68,0.08))",
            border: "1px solid var(--danger, #EF4444)",
            borderRadius: 8,
            padding: "10px 14px",
            fontSize: 14,
            color: "var(--danger, #EF4444)",
            marginBottom: 16,
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
          }}
        >
          <span>{syncOrgError}</span>
          <button
            onClick={() => setSyncOrgError(null)}
            style={{
              background: "none",
              border: "none",
              color: "inherit",
              cursor: "pointer",
              fontSize: 16,
              lineHeight: 1,
              padding: "0 4px",
            }}
            aria-label="Dismiss error"
          >
            ×
          </button>
        </div>
      )}

      {/* Per-Specialist subscriptions — the frames' `Subscriptions` tab. The
          counters that used to head this section now lead the page, where §4
          draws them, and cover both tables rather than only this one. */}
      {activeTab === "subscriptions" && (
        <div style={{ marginBottom: 32 }}>
          <TanStackTable
            columns={lagoColumns}
            data={scopedSubscriptions}
            rowKey={(s) => s.assignmentId}
            loading={overviewPending}
            rowsPerPage={25}
            emptyTitle="No Lago subscriptions"
            emptyIcon={<RotateCcw size={28} strokeWidth={1.5} />}
          />
        </div>
      )}

      {/* #4797 platform cost statistics MOVED to /ops/analytics (owner call
        * 2026-07-24); #5005 moved the #4823 org/OSA cost trends and the #1860
        * Org Usage telemetry there too (ClientUsagePanels, rendered when a
        * client is selected). Billing keeps the money-of-record surfaces +
        * the per-client cycle column. */}
    </div>
  );
}

/**
 * §4's heading strip: four counters on the same 4-up grid the DLQ strip uses,
 * each with the caption the frame writes under its number.
 *
 * The spec recorded these as blocked because "the frame draws 596/588/4/3 and
 * never says what they count" — it does, in a 12/400 uppercase label above each
 * one, which a numerals-only reading of the frame missed.
 */
function BillingStatStrip({
  overview,
  loading,
  paymentFailureCount,
  paymentFailuresLoading,
  isSuperadmin,
}: {
  overview: LagoOpsBillingOverview | null;
  loading: boolean;
  paymentFailureCount: number;
  paymentFailuresLoading: boolean;
  isSuperadmin: boolean;
}) {
  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: `repeat(${isSuperadmin ? 4 : 2}, 1fr)`,
        gap: 15,
        marginBottom: 24,
      }}
    >
      <StatCard
        label="Total subscriptions"
        value={loading ? "—" : (overview?.totalSubscriptions ?? 0)}
        hint={BILLING_STAT_HINTS.total}
      />
      <StatCard
        label="Active"
        value={loading ? "—" : (overview?.activeCount ?? 0)}
        hint={BILLING_STAT_HINTS.active}
      />
      {/* No severity bar: §5 draws that on DLQ's `FAILED (DEAD)` alone and §7.5
          files it as a one-off pending a spec. The billing frames draw these
          two plain, and the banner above already carries the urgency. */}
      {isSuperadmin && (
        <>
          <StatCard
            label="Sync failed"
            value={loading ? "—" : (overview?.syncFailedCount ?? 0)}
            hint={BILLING_STAT_HINTS.syncFailed}
          />
          <StatCard
            label="Payment failures"
            value={paymentFailuresLoading ? "—" : paymentFailureCount}
            hint={BILLING_STAT_HINTS.paymentFailures}
          />
        </>
      )}
    </div>
  );
}

/**
 * §4's `Payments` / `Subscriptions` row, each carrying its attention count.
 *
 * Both counts are scoped to the selected client, and the previous client's
 * rows stay in state until the new read lands. A badge left up through that
 * window attributes one org's failures to another, so each drops while its own
 * table is loading rather than showing a number for the wrong scope.
 */
function BillingTabs({
  active,
  onSelect,
  paymentFailureCount,
  paymentFailuresLoading,
  syncFailedCount,
  syncFailedLoading,
}: {
  active: BillingTab;
  onSelect: (tab: BillingTab) => void;
  paymentFailureCount: number;
  paymentFailuresLoading: boolean;
  syncFailedCount: number;
  syncFailedLoading: boolean;
}) {
  const countOf = (tab: BillingTab) => {
    if (tab === "payments") return paymentFailuresLoading ? 0 : paymentFailureCount;
    return syncFailedLoading ? 0 : syncFailedCount;
  };

  return (
    <div
      role="tablist"
      aria-label="Billing tables"
      style={{
        display: "flex",
        gap: 2,
        borderBottom: "1px solid var(--border)",
        marginBottom: 20,
      }}
    >
      {BILLING_TABS.map((tab) => {
        const isActive = active === tab.id;
        const count = countOf(tab.id);
        return (
          <button
            key={tab.id}
            role="tab"
            aria-selected={isActive}
            onClick={() => onSelect(tab.id)}
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              background: "transparent",
              border: "none",
              padding: "8px 12px",
              marginBottom: -1,
              fontSize: 14,
              fontWeight: isActive ? 600 : 400,
              color: isActive ? "var(--text-primary)" : "var(--text-secondary)",
              borderBottom: `2px solid ${isActive ? "var(--brand)" : "transparent"}`,
              cursor: "pointer",
            }}
          >
            {tab.label}
            {count > 0 && (
              <Badge variant="danger" size="sm">
                {count}
              </Badge>
            )}
          </button>
        );
      })}
    </div>
  );
}

function RollupFilters({
  orgs,
  selectedOrgId,
  fromDate,
  toDate,
  onChange,
}: {
  orgs: OrgOverview[];
  selectedOrgId: string;
  fromDate: string;
  toDate: string;
  onChange: (key: "org" | "from" | "to", value: string) => void;
}) {
  return (
    <div
      style={{
        display: "flex",
        flexWrap: "wrap",
        gap: 8,
        alignItems: "center",
        marginBottom: 16,
      }}
    >
      <SearchableSelect
        value={selectedOrgId}
        onChange={(v) => onChange("org", v)}
        options={orgs.map((org) => ({ value: org.id, label: org.name }))}
        placeholder="All clients"
        ariaLabel="Client"
        style={{ width: 200 }}
      />
      <label style={{ display: "flex", alignItems: "center", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--text-muted)" }}>From</span>
        <DateField
          value={fromDate}
          onChange={(v) => onChange("from", v)}
          max={toDate || undefined}
          inputStyle={filterControlStyle}
          aria-label="From date"
        />
      </label>
      <label style={{ display: "flex", alignItems: "center", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--text-muted)" }}>To</span>
        <DateField
          value={toDate}
          onChange={(v) => onChange("to", v)}
          min={fromDate || undefined}
          inputStyle={filterControlStyle}
          aria-label="To date"
        />
      </label>
    </div>
  );
}

const filterControlStyle: CSSProperties = {
  height: 32,
  border: "1px solid var(--border)",
  borderRadius: 6,
  background: "var(--bg-surface)",
  color: "var(--text-secondary)",
  fontSize: 14,
  padding: "0 10px",
};

function formatCurrency(amountCents: string, currency: string): string {
  const cents = Number(amountCents);
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: (currency ?? "USD").toUpperCase(),
      minimumFractionDigits: 2,
    }).format(cents / 100);
  } catch {
    return `${(cents / 100).toFixed(2)} ${currency ?? ""}`;
  }
}

// ============================================================================
