'use client'
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { Plus, Trash2, Check, Sun, Moon, Monitor, type LucideIcon } from 'lucide-react'
import { useTheme } from '@/components/providers/theme-provider'
import { initializePaddle, type Paddle } from '@paddle/paddle-js'
import { cn, formatDate } from '@/lib/utils'
import { Topbar } from '@/components/layout/topbar'
import { Section, FormRow, PrimaryBtn, GhostBtn } from '@/components/ui/primitives'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import {
  useOrganization,
  useUpdateOrganization,
  useUpdateOverageSettings,
  useUpdateSecuritySettings,
  useUpdateBrandingSettings,
} from '@/lib/queries/use-organization'
import {
  useSubscription,
  useCreateCheckout,
  useRefreshSubscription,
  useQuota,
} from '@/lib/queries/use-billing'
import { QuotaBanner } from '@/components/dashboard/quota-banner'
import {
  useAuditLogActions,
  useAuditLogs,
  useAuditLogsPage,
  type UseAuditLogsParams,
} from '@/lib/queries/use-audit-logs'
import { inferAuditSeverity } from '@/lib/audit-logs'
import { AuditLogsTable } from '@/components/audit-logs/AuditLogsTable'
import { useIsAdmin, useCurrentRoleLoading } from '@/lib/queries/use-current-role'
import { useCurrentUser } from '@/lib/queries/use-current-user'
import {
  useIdentities,
  useLinkIdentity,
  useUnlinkIdentity,
  type LinkableProvider,
} from '@/lib/queries/use-identities'
import type { UserIdentity } from '@supabase/supabase-js'
import {
  useMembers,
  useInvitations,
  useInviteMember,
  useUpdateMemberRole,
  useRemoveMember,
  useCancelInvitation,
  useCurrentMember,
  type OrgRole,
} from '@/lib/queries/use-members'
import { PLANS, PLAN_REQUEST_LIMITS, PLAN_SEAT_LIMITS, PLAN_RETENTION_DAYS, PLAN_WORKSPACE_LIMITS, formatPlanLabel } from '@/lib/billing-plans'
import type { BillingPlan } from '@/lib/queries/types'
import {
  useWebhooks,
  useCreateWebhook,
  useUpdateWebhook,
  useDeleteWebhook,
  useTestWebhook,
  useWebhookDeliveries,
} from '@/lib/queries/use-webhooks'
import type { WebhookEvent, WebhookRow } from '@/lib/queries/types'
import { useNotificationChannels, useDeleteChannel } from '@/lib/queries/use-alerts'
import {
  useNotificationPrefs,
  useUpdateNotificationPrefs,
} from '@/lib/queries/use-notification-prefs'
import type { ChannelKind, NotificationChannelRow } from '@/lib/queries/types'
import { AddChannelDialog } from '@/components/channels/add-channel-dialog'
import { PermissionGate } from '@/components/permission-gate'
import { CronJobsPanel } from './cron-jobs-panel'

// ─── types ───────────────────────────────────────────────────────────────────

type TabId =
  | 'general' | 'members' | 'security' | 'audit-log' | 'system'
  | 'billing' | 'plan' | 'invoices'
  | 'profile' | 'auth-methods' | 'notifications' | 'preferences'
  | 'integrations' | 'destinations' | 'webhooks' | 'opentelemetry'

interface NavItem { id: TabId; label: string; crumbs: { label: string }[] }

// ─── nav definition ───────────────────────────────────────────────────────────

// Crumb shape is now `[{ label: 'Settings' }, { label: <Item> }]` across the
// board. The leading "Workspace" / "Account" / "Connect" prefix was
// redundant (the inner nav already shows the group) and inconsistent
// between groups. Normalising everything to "Settings / <Item>" matches
// the breadcrumb pattern used on the rest of the dashboard.
const NAV: { group: string; items: NavItem[] }[] = [
  {
    group: 'Workspace',
    items: [
      { id: 'general',    label: 'General',       crumbs: [{ label: 'Settings' }, { label: 'General' }] },
      { id: 'members',    label: 'Members',       crumbs: [{ label: 'Settings' }, { label: 'Members' }] },
      { id: 'security',   label: 'Security',      crumbs: [{ label: 'Settings' }, { label: 'Security' }] },
      { id: 'audit-log',  label: 'Audit log',     crumbs: [{ label: 'Settings' }, { label: 'Audit log' }] },
      { id: 'system',     label: 'System',        crumbs: [{ label: 'Settings' }, { label: 'System' }] },
    ],
  },
  {
    group: 'Usage',
    items: [
      { id: 'billing',  label: 'Billing',         crumbs: [{ label: 'Settings' }, { label: 'Billing' }] },
      { id: 'plan',     label: 'Plan & limits',   crumbs: [{ label: 'Settings' }, { label: 'Plan & limits' }] },
      { id: 'invoices', label: 'Invoices',        crumbs: [{ label: 'Settings' }, { label: 'Invoices' }] },
    ],
  },
  {
    group: 'Account',
    items: [
      { id: 'profile',       label: 'Profile',         crumbs: [{ label: 'Settings' }, { label: 'Profile' }] },
      { id: 'auth-methods',  label: 'Sign-in methods', crumbs: [{ label: 'Settings' }, { label: 'Sign-in methods' }] },
      { id: 'notifications', label: 'Notifications',   crumbs: [{ label: 'Settings' }, { label: 'Notifications' }] },
      { id: 'preferences',   label: 'Preferences',     crumbs: [{ label: 'Settings' }, { label: 'Preferences' }] },
    ],
  },
  {
    group: 'Connect',
    items: [
      { id: 'integrations',  label: 'Integrations',   crumbs: [{ label: 'Settings' }, { label: 'Integrations' }] },
      // DESTINATIONS_HIDDEN: uncomment when BigQuery/S3/Snowflake connectors are implemented
      // { id: 'destinations',  label: 'Destinations',   crumbs: [{ label: 'Settings' }, { label: 'Destinations' }] },
      { id: 'webhooks',      label: 'Webhooks',       crumbs: [{ label: 'Settings' }, { label: 'Webhooks' }] },
      { id: 'opentelemetry', label: 'OpenTelemetry',  crumbs: [{ label: 'Settings' }, { label: 'OpenTelemetry' }] },
    ],
  },
]

const ALL_ITEMS = NAV.flatMap((g) => g.items)

function NativeInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
  const { className, ...rest } = props
  return (
    <input
      {...rest}
      className={cn(
        'h-9 px-3 rounded-[6px] border border-border bg-bg text-[13px] text-text placeholder:text-text-faint focus:outline-none focus:border-border-strong transition-colors',
        className,
      )}
    />
  )
}

// ─── helpers ─────────────────────────────────────────────────────────────────

function Toggle({ on, disabled, onToggle }: { on: boolean; disabled?: boolean; onToggle?: () => void }) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={on}
      disabled={disabled}
      onClick={onToggle}
      className={cn(
        'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors',
        on ? 'bg-text' : 'bg-border-strong',
        disabled && 'opacity-50 cursor-not-allowed',
        !disabled && 'cursor-pointer',
      )}
    >
      <span
        className={cn(
          'inline-block h-3.5 w-3.5 rounded-full bg-bg transition-transform',
          on ? 'translate-x-[18px]' : 'translate-x-[3px]',
        )}
      />
    </button>
  )
}

function MonoPill({
  children,
  dot,
  variant = 'neutral',
}: {
  children: React.ReactNode
  dot?: boolean
  variant?: 'neutral' | 'accent' | 'good' | 'faint'
}) {
  return (
    <span
      className={cn(
        'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border font-mono text-[10px] uppercase tracking-[0.04em]',
        variant === 'neutral' && 'border-border bg-bg-elev text-text-muted',
        variant === 'accent'  && 'border-accent-border bg-accent-bg text-accent',
        variant === 'good'    && 'border-good/20 bg-good-bg text-good',
        variant === 'faint'   && 'border-border bg-transparent text-text-faint',
      )}
    >
      {dot && <span className={cn('w-1.5 h-1.5 rounded-full', variant === 'accent' ? 'bg-accent' : variant === 'good' ? 'bg-good' : 'bg-text-faint')} />}
      {children}
    </span>
  )
}

function Hint({ children }: { children: React.ReactNode }) {
  return <span className="font-mono text-[10.5px] text-text-faint tracking-[0.03em]">{children}</span>
}

function TabHeader({ title, description, action }: { title: string; description: string; action?: React.ReactNode }) {
  return (
    <div className="flex items-end justify-between mb-6">
      <div>
        <h1 className="text-[26px] font-medium tracking-[-0.6px] mb-1">{title}</h1>
        <p className="text-[13px] text-text-muted">{description}</p>
      </div>
      {action && <div className="flex items-center gap-2">{action}</div>}
    </div>
  )
}

// ─── GENERAL tab ─────────────────────────────────────────────────────────────

function GeneralTab() {
  const { data: org } = useOrganization()
  const updateOrg = useUpdateOrganization()
  const [name, setName] = useState(org?.name ?? '')
  const currentMember = useCurrentMember()
  const isAdmin = currentMember?.role === 'admin'
  // useSyncExternalStore returns false on the server and true on the client
  // without needing useEffect + setState (avoids react-hooks/set-state-in-effect).
  const mounted = useSyncExternalStore(
    (_cb) => () => {},
    () => true,
    () => false,
  )

  const plan = mounted ? org?.plan : undefined
  const retention = plan === 'team' ? '90 days'
    : plan === 'starter' ? '30 days'
    : plan === 'enterprise' ? '1 year'
    : '7 days'
  const timezone = mounted ? Intl.DateTimeFormat().resolvedOptions().timeZone : '—'

  return (
    <div className="max-w-[920px]">
      <TabHeader
        title="General"
        description="Workspace identity, storage region, and retention."
      />

      <Section title="Identity" description="Visible within your workspace" className="mb-5">
        <FormRow label="Workspace name" hint="Shown in the app header and on shared traces.">
          <div className="flex items-center gap-3 w-full max-w-[460px]">
            <NativeInput
              value={name || (org?.name ?? '')}
              onChange={(e) => setName(e.target.value)}
              className="flex-1 font-mono text-[12.5px]"
              disabled={!isAdmin}
            />
            {isAdmin && (
              <GhostBtn
                disabled={updateOrg.isPending || !name.trim() || name === org?.name}
                onClick={() => org && void updateOrg.mutateAsync({ id: org.id, name })}
              >
                {updateOrg.isPending ? 'Saving…' : 'Save'}
              </GhostBtn>
            )}
          </div>
        </FormRow>
        <FormRow label="Plan">
          <MonoPill variant={plan === 'enterprise' ? 'good' : 'accent'} dot>
            {plan ?? '—'}
          </MonoPill>
        </FormRow>
      </Section>

      {isAdmin && <BrandingSection plan={plan ?? null} hideBadge={org?.hide_powered_by_badge ?? false} />}
      {org && <EmbedBadgeSection orgId={org.id} />}

      <Section title="Data retention" description="Log retention is determined by your plan" className="mb-5">
        <FormRow label="Current retention">
          <div className="font-mono text-[12.5px] text-text-muted">
            {retention}
            <span className="ml-2 text-text-faint">· {plan ?? 'free'} plan</span>
          </div>
        </FormRow>
        <FormRow label="Timestamps" hint="All timestamps in the UI use your browser's local timezone.">
          <div className="font-mono text-[12.5px] text-text-muted">
            {timezone}
          </div>
        </FormRow>
      </Section>

      <Section title="Delete workspace" description="Contact support to delete your workspace" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed">
          Workspace deletion requires verification and isn&apos;t available in the self-service UI yet.
          Email <span className="font-mono text-text">support@spanlens.io</span> from the owner address
          and we&apos;ll purge data and cancel billing within one business day.
        </div>
      </Section>
    </div>
  )
}

// ─── Branding (share footer) — PLG Loop ② ────────────────────────────────────

function BrandingSection({ plan, hideBadge }: { plan: string | null; hideBadge: boolean }) {
  const update = useUpdateBrandingSettings()
  const canHide = plan === 'team' || plan === 'enterprise'
  const [error, setError] = useState<string | null>(null)

  async function handleToggle() {
    setError(null)
    try {
      await update.mutateAsync({ hide_powered_by_badge: !hideBadge })
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Update failed')
    }
  }

  return (
    <Section
      title="Share branding"
      description='Public share pages show an "Observed by Spanlens" footer.'
      className="mb-5"
    >
      <FormRow
        label="Hide Spanlens footer"
        hint={
          canHide
            ? 'Removes the footer from /share/<token> pages your workspace creates.'
            : 'Available on the Team plan. The footer is part of how we keep Free and Starter sustainable.'
        }
      >
        <div className="flex items-center gap-3">
          <Toggle
            on={canHide && hideBadge}
            disabled={!canHide || update.isPending}
            onToggle={handleToggle}
          />
          {!canHide && (
            <Link
              href="/settings?tab=plan"
              className="font-mono text-[11.5px] text-accent hover:opacity-80"
            >
              Upgrade →
            </Link>
          )}
        </div>
      </FormRow>
      {error && (
        <div className="px-6 pb-4 -mt-2 font-mono text-[11.5px] text-status-error">
          {error}
        </div>
      )}
    </Section>
  )
}

// ─── Embed badge (README SVG) — PLG Loop ③ ───────────────────────────────────

function EmbedBadgeSection({ orgId }: { orgId: string }) {
  // Snippet always points at the canonical production domain so users don't
  // accidentally paste a localhost URL into their README during local dev.
  const snippetUrl = `https://www.spanlens.io/badge/${orgId}.svg`
  const markdown = `[![Observed by Spanlens](${snippetUrl})](https://www.spanlens.io)`
  const [copied, setCopied] = useState(false)

  async function copy() {
    try {
      await navigator.clipboard.writeText(markdown)
      setCopied(true)
      setTimeout(() => setCopied(false), 1500)
    } catch {
      // Older browsers / non-secure contexts — fall through silently. User can
      // still select-all manually.
    }
  }

  return (
    <Section
      title="README badge"
      description="Show off Spanlens observability on your repo. Paste this snippet into your project's README."
      className="mb-5"
    >
      <FormRow label="Markdown" hint="Renders as the badge above on GitHub, GitLab, and npm.">
        <div className="flex flex-col gap-2 w-full max-w-[640px]">
          <div className="flex items-center gap-3">
            {/* Preview uses same-origin so devs see the actual rendered SVG
                even before the change ships to production. Plain <img> is
                deliberate — next/image is overkill for a 148x20 static SVG
                served straight from the server with long cache headers. */}
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={`/badge/${orgId}.svg`}
              alt="Observed by Spanlens"
              width={148}
              height={20}
              className="h-5"
            />
          </div>
          <textarea
            readOnly
            value={markdown}
            onFocus={(e) => e.currentTarget.select()}
            className="w-full px-3 py-2 border border-border rounded-md bg-bg-elevated font-mono text-[11.5px] resize-none"
            rows={2}
          />
          <div className="flex">
            <GhostBtn onClick={copy}>
              {copied ? 'Copied' : 'Copy markdown'}
            </GhostBtn>
          </div>
        </div>
      </FormRow>
    </Section>
  )
}

// ─── MEMBERS tab ─────────────────────────────────────────────────────────────

function MembersTab() {
  const members = useMembers()
  const invitations = useInvitations()
  const currentMember = useCurrentMember()
  const inviteMutation = useInviteMember()
  const updateRoleMutation = useUpdateMemberRole()
  const removeMutation = useRemoveMember()
  const cancelInvitation = useCancelInvitation()

  const [inviteOpen, setInviteOpen] = useState(false)
  const [inviteEmail, setInviteEmail] = useState('')
  const [inviteRole, setInviteRole] = useState<OrgRole>('editor')
  // Capture "now" once at mount — drives the invitations expiry countdown.
  const [mountNow] = useState(() => Date.now())
  // Hydration-safe gate so the "expires in Xd" cell doesn't flicker between
  // SSR (no clock) and the first client paint.
  const mounted = useSyncExternalStore(
    (_cb) => () => {},
    () => true,
    () => false,
  )
  const [inviteError, setInviteError] = useState('')
  const [inviteSuccess, setInviteSuccess] = useState<string | null>(null)
  const [rowError, setRowError] = useState<string | null>(null)

  const isAdmin = currentMember?.role === 'admin'
  const adminCount = (members.data ?? []).filter((m) => m.role === 'admin').length
  const isLastAdmin = (role: OrgRole) => role === 'admin' && adminCount <= 1

  async function submitInvite(e: React.FormEvent) {
    e.preventDefault()
    setInviteError('')
    setInviteSuccess(null)
    try {
      const result = await inviteMutation.mutateAsync({ email: inviteEmail.trim(), role: inviteRole })
      if (result.devAcceptUrl) {
        setInviteSuccess(`Dev: ${result.devAcceptUrl}`)
      } else {
        setInviteSuccess(`Invitation sent to ${inviteEmail.trim()}`)
      }
      setInviteEmail('')
    } catch (err) {
      setInviteError(err instanceof Error ? err.message : 'Failed to invite')
    }
  }

  async function handleRoleChange(userId: string, newRole: OrgRole) {
    setRowError(null)
    try {
      await updateRoleMutation.mutateAsync({ userId, role: newRole })
    } catch (err) {
      setRowError(err instanceof Error ? err.message : 'Failed to update role')
    }
  }

  async function handleRemove(userId: string) {
    if (!confirm('Remove this member from the workspace?')) return
    setRowError(null)
    try {
      await removeMutation.mutateAsync(userId)
    } catch (err) {
      setRowError(err instanceof Error ? err.message : 'Failed to remove')
    }
  }

  async function handleCancel(id: string) {
    if (!confirm('Cancel this pending invitation?')) return
    await cancelInvitation.mutateAsync(id)
  }

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Members"
        description="Team members with access to this workspace."
        action={
          isAdmin ? (
            <PrimaryBtn onClick={() => { setInviteOpen(true); setInviteError(''); setInviteSuccess(null) }}>
              <Plus className="w-3.5 h-3.5" /> Invite member
            </PrimaryBtn>
          ) : null
        }
      />

      {rowError && (
        <div className="mb-3 border border-bad/30 bg-bad-bg rounded-lg px-4 py-2.5 text-[12.5px] text-bad">
          {rowError}
        </div>
      )}

      <Section title="Members" className="mb-5">
        {/* Gate on `mounted` so SSR + first paint pick the same branch.
            Without this, SSR renders "No members yet" (no query data) and
            client renders the loaded list, triggering React #418. */}
        {!mounted || members.isLoading ? (
          <div className="px-6 py-4 text-[12.5px] text-text-faint">Loading…</div>
        ) : (members.data ?? []).length === 0 ? (
          <div className="px-6 py-4 text-[12.5px] text-text-faint">No members yet.</div>
        ) : (
          <div className="overflow-x-auto">
          <div className="divide-y divide-border min-w-[520px]">
            {(members.data ?? []).map((m) => {
              const isMe = currentMember?.userId === m.userId
              const lockedLastAdmin = isLastAdmin(m.role)
              return (
                <div
                  key={m.userId}
                  className="grid grid-cols-[1.6fr_1fr_130px_100px] gap-4 px-6 py-3 items-center"
                >
                  <span className="text-[13px] font-medium text-text truncate">
                    {m.email} {isMe && <span className="text-text-faint font-normal">(you)</span>}
                  </span>
                  <span className="font-mono text-[11px] text-text-muted truncate">
                    joined {formatDate(m.createdAt)}
                  </span>
                  {isAdmin && !lockedLastAdmin ? (
                    <Select
                      value={m.role}
                      onValueChange={(v) => void handleRoleChange(m.userId, v as OrgRole)}
                    >
                      <SelectTrigger className="h-8 text-[12px]">
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent>
                        <SelectItem value="admin">Admin</SelectItem>
                        <SelectItem value="editor">Editor</SelectItem>
                        <SelectItem value="viewer">Viewer</SelectItem>
                      </SelectContent>
                    </Select>
                  ) : (
                    <MonoPill variant={m.role === 'admin' ? 'accent' : 'neutral'} dot>
                      {m.role}
                    </MonoPill>
                  )}
                  {isAdmin && !lockedLastAdmin ? (
                    <button
                      type="button"
                      onClick={() => void handleRemove(m.userId)}
                      className="text-[12px] text-text-muted hover:text-bad transition-colors justify-self-end"
                    >
                      Remove
                    </button>
                  ) : lockedLastAdmin ? (
                    <span className="font-mono text-[10px] text-text-faint justify-self-end" title="Cannot remove the last admin">
                      🔒 last admin
                    </span>
                  ) : (
                    <span />
                  )}
                </div>
              )
            })}
          </div>
          </div>
        )}
      </Section>

      {(invitations.data ?? []).length > 0 && (
        <Section title="Pending invitations" className="mb-5">
          <div className="overflow-x-auto">
          <div className="divide-y divide-border min-w-[520px]">
            {(invitations.data ?? []).map((inv) => {
              const expires = new Date(inv.expires_at)
              const daysLeft = Math.max(0, Math.ceil((expires.getTime() - mountNow) / 86_400_000))
              return (
                <div
                  key={inv.id}
                  className="grid grid-cols-[1.6fr_1fr_130px_100px] gap-4 px-6 py-3 items-center"
                >
                  <span className="text-[13px] text-text truncate">{inv.email}</span>
                  <span className="font-mono text-[11px] text-text-muted">
                    {/* Pre-mount we don't know the user's local clock yet —
                        render a stable placeholder so SSR + first paint agree.
                        The `mountNow` closure recomputes daysLeft on the
                        post-mount render. */}
                    {mounted ? `expires in ${daysLeft}d` : 'expires in …'}
                  </span>
                  <MonoPill variant="neutral" dot>{inv.role}</MonoPill>
                  {isAdmin ? (
                    <button
                      type="button"
                      onClick={() => void handleCancel(inv.id)}
                      className="text-[12px] text-text-muted hover:text-bad transition-colors justify-self-end"
                    >
                      Cancel
                    </button>
                  ) : (
                    <span />
                  )}
                </div>
              )
            })}
          </div>
          </div>
        </Section>
      )}

      <Dialog open={inviteOpen} onOpenChange={setInviteOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Invite member</DialogTitle>
          </DialogHeader>
          {/* Stacked layout (label above input). The settings page's
              <FormRow> uses a 260px label column + px-6, that overflows the
              ~512px dialog width and pushes the inputs past the right edge. */}
          <form onSubmit={(e) => void submitInvite(e)} className="mt-3 space-y-4">
            <div>
              <label className="block text-[12px] text-text-muted mb-1.5">Email</label>
              <input
                type="email"
                required
                value={inviteEmail}
                onChange={(e) => setInviteEmail(e.target.value)}
                placeholder="teammate@company.com"
                autoFocus
                className="w-full px-3 py-2 border border-border-strong rounded-[6px] bg-bg text-[13px] outline-none focus:border-accent"
              />
            </div>
            <div>
              <label className="block text-[12px] text-text-muted mb-1.5">Role</label>
              <Select value={inviteRole} onValueChange={(v) => setInviteRole(v as OrgRole)}>
                <SelectTrigger className="w-full">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="admin">Admin, manage everything</SelectItem>
                  <SelectItem value="editor">Editor, create/modify data</SelectItem>
                  <SelectItem value="viewer">Viewer, read-only</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {inviteError && <div className="text-[12.5px] text-bad">{inviteError}</div>}
            {inviteSuccess && (
              <div className="text-[12px] text-good break-all">{inviteSuccess}</div>
            )}
            <div className="flex gap-2 justify-end pt-1">
              <GhostBtn type="button" onClick={() => setInviteOpen(false)}>Close</GhostBtn>
              <PrimaryBtn type="submit" disabled={inviteMutation.isPending}>
                {inviteMutation.isPending ? 'Sending…' : 'Send invitation'}
              </PrimaryBtn>
            </div>
          </form>
        </DialogContent>
      </Dialog>
    </div>
  )
}

// ─── SECURITY tab ─────────────────────────────────────────────────────────────

function SecurityTab() {
  const { data: org } = useOrganization()
  // Remount inner panel when org's threshold value changes so the local input
  // state initialises from the new server value — avoids setState-in-effect.
  return <SecurityTabInner key={String(org?.stale_key_threshold_days ?? '__loading__')} />
}

function SecurityTabInner() {
  const { data: org } = useOrganization()
  const updateSecurity = useUpdateSecuritySettings()
  const [thresholdDays, setThresholdDays] = useState<string>(
    org ? String(org.stale_key_threshold_days) : '90',
  )
  const staleAlertsEnabled = org?.stale_key_alerts_enabled ?? true
  const leakDetectionEnabled = org?.leak_detection_enabled ?? false

  function commitThreshold() {
    const n = Number(thresholdDays)
    if (!Number.isInteger(n) || n < 30 || n > 365) {
      if (org) setThresholdDays(String(org.stale_key_threshold_days))
      return
    }
    if (org && n === org.stale_key_threshold_days) return
    void updateSecurity.mutateAsync({ stale_key_threshold_days: n })
  }

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Security"
        description="Notification settings for key health monitoring. Neither setting auto-revokes keys."
      />
      <Section title="Key monitoring" className="mb-5">
        <FormRow label="Stale key reminders" hint="Email admins a weekly digest of keys idle this long.">
          <div className="flex items-center gap-3">
            <NativeInput
              type="number"
              min={30}
              max={365}
              value={thresholdDays}
              onChange={(e) => setThresholdDays(e.target.value)}
              onBlur={commitThreshold}
              disabled={!staleAlertsEnabled || updateSecurity.isPending}
              className="w-20 font-mono text-[12.5px]"
            />
            <span className="font-mono text-[11px] text-text-faint">days (30–365)</span>
            <Toggle
              on={staleAlertsEnabled}
              disabled={updateSecurity.isPending}
              onToggle={() => void updateSecurity.mutateAsync({ stale_key_alerts_enabled: !staleAlertsEnabled })}
            />
          </div>
        </FormRow>
        <FormRow label="Leaked-key detection" hint="Daily GitGuardian scan against public sources. Email-only, admins decide whether to revoke.">
          <Toggle
            on={leakDetectionEnabled}
            disabled={updateSecurity.isPending}
            onToggle={() => void updateSecurity.mutateAsync({ leak_detection_enabled: !leakDetectionEnabled })}
          />
        </FormRow>
      </Section>
    </div>
  )
}

// ─── AUDIT LOG tab ────────────────────────────────────────────────────────────

const PAGE_SIZE_AUDIT = 50
type AuditTimeWindow = '7d' | '30d' | '90d' | 'all'

const AUDIT_TIME_WINDOWS: { value: AuditTimeWindow; label: string; days: number | null }[] = [
  { value: '7d',  label: 'Last 7 days',  days: 7 },
  { value: '30d', label: 'Last 30 days', days: 30 },
  { value: '90d', label: 'Last 90 days', days: 90 },
  { value: 'all', label: 'All time',     days: null },
]

function auditWindowToFrom(window: AuditTimeWindow): string | undefined {
  const entry = AUDIT_TIME_WINDOWS.find((w) => w.value === window)
  if (!entry || entry.days === null) return undefined
  return new Date(Date.now() - entry.days * 24 * 60 * 60 * 1000).toISOString()
}

/**
 * AuditLogTab — full audit log viewer.
 *
 * Renders the same content the dedicated /settings/audit-logs route used to
 * before we collapsed it back here. Filters by time window + action, paginates,
 * and opens a Drawer with the metadata JSON when a row is clicked.
 *
 * Admin-gated because audit logs surface IPs and actor UUIDs across the org.
 * Non-admins land on a clean explanation instead of a hard 403.
 */
function AuditLogTab() {
  const roleLoading = useCurrentRoleLoading()
  const isAdmin = useIsAdmin()
  const [timeWindow, setTimeWindow] = useState<AuditTimeWindow>('30d')
  const [actionFilter, setActionFilter] = useState<string>('')
  const [page, setPage] = useState(0)

  const params: UseAuditLogsParams = useMemo(() => {
    // exactOptionalPropertyTypes: true — only set keys we have values for.
    const next: UseAuditLogsParams = {
      limit: PAGE_SIZE_AUDIT,
      offset: page * PAGE_SIZE_AUDIT,
    }
    if (actionFilter) next.action = actionFilter
    const from = auditWindowToFrom(timeWindow)
    if (from) next.from = from
    return next
  }, [actionFilter, timeWindow, page])

  const query = useAuditLogsPage(params, { enabled: isAdmin })
  const actionsQuery = useAuditLogActions()
  // Stabilise `rows` so the dependent useMemo doesn't recompute every render.
  // The `?? []` on the raw expression would create a fresh array on each call
  // and bust memoisation downstream.
  const rows = useMemo(() => query.data?.rows ?? [], [query.data?.rows])
  const total = query.data?.meta.total ?? 0
  const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE_AUDIT))
  const showingFrom = total === 0 ? 0 : page * PAGE_SIZE_AUDIT + 1
  const showingTo = Math.min(total, page * PAGE_SIZE_AUDIT + PAGE_SIZE_AUDIT)

  const bySev = useMemo(() => ({
    high: rows.filter((e) => inferAuditSeverity(e.action) === 'high').length,
    med:  rows.filter((e) => inferAuditSeverity(e.action) === 'med').length,
    low:  rows.filter((e) => inferAuditSeverity(e.action) === 'low').length,
  }), [rows])

  if (roleLoading) {
    return (
      <div className="max-w-[980px]">
        <TabHeader title="Audit log" description="Checking permissions…" />
      </div>
    )
  }

  if (!isAdmin) {
    return (
      <div className="max-w-[980px]">
        <TabHeader
          title="Audit log"
          description="Every state change in the workspace. Immutable · service-role writes only."
        />
        <div className="rounded-lg border border-border bg-bg-elev p-6">
          <div className="font-mono text-[12.5px] text-text mb-2">Admin only</div>
          <div className="font-mono text-[11.5px] text-text-muted">
            The full audit log viewer is restricted to workspace admins. Audit rows
            surface actor IDs and IP addresses across every member — only admins
            can see them. Editors and viewers don&apos;t lose the ability to act;
            they just can&apos;t inspect history here.
          </div>
        </div>
      </div>
    )
  }

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Audit log"
        description="Every state change in the workspace. Immutable · service-role writes only."
      />

      <div className="grid grid-cols-3 gap-3 mb-5">
        {[
          { k: 'HIGH', n: bySev.high, sub: 'billing · auth · destructive', accent: true },
          { k: 'MED',  n: bySev.med,  sub: 'create · update · invite',     accent: false },
          { k: 'LOW',  n: bySev.low,  sub: 'other events',                 accent: false },
        ].map((s) => (
          <div key={s.k} className={cn('border rounded-lg p-3', s.accent ? 'border-accent-border bg-accent-bg' : 'border-border bg-bg-elev')}>
            <div className="flex items-baseline justify-between">
              <span className={cn('font-mono text-[10px] tracking-[0.05em]', s.accent ? 'text-accent' : 'text-text-faint')}>{s.k}</span>
              <span className="font-mono text-[22px] font-medium text-text">{s.n}</span>
            </div>
            <div className="font-mono text-[10.5px] text-text-muted mt-1">{s.sub}</div>
          </div>
        ))}
      </div>

      <Section
        title="Events"
        action={(
          <div className="flex items-center gap-2">
            <select
              value={timeWindow}
              onChange={(e) => { setTimeWindow(e.target.value as AuditTimeWindow); setPage(0) }}
              className="bg-bg-elev border border-border rounded-[5px] px-2 py-1 font-mono text-[11px] text-text"
            >
              {AUDIT_TIME_WINDOWS.map((w) => (
                <option key={w.value} value={w.value}>{w.label}</option>
              ))}
            </select>
            <select
              value={actionFilter}
              onChange={(e) => { setActionFilter(e.target.value); setPage(0) }}
              className="bg-bg-elev border border-border rounded-[5px] px-2 py-1 font-mono text-[11px] text-text min-w-[180px]"
            >
              <option value="">All actions</option>
              {(actionsQuery.data ?? []).map((action) => (
                <option key={action} value={action}>{action}</option>
              ))}
            </select>
          </div>
        )}
        className="mb-5"
      >
        <AuditLogsTable
          rows={rows}
          isLoading={query.isLoading}
          emptyHint={
            actionFilter || timeWindow !== 'all'
              ? 'No events match the current filters. Try widening the time range.'
              : 'No audit events yet.'
          }
        />

        {total > 0 && (
          <div className="border-t border-border px-6 py-3 flex items-center justify-between text-[11.5px] font-mono">
            <span className="text-text-faint">
              Showing {showingFrom}–{showingTo} of {total.toLocaleString('en-US')}
            </span>
            <div className="flex items-center gap-1">
              <button
                type="button"
                disabled={page === 0}
                onClick={() => setPage((p) => Math.max(0, p - 1))}
                className={cn(
                  'rounded-[5px] border border-border px-2 py-1 text-[11px]',
                  page === 0 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-bg-muted',
                )}
              >
                ← Prev
              </button>
              <span className="text-text-faint px-2">
                Page {page + 1} / {pageCount}
              </span>
              <button
                type="button"
                disabled={page + 1 >= pageCount}
                onClick={() => setPage((p) => p + 1)}
                className={cn(
                  'rounded-[5px] border border-border px-2 py-1 text-[11px]',
                  page + 1 >= pageCount ? 'opacity-40 cursor-not-allowed' : 'hover:bg-bg-muted',
                )}
              >
                Next →
              </button>
            </div>
          </div>
        )}
      </Section>
    </div>
  )
}

// ─── SYSTEM tab ───────────────────────────────────────────────────────────────

function SystemTab() {
  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="System"
        description="Cron job execution history. Runs are logged after each execution. Refreshes every 60s."
      />
      <div className="border border-border rounded-[8px] overflow-hidden">
        <CronJobsPanel />
      </div>
    </div>
  )
}

// ─── BILLING tab ──────────────────────────────────────────────────────────────

function BillingTab() {
  const { data: subscription, isLoading } = useSubscription()
  const { data: quota } = useQuota()

  const planName = subscription?.plan ?? 'free'
  const planLabel = formatPlanLabel(planName)

  const usedThisMonth = quota?.usedThisMonth ?? 0
  const limit = quota?.limit ?? 10_000
  const pct = limit > 0 ? Math.min(1, usedThisMonth / limit) : 0

  return (
    <div className="max-w-[920px]">
      <TabHeader title="Billing" description="Per-request pricing. What ingests this month is what you pay." />

      <QuotaBanner />

      {/* Hero card */}
      <div className="border border-border rounded-xl bg-bg-elev p-6 grid grid-cols-1 sm:grid-cols-2 gap-6 mb-5">
        <div>
          <div className="font-mono text-[10px] text-text-faint uppercase tracking-[0.05em] mb-3">Current plan</div>
          {isLoading ? (
            <div className="h-8 w-32 bg-bg-muted rounded animate-pulse mb-4" />
          ) : (
            <>
              <div className="flex items-center gap-3 mb-1">
                <span className="text-[30px] font-medium tracking-[-0.6px]">{planLabel}</span>
                <span className={cn(
                  'font-mono text-[10px] uppercase tracking-[0.04em] px-2 py-0.5 rounded-full border',
                  subscription?.status === 'active'
                    ? 'bg-good-bg border-good/20 text-good'
                    : subscription?.status === 'past_due'
                      ? 'bg-accent-bg border-accent-border text-accent'
                      : 'bg-bg border-border text-text-muted',
                )}>
                  {subscription?.status ?? 'free'}
                </span>
              </div>
              <div className="text-[12.5px] text-text-muted mb-4">
                {subscription?.current_period_end
                  ? subscription.cancel_at_period_end
                    ? `Access until ${formatDate(subscription.current_period_end)}`
                    : `Renews on ${formatDate(subscription.current_period_end)}`
                  : 'No active subscription'}
              </div>
            </>
          )}
        </div>
        <div>
          <div className="font-mono text-[10px] text-text-faint uppercase tracking-[0.05em] mb-3">This cycle</div>
          <div className="h-2.5 bg-bg-muted rounded-full overflow-hidden mb-2">
            <div className="h-full bg-text rounded-full" style={{ width: `${(pct * 100).toFixed(1)}%` }} />
          </div>
          <div className="flex justify-between font-mono text-[11px] text-text-muted">
            <span><span className="text-text">{usedThisMonth.toLocaleString()}</span> / {limit.toLocaleString()} req</span>
            <span>{(pct * 100).toFixed(0)}% used</span>
          </div>
        </div>
      </div>

      {/* Payment + refund disclosure, post-purchase variant. PLAN & LIMITS
          has the same information in pre-purchase wording; keeping both
          surfaces self-contained means a user who lands on either tab
          finds the cancellation and refund paths without bouncing around. */}
      <Section title="Payment, cancellation & refunds" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed space-y-2">
          <p>
            Payments are processed by Paddle. Update your payment method or
            cancel your subscription from the link Paddle sent when you
            subscribed, or email{' '}
            <a
              href="mailto:support@spanlens.io"
              className="text-accent font-medium hover:opacity-80 transition-opacity"
            >
              support@spanlens.io
            </a>
            . Cancellation stops future renewals; your current plan stays
            active through the billing period you already paid for.
          </p>
          <p>
            Eligible for a full refund within 14 days of the initial charge
            if usage is below 10% of plan quota. See our{' '}
            <Link
              href="/refund"
              className="text-accent font-medium hover:opacity-80 transition-opacity"
            >
              Refund Policy
            </Link>
            {' '}for the full terms, EU statutory withdrawal rights, and how
            to request a refund.
          </p>
        </div>
      </Section>

      <Section title="Budget alerts" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted">
          Set cost and request thresholds in the{' '}
          <Link href="/alerts" className="text-accent font-medium hover:opacity-80 transition-opacity">
            Alerts →
          </Link>{' '}
          tab to get notified before spend exceeds your quota.
        </div>
      </Section>
    </div>
  )
}

// ─── PLAN & LIMITS tab ────────────────────────────────────────────────────────

function PlanLimitsTab() {
  const { data: org } = useOrganization()
  const { data: subscription, isLoading: subLoading } = useSubscription()
  const { data: quota } = useQuota()
  const createCheckout = useCreateCheckout()
  const refreshSubscription = useRefreshSubscription()
  const update = useUpdateOverageSettings()
  const currentMember = useCurrentMember()
  const isAdmin = currentMember?.role === 'admin'
  const [multiplierDraft, setMultiplierDraft] = useState(String(org?.overage_cap_multiplier ?? 2))
  const [paddle, setPaddle] = useState<Paddle | null>(null)
  const [checkoutError, setCheckoutError] = useState<string | null>(null)

  const clientToken = process.env['NEXT_PUBLIC_PADDLE_CLIENT_TOKEN']
  const paddleEnv = (process.env['NEXT_PUBLIC_PADDLE_ENVIRONMENT'] ?? 'sandbox') as 'sandbox' | 'production'

  useEffect(() => {
    if (!clientToken) return
    let cancelled = false
    void initializePaddle({
      environment: paddleEnv,
      token: clientToken,
      eventCallback: (event) => {
        if (event.name === 'checkout.completed') {
          setTimeout(() => refreshSubscription(), 1500)
        }
      },
    }).then((instance) => {
      if (!cancelled && instance) setPaddle(instance)
    })
    return () => { cancelled = true }
  }, [clientToken, paddleEnv, refreshSubscription])

  const handleUpgrade = useCallback(async (plan: 'starter' | 'team') => {
    setCheckoutError(null)
    if (!paddle) {
      setCheckoutError('Paddle.js is not ready yet. Please try again in a moment.')
      return
    }
    try {
      const res = await createCheckout.mutateAsync({ plan })
      paddle.Checkout.open({ transactionId: res.transactionId })
    } catch (err) {
      setCheckoutError(err instanceof Error ? err.message : 'Failed to start checkout')
    }
  }, [paddle, createCheckout])

  const currentPlan: BillingPlan = subscription?.plan ?? 'free'
  const isFree = currentPlan === 'free'
  const isEnterprise = currentPlan === 'enterprise'

  const usedThisMonth = quota?.usedThisMonth ?? 0
  const planLimit = PLAN_REQUEST_LIMITS[currentPlan]
  const limitLabel = planLimit != null ? planLimit.toLocaleString() : 'unlimited'
  const headroom = planLimit != null
    ? `${Math.max(0, Math.round((1 - usedThisMonth / planLimit) * 100))}%`
    : '∞'
  const seatLimit = PLAN_SEAT_LIMITS[currentPlan]
  const seatLimitLabel = seatLimit == null ? 'unlimited' : String(seatLimit)
  const workspaceLimit = PLAN_WORKSPACE_LIMITS[currentPlan]
  const workspaceLimitLabel = workspaceLimit == null ? 'unlimited' : String(workspaceLimit)
  const retentionDays = PLAN_RETENTION_DAYS[currentPlan] ?? 14
  const retentionLabel = `${retentionDays} days`

  return (
    <div className="max-w-[1040px]">
      <TabHeader title="Plan & limits" description="Compare plans. Hard limits apply per-workspace; can be lifted on Enterprise." />

      {checkoutError && (
        <div className="rounded-lg border border-accent-border bg-accent-bg px-4 py-3 mb-5 text-[13px] text-accent">
          {checkoutError}
        </div>
      )}

      {/* Plan cards */}
      <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 mb-6">
        {PLANS.map((plan) => {
          const isCurrent = currentPlan === plan.id
          const isUpgradeInFlight = createCheckout.isPending && createCheckout.variables?.plan === plan.id
          return (
            <div
              key={plan.id}
              className={cn(
                'border rounded-xl p-4 flex flex-col gap-3 min-h-[280px]',
                isCurrent ? 'border-accent bg-accent-bg' : 'border-border bg-bg-elev',
              )}
            >
              <div className="flex items-start justify-between">
                <span className="text-[15px] font-medium text-text">{plan.name}</span>
                {isCurrent && <MonoPill variant="accent" dot>current</MonoPill>}
              </div>
              <div>
                {plan.priceUsd !== null ? (
                  <div className="flex items-baseline gap-1">
                    <span className="font-mono text-[20px] font-medium tracking-[-0.2px] text-text">${plan.priceUsd}</span>
                    <span className="font-mono text-[10.5px] text-text-muted">/ {plan.pricePeriod}</span>
                  </div>
                ) : (
                  <div className="font-mono text-[20px] font-medium text-text">Custom</div>
                )}
                <div className="font-mono text-[10.5px] text-text-muted mt-1">{plan.description}</div>
              </div>
              <ul className="flex-1 space-y-1.5">
                {plan.features.map((f) => (
                  <li key={f} className="flex items-start gap-2 font-mono text-[10.5px] text-text-muted">
                    <Check className="h-3 w-3 mt-0.5 text-good shrink-0" />
                    <span>{f}</span>
                  </li>
                ))}
              </ul>
              <div>
                {plan.id === 'free' ? (
                  <button type="button" disabled className="w-full h-8 rounded-[6px] border border-border bg-bg text-[12.5px] font-medium text-text-faint cursor-not-allowed">
                    Default
                  </button>
                ) : plan.id === 'enterprise' ? (
                  <GhostBtn className="w-full justify-center" onClick={() => window.open('mailto:sales@spanlens.io', '_blank')}>
                    Contact sales
                  </GhostBtn>
                ) : isCurrent ? (
                  <button type="button" disabled className="w-full h-8 rounded-[6px] border border-border bg-bg text-[12.5px] font-medium text-text-faint cursor-not-allowed">
                    Current plan
                  </button>
                ) : !isAdmin ? (
                  <button type="button" disabled className="w-full h-8 rounded-[6px] border border-border bg-bg text-[12.5px] font-medium text-text-faint cursor-not-allowed" title="Only admins can change the plan">
                    Admin only
                  </button>
                ) : (
                  <button
                    type="button"
                    disabled={createCheckout.isPending || !paddle || subLoading}
                    onClick={() => void handleUpgrade(plan.id as 'starter' | 'team')}
                    className="w-full h-8 rounded-[6px] bg-text text-bg text-[12.5px] font-medium hover:opacity-90 transition-opacity disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed"
                  >
                    {isUpgradeInFlight ? 'Opening checkout…' : !paddle ? 'Loading…' : `Upgrade to ${plan.name}`}
                  </button>
                )}
              </div>
            </div>
          )
        })}
      </div>

      {/* Renewal + refund disclosure — placed right under the plan cards so
          the policy is visible at the point of purchase decision. Matches the
          requirement of the Korean Act on Consumer Protection in Electronic
          Commerce (전자상거래법) for refund terms accessible from the order
          surface, and keeps the trust-building information one scroll away
          from the upgrade buttons. */}
      <Section title="Billing & refunds" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed space-y-2">
          <p>
            Paid plans renew automatically at the start of each billing period.
            Cancel any time from the Paddle receipt link or by emailing{' '}
            <a
              href="mailto:support@spanlens.io"
              className="text-accent font-medium hover:opacity-80 transition-opacity"
            >
              support@spanlens.io
            </a>
            . Your plan stays active through the period you have already paid for.
          </p>
          <p>
            Full refund available within 14 days of the initial charge if usage
            is below 10% of plan quota. See our{' '}
            <Link
              href="/refund"
              className="text-accent font-medium hover:opacity-80 transition-opacity"
            >
              Refund Policy
            </Link>
            {' '}for the full terms, EU statutory withdrawal rights, and how to
            request a refund.
          </p>
        </div>
      </Section>

      <Section title="Hard limits" action={<Hint>{currentPlan} plan</Hint>} className="mb-5">
        <div className="overflow-x-auto">
        <div className="divide-y divide-border min-w-[420px]">
          <div className="grid grid-cols-[1.4fr_1fr_1fr_1fr] gap-4 px-6 py-3 font-mono text-[10px] uppercase tracking-[0.05em] text-text-faint">
            {['Resource', 'Limit', 'Used now', 'Headroom'].map((h) => <span key={h}>{h}</span>)}
          </div>
          {[
            ['Requests / month',     limitLabel,           usedThisMonth.toLocaleString(), headroom],
            ['Team seats',           seatLimitLabel,       '—',                            '—'],
            ['Workspaces (owned)',   workspaceLimitLabel,  '—',                            '—'],
            ['Log retention',        retentionLabel,       '—',                            '—'],
            ['API keys',             '25',                 '—',                            '—'],
            ['Alert rules',          '100',                '—',                            '—'],
          ].map(([res, lim, used, head]) => (
            <div key={res} className="grid grid-cols-[1.4fr_1fr_1fr_1fr] gap-4 px-6 py-3">
              <span className="font-mono text-[12px] text-text-muted">{res}</span>
              <span className="font-mono text-[12px] text-text">{lim}</span>
              <span className="font-mono text-[12px] text-text">{used}</span>
              <span className="font-mono text-[12px] text-text">{head}</span>
            </div>
          ))}
        </div>
        </div>
      </Section>

      {!isEnterprise && (
        <Section title="Overage billing" description="Applies when your monthly quota is reached" className="mb-5">
          {isFree ? (
            <div className="px-6 py-4 text-[13px] text-text-muted">
              Overage is not available on the Free plan. Logging pauses past the quota, but the proxy keeps forwarding requests. Upgrade to Pro or Team to resume logging and enable overage.
            </div>
          ) : (
            <>
              <FormRow label="Allow overage charges" hint="When quota is reached, continue serving and bill overage. Off = 429 past the limit.">
                <Toggle
                  on={org?.allow_overage ?? false}
                  disabled={update.isPending || !isAdmin}
                  onToggle={() => void update.mutateAsync({ allow_overage: !(org?.allow_overage ?? false) })}
                />
              </FormRow>
              <FormRow label="Max overage multiplier" hint="Hard cap = monthly limit × this value. Requests past the cap return 429.">
                <div className="flex items-center gap-2">
                  <NativeInput
                    type="number"
                    min={1}
                    max={100}
                    disabled={!(org?.allow_overage ?? false) || update.isPending || !isAdmin}
                    value={multiplierDraft}
                    onChange={(e) => setMultiplierDraft(e.target.value)}
                    className="w-20 font-mono text-[12.5px]"
                  />
                  <span className="font-mono text-[11.5px] text-text-faint">×</span>
                  {isAdmin && (
                    <GhostBtn
                      disabled={
                        !(org?.allow_overage ?? false) ||
                        update.isPending ||
                        Number(multiplierDraft) === (org?.overage_cap_multiplier ?? 2) ||
                        !Number.isInteger(Number(multiplierDraft)) ||
                        Number(multiplierDraft) < 1 ||
                        Number(multiplierDraft) > 100
                      }
                      onClick={() => void update.mutateAsync({ overage_cap_multiplier: Number(multiplierDraft) })}
                    >
                      {update.isPending ? 'Saving…' : 'Save'}
                    </GhostBtn>
                  )}
                </div>
              </FormRow>
              {!isAdmin && (
                <div className="px-6 pb-4 text-[11.5px] text-text-faint">Only admins can change overage settings.</div>
              )}
            </>
          )}
        </Section>
      )}
    </div>
  )
}

// ─── INVOICES tab ─────────────────────────────────────────────────────────────

function InvoicesTab() {
  const { data: subscription } = useSubscription()

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Invoices"
        description="Invoices are issued and delivered by Paddle, our payment processor."
      />

      <Section title="Where to find your invoices" className="mb-5">
        <div className="px-6 py-5 space-y-4 text-[13px] text-text-muted leading-relaxed">
          <p>
            Every invoice lands in your inbox from <span className="font-mono text-text">noreply@paddle.com</span> as
            a PDF attachment, usually within minutes of each renewal or top-up charge.
          </p>
          <p>
            To browse past invoices, update your payment method, or cancel, use the self-service link that
            Paddle emailed when you first subscribed. Paddle&apos;s customer portal is the source of truth for
            billing history.
          </p>
          {subscription ? (
            <p className="font-mono text-[11.5px] text-text-faint">
              Current subscription · {subscription.plan} · renews {formatDate(subscription.current_period_end)}
            </p>
          ) : (
            <p className="font-mono text-[11.5px] text-text-faint">
              You&apos;re on the free plan, no invoices generated.
            </p>
          )}
        </div>
      </Section>
    </div>
  )
}

// ─── PROFILE tab ──────────────────────────────────────────────────────────────

function ProfileTab() {
  const { data: user, isLoading } = useCurrentUser()
  return (
    <div className="max-w-[920px]">
      <TabHeader
        title="Profile"
        description="Your sign-in identity. Managed by Supabase Auth."
      />

      <Section title="Account" className="mb-5">
        {isLoading ? (
          <div className="px-6 py-4 font-mono text-[12.5px] text-text-faint">Loading…</div>
        ) : user ? (
          <>
            <FormRow label="Email">
              <div className="font-mono text-[12.5px] text-text">{user.email ?? '—'}</div>
            </FormRow>
            <FormRow label="User ID">
              <div className="font-mono text-[11px] text-text-muted truncate">{user.id}</div>
            </FormRow>
            <FormRow label="Account created">
              <div className="font-mono text-[12px] text-text-muted">
                {formatDate(user.created_at)}
              </div>
            </FormRow>
          </>
        ) : (
          <div className="px-6 py-4 font-mono text-[12.5px] text-text-faint">Not signed in.</div>
        )}
      </Section>

      <Section title="Change sign-in details" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed">
          Email changes, password resets, and two-factor setup go through Supabase&apos;s auth flows.
          Use the <span className="font-mono text-text">&quot;Forgot password?&quot;</span> link on the login
          page to trigger a reset email.
        </div>
      </Section>
    </div>
  )
}

// ─── SIGN-IN METHODS tab ──────────────────────────────────────────────────────

interface ProviderConfig {
  id: LinkableProvider
  label: string
  glyph: string
}

const SIGNIN_PROVIDERS: ProviderConfig[] = [
  { id: 'google', label: 'Google', glyph: 'G' },
  { id: 'github', label: 'GitHub', glyph: '⌥' },
]

/**
 * Translates `?error=<code>` query left by /auth/callback after a failed
 * linkIdentity flow into something the user can act on. Codes mirror
 * `mapOAuthError` in apps/web/app/auth/callback/route.ts.
 */
const LINK_ERROR_MESSAGES: Record<string, string> = {
  identity_already_linked:
    'This provider is already connected to your account.',
  identity_linked_to_other_user:
    'This Google or GitHub account is already linked to a different Spanlens user. Use a different provider account, or sign in with that one instead.',
  manual_linking_disabled:
    'Account linking is currently disabled. Please contact support.',
  provider_disabled:
    'This sign-in method is currently unavailable.',
  oauth_callback_failed: 'Connecting the provider failed. Please try again.',
}

function SignInMethodsTab() {
  const { data: user, isLoading: userLoading } = useCurrentUser()
  const { data: identities, isLoading: identitiesLoading, error: identitiesError } = useIdentities()
  const linkMutation = useLinkIdentity()
  const unlinkMutation = useUnlinkIdentity()
  const [actionError, setActionError] = useState<string | null>(null)

  // Surface failures that happened in /auth/callback (linkIdentity flow
  // round-trips through the provider, so a thrown mutation here can't
  // catch them — the callback redirects back with `?error=<code>`).
  // Same `window.location` pattern as the login page; runs once per
  // mount so cascading-render concerns don't apply.
  useEffect(() => {
    if (typeof window === 'undefined') return
    const params = new URLSearchParams(window.location.search)
    const code = params.get('error')
    if (!code) return
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setActionError(LINK_ERROR_MESSAGES[code] ?? 'Connecting the provider failed. Please try again.')
    params.delete('error')
    const next = params.toString()
    window.history.replaceState(
      null,
      '',
      `${window.location.pathname}${next ? `?${next}` : ''}`,
    )
  }, [])

  const identityList: UserIdentity[] = identities ?? []
  const isLastIdentity = identityList.length <= 1
  const isLoading = userLoading || identitiesLoading
  const isBusy = linkMutation.isPending || unlinkMutation.isPending

  function findIdentity(provider: LinkableProvider): UserIdentity | undefined {
    return identityList.find((row) => row.provider === provider)
  }

  async function handleConnect(provider: LinkableProvider) {
    setActionError(null)
    try {
      await linkMutation.mutateAsync({ provider })
    } catch (err) {
      setActionError(err instanceof Error ? err.message : 'Failed to start sign-in flow.')
    }
  }

  async function handleDisconnect(identity: UserIdentity) {
    setActionError(null)
    try {
      await unlinkMutation.mutateAsync(identity)
    } catch (err) {
      setActionError(err instanceof Error ? err.message : 'Failed to disconnect provider.')
    }
  }

  return (
    <div className="max-w-[920px]">
      <TabHeader
        title="Sign-in methods"
        description="Manage how you sign in to Spanlens. Connect multiple providers to the same account, then sign in with any of them."
      />

      <Section title="Linked providers" className="mb-5">
        {isLoading ? (
          <div className="px-6 py-4 font-mono text-[12.5px] text-text-faint">Loading…</div>
        ) : identitiesError ? (
          <div className="px-6 py-4 font-mono text-[12.5px] text-bad">
            Failed to load identities. Reload the page to try again.
          </div>
        ) : (
          <>
            <FormRow label="Email">
              <div className="flex items-center justify-between gap-3">
                <div className="font-mono text-[12.5px] text-text">{user?.email ?? '—'}</div>
                <MonoPill variant="accent">Primary</MonoPill>
              </div>
            </FormRow>

            {SIGNIN_PROVIDERS.map((provider) => {
              const linked = findIdentity(provider.id)
              const disconnectBlocked = Boolean(linked) && isLastIdentity
              return (
                <FormRow key={provider.id} label={provider.label}>
                  <div className="flex items-center justify-between gap-3">
                    <div className="flex items-center gap-2.5 min-w-0">
                      <span className="w-[18px] h-[18px] rounded-[4px] bg-bg-muted flex items-center justify-center font-mono text-[10px] text-text-muted font-bold shrink-0">
                        {provider.glyph}
                      </span>
                      {linked ? (
                        <MonoPill variant="good" dot>
                          Connected
                          {linked.last_sign_in_at
                            ? ` · last used ${formatDate(linked.last_sign_in_at)}`
                            : ''}
                        </MonoPill>
                      ) : (
                        <MonoPill variant="faint" dot>Not connected</MonoPill>
                      )}
                    </div>
                    {linked ? (
                      <GhostBtn
                        onClick={() => void handleDisconnect(linked)}
                        disabled={isBusy || disconnectBlocked}
                        title={disconnectBlocked ? 'Connect another sign-in method before removing this one.' : undefined}
                      >
                        Disconnect
                      </GhostBtn>
                    ) : (
                      <PrimaryBtn
                        onClick={() => void handleConnect(provider.id)}
                        disabled={isBusy}
                      >
                        Connect
                      </PrimaryBtn>
                    )}
                  </div>
                </FormRow>
              )
            })}
          </>
        )}

        {actionError && (
          <div className="px-6 py-3 border-t border-border text-[12.5px] text-bad">
            {actionError}
          </div>
        )}
      </Section>

      <Section title="Why connect multiple providers?" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed space-y-2">
          <p>
            One account, multiple ways in. Connect Google or GitHub to sign in faster the
            next time and keep email as a fallback if a provider is unavailable.
          </p>
          <p>
            Spanlens never receives your provider password. Disconnecting a provider
            removes its OAuth token from this account immediately and cannot be undone
            from the provider&apos;s side.
          </p>
        </div>
      </Section>
    </div>
  )
}

// ─── NOTIFICATIONS tab ────────────────────────────────────────────────────────

interface NotificationPrefDef {
  key: 'security_alert_emails' | 'marketing_emails' | 'product_update_emails'
  label: string
  hint: string
}

const NOTIFICATION_PREFS: NotificationPrefDef[] = [
  {
    key: 'security_alert_emails',
    label: 'Security alerts',
    hint: 'Stale-key reminders and leaked-key detection alerts for workspaces you admin.',
  },
  {
    key: 'product_update_emails',
    label: 'Product updates',
    hint: 'Occasional changelog and new-feature emails. No more than monthly.',
  },
  {
    key: 'marketing_emails',
    label: 'Marketing',
    hint: 'Launch announcements and offers. You can opt out at any time.',
  },
]

function NotificationsTab() {
  const { data: prefs, isLoading } = useNotificationPrefs()
  const update = useUpdateNotificationPrefs()

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Notifications"
        description="Which emails Spanlens sends you. These are personal to your account."
      />

      <Section title="Email preferences" className="mb-5">
        {NOTIFICATION_PREFS.map((pref) => (
          <FormRow key={pref.key} label={pref.label} hint={pref.hint}>
            <Toggle
              on={prefs?.[pref.key] ?? true}
              disabled={isLoading || update.isPending}
              onToggle={() =>
                void update.mutateAsync({ [pref.key]: !(prefs?.[pref.key] ?? true) })
              }
            />
          </FormRow>
        ))}
      </Section>

      <Section title="Alert routing" className="mb-5">
        <div className="px-6 py-4 text-[13px] text-text-muted leading-relaxed">
          Where alerts are delivered (Slack, Discord, or email channels) is a workspace-level
          setting. Manage destinations in{' '}
          <a href="/settings?tab=integrations" className="text-accent hover:opacity-80 transition-opacity">
            Integrations
          </a>{' '}
          and the rules that trigger them on the{' '}
          <a href="/alerts" className="text-accent hover:opacity-80 transition-opacity">Alerts page</a>.
        </div>
      </Section>
    </div>
  )
}

// ─── PREFERENCES tab ──────────────────────────────────────────────────────────

type ThemeOption = 'system' | 'light' | 'dark'

interface ThemeOptionDef {
  value: ThemeOption
  label: string
  Icon: LucideIcon
}

const THEME_OPTIONS: ThemeOptionDef[] = [
  { value: 'system', label: 'System', Icon: Monitor },
  { value: 'light',  label: 'Light',  Icon: Sun },
  { value: 'dark',   label: 'Dark',   Icon: Moon },
]

function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  const current = (theme ?? 'system') as ThemeOption

  return (
    <div className="flex items-center gap-1 rounded-[6px] border border-border bg-bg-muted p-1">
      {THEME_OPTIONS.map(({ value, label, Icon }) => (
        <button
          key={value}
          type="button"
          onClick={() => setTheme(value)}
          className={cn(
            'flex items-center gap-1.5 px-3 py-[5px] rounded-[4px] text-[12.5px] transition-colors',
            current === value
              ? 'bg-bg text-text font-medium shadow-sm'
              : 'text-text-muted hover:text-text',
          )}
        >
          <Icon className="h-3.5 w-3.5 shrink-0" />
          {label}
        </button>
      ))}
    </div>
  )
}

function PreferencesTab() {
  return (
    <div className="max-w-[920px]">
      <TabHeader
        title="Preferences"
        description="Personal UI preferences stored in your browser."
      />

      <Section title="Theme" className="mb-5">
        <FormRow label="Color theme" hint="Override your system preference. Stored locally in your browser.">
          <ThemeToggle />
        </FormRow>
      </Section>
    </div>
  )
}

// ─── WEBHOOKS tab ─────────────────────────────────────────────────────────────

const ALL_WEBHOOK_EVENTS: { value: WebhookEvent; label: string; hint: string }[] = [
  { value: 'request.created',  label: 'request.created',  hint: 'fires on every LLM call' },
  { value: 'trace.completed',  label: 'trace.completed',  hint: 'fires when a trace ends' },
  { value: 'alert.triggered',  label: 'alert.triggered',  hint: 'fires when an alert rule trips' },
]

function SecretField({ secret }: { secret: string }) {
  const [revealed, setRevealed] = useState(false)
  const [copied, setCopied] = useState(false)

  function handleCopy() {
    void navigator.clipboard.writeText(secret).then(() => {
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    })
  }

  return (
    <div className="flex items-center gap-2">
      <span className="font-mono text-[11px] text-text-muted bg-bg-muted px-2 py-1 rounded border border-border">
        {revealed ? secret : '•'.repeat(Math.min(secret.length, 32))}
      </span>
      <button
        type="button"
        onClick={() => setRevealed((v) => !v)}
        className="text-[11px] text-text-faint hover:text-text transition-colors"
      >
        {revealed ? 'Hide' : 'Show'}
      </button>
      <button
        type="button"
        onClick={handleCopy}
        className="text-[11px] text-text-faint hover:text-text transition-colors"
      >
        {copied ? 'Copied!' : 'Copy'}
      </button>
    </div>
  )
}

function DeliveryHistory({ webhookId }: { webhookId: string }) {
  const { data: deliveries, isLoading } = useWebhookDeliveries(webhookId)

  if (isLoading) {
    return <div className="px-6 py-3 font-mono text-[11.5px] text-text-faint">Loading…</div>
  }
  if (!deliveries || deliveries.length === 0) {
    return <div className="px-6 py-3 font-mono text-[11.5px] text-text-faint">No deliveries yet.</div>
  }

  return (
    <div className="overflow-x-auto">
    <div className="divide-y divide-border min-w-[420px]">
      <div className="grid grid-cols-[140px_80px_80px_1fr] gap-4 px-6 py-2 font-mono text-[10px] uppercase tracking-[0.05em] text-text-faint">
        <span>Time</span>
        <span>Status</span>
        <span>HTTP</span>
        <span>Error</span>
      </div>
      {deliveries.map((d) => (
        <div key={d.id} className="grid grid-cols-[140px_80px_80px_1fr] gap-4 px-6 py-2 items-center">
          <span className="font-mono text-[11px] text-text-muted">
            {new Date(d.delivered_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}
          </span>
          <MonoPill variant={d.status === 'success' ? 'good' : 'faint'} dot>
            {d.status}
          </MonoPill>
          <span className="font-mono text-[11px] text-text-muted">{d.http_status ?? '—'}</span>
          <span className="font-mono text-[11px] text-text-faint truncate">{d.error_message ?? '—'}</span>
        </div>
      ))}
    </div>
    </div>
  )
}

function WebhooksTab() {
  const { data: webhooks, isLoading } = useWebhooks()
  const createWebhook = useCreateWebhook()
  const updateWebhook = useUpdateWebhook()
  const deleteWebhook = useDeleteWebhook()
  const testWebhook = useTestWebhook()

  const [createOpen, setCreateOpen] = useState(false)
  const [newName, setNewName] = useState('')
  const [newUrl, setNewUrl] = useState('')
  const [newEvents, setNewEvents] = useState<WebhookEvent[]>(['request.created'])
  const [newActive, setNewActive] = useState(true)
  const [createError, setCreateError] = useState('')

  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [testResult, setTestResult] = useState<Record<string, string>>({})

  const currentMember = useCurrentMember()
  const canEdit = currentMember?.role === 'admin' || currentMember?.role === 'editor'

  function toggleEvent(ev: WebhookEvent) {
    setNewEvents((prev) =>
      prev.includes(ev) ? prev.filter((e) => e !== ev) : [...prev, ev],
    )
  }

  async function handleCreate(e: React.FormEvent) {
    e.preventDefault()
    setCreateError('')
    try {
      await createWebhook.mutateAsync({ name: newName, url: newUrl, events: newEvents, is_active: newActive })
      setCreateOpen(false)
      setNewName('')
      setNewUrl('')
      setNewEvents(['request.created'])
      setNewActive(true)
    } catch (err) {
      setCreateError(err instanceof Error ? err.message : 'Failed to create webhook')
    }
  }

  async function handleTest(webhook: WebhookRow) {
    try {
      const result = await testWebhook.mutateAsync(webhook.id)
      setTestResult((prev) => ({
        ...prev,
        [webhook.id]: result
          ? `${result.status} · HTTP ${result.http_status ?? '—'} · ${result.duration_ms}ms`
          : 'Sent',
      }))
      setSelectedId(webhook.id)
    } catch (err) {
      setTestResult((prev) => ({
        ...prev,
        [webhook.id]: err instanceof Error ? err.message : 'Test failed',
      }))
    }
  }

  async function handleDelete(id: string) {
    if (!confirm('Delete this webhook?')) return
    await deleteWebhook.mutateAsync(id)
    if (selectedId === id) setSelectedId(null)
  }

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Webhooks"
        description="Receive real-time HTTP callbacks when events occur in your workspace."
        action={
          canEdit ? (
            <PrimaryBtn onClick={() => { setCreateOpen(true); setCreateError('') }}>
              <Plus className="w-3.5 h-3.5" /> New webhook
            </PrimaryBtn>
          ) : null
        }
      />

      <Section title="Webhook endpoints" className="mb-5">
        {isLoading ? (
          <div className="px-6 py-8 text-center font-mono text-[12.5px] text-text-faint">Loading…</div>
        ) : (webhooks ?? []).length === 0 ? (
          <div className="px-6 py-8 text-center font-mono text-[12.5px] text-text-faint">
            No webhooks yet. Add one to start receiving events.
          </div>
        ) : (
          <div className="overflow-x-auto">
          <div className="divide-y divide-border min-w-[620px]">
            <div className="grid grid-cols-[1.8fr_1.2fr_1fr_110px_90px] gap-4 px-6 py-3 font-mono text-[10px] uppercase tracking-[0.05em] text-text-faint">
              {['Name', 'URL', 'Events', 'Status', ''].map((h, i) => <span key={i}>{h}</span>)}
            </div>
            {(webhooks ?? []).map((wh) => (
              <div key={wh.id} className="grid grid-cols-[1.8fr_1.2fr_1fr_110px_90px] gap-4 px-6 py-3 items-center">
                <button
                  type="button"
                  onClick={() => setSelectedId(wh.id === selectedId ? null : wh.id)}
                  className="text-[13px] font-medium text-left hover:text-accent transition-colors truncate"
                >
                  {wh.name}
                </button>
                <span className="font-mono text-[11px] text-text-muted truncate" title={wh.url}>
                  {wh.url}
                </span>
                <div className="flex flex-wrap gap-1">
                  {wh.events.map((ev) => (
                    <MonoPill key={ev} variant="neutral">{ev}</MonoPill>
                  ))}
                </div>
                <div className="flex items-center gap-2">
                  <Toggle
                    on={wh.is_active}
                    disabled={!canEdit || updateWebhook.isPending}
                    onToggle={() => void updateWebhook.mutateAsync({ id: wh.id, is_active: !wh.is_active })}
                  />
                  <MonoPill variant={wh.is_active ? 'good' : 'faint'} dot>
                    {wh.is_active ? 'active' : 'off'}
                  </MonoPill>
                </div>
                {canEdit && (
                  <div className="flex items-center gap-1">
                    <button
                      type="button"
                      title="Send test event"
                      disabled={testWebhook.isPending}
                      onClick={() => void handleTest(wh)}
                      className="px-2 py-1 rounded text-[11px] border border-border text-text-muted hover:text-text hover:border-border-strong transition-colors disabled:opacity-40"
                    >
                      Test
                    </button>
                    <button
                      type="button"
                      title="Delete"
                      disabled={deleteWebhook.isPending}
                      onClick={() => void handleDelete(wh.id)}
                      className="p-1.5 rounded hover:bg-accent-bg text-text-faint hover:text-accent transition-colors"
                    >
                      <Trash2 className="h-3.5 w-3.5" />
                    </button>
                  </div>
                )}
              </div>
            ))}
          </div>
          </div>
        )}
      </Section>

      {selectedId && (
        <Section title="Webhook details" className="mb-5">
          {(() => {
            const wh = (webhooks ?? []).find((w) => w.id === selectedId)
            if (!wh) return null
            return (
              <>
                <FormRow label="Signing secret" hint="Used to verify X-Spanlens-Signature on incoming events.">
                  <SecretField secret={wh.secret} />
                </FormRow>
                {testResult[selectedId] && (
                  <FormRow label="Last test result">
                    <span className="font-mono text-[11.5px] text-text-muted">{testResult[selectedId]}</span>
                  </FormRow>
                )}
              </>
            )
          })()}
        </Section>
      )}

      {selectedId && (
        <Section title="Delivery history" action={<Hint>Last 10</Hint>} className="mb-5">
          <DeliveryHistory webhookId={selectedId} />
        </Section>
      )}

      <Dialog open={createOpen} onOpenChange={setCreateOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>New webhook</DialogTitle>
          </DialogHeader>
          <form onSubmit={(e) => void handleCreate(e)} className="mt-3 space-y-4">
            <div>
              <label className="block text-[12px] text-text-muted mb-1.5">Name</label>
              <input
                required
                value={newName}
                onChange={(e) => setNewName(e.target.value)}
                placeholder="My webhook"
                className="w-full px-3 py-2 border border-border-strong rounded-[6px] bg-bg text-[13px] outline-none focus:border-accent"
              />
            </div>
            <div>
              <label className="block text-[12px] text-text-muted mb-1.5">URL</label>
              <input
                required
                type="url"
                value={newUrl}
                onChange={(e) => setNewUrl(e.target.value)}
                placeholder="https://example.com/webhook"
                className="w-full px-3 py-2 border border-border-strong rounded-[6px] bg-bg text-[13px] outline-none focus:border-accent"
              />
            </div>
            <div>
              <label className="block text-[12px] text-text-muted mb-2">Events</label>
              <div className="space-y-2">
                {ALL_WEBHOOK_EVENTS.map(({ value, label, hint }) => (
                  <label key={value} className="flex items-baseline gap-2 cursor-pointer">
                    <input
                      type="checkbox"
                      checked={newEvents.includes(value)}
                      onChange={() => toggleEvent(value)}
                      className="rounded border-border self-center"
                    />
                    <span className="font-mono text-[12px] text-text-muted">{label}</span>
                    <span className="text-[11px] text-text-faint">{hint}</span>
                  </label>
                ))}
              </div>
            </div>
            <div className="flex items-center gap-3">
              <label className="text-[12px] text-text-muted">Active</label>
              <Toggle on={newActive} onToggle={() => setNewActive((v) => !v)} />
            </div>
            {createError && <div className="text-[12.5px] text-bad">{createError}</div>}
            <div className="flex gap-2 justify-end pt-1">
              <GhostBtn type="button" onClick={() => setCreateOpen(false)}>Cancel</GhostBtn>
              <PrimaryBtn type="submit" disabled={createWebhook.isPending || newEvents.length === 0}>
                {createWebhook.isPending ? 'Creating…' : 'Create webhook'}
              </PrimaryBtn>
            </div>
          </form>
        </DialogContent>
      </Dialog>
    </div>
  )
}

// ─── INTEGRATIONS tab ─────────────────────────────────────────────────────────

interface ChannelProviderDef {
  kind: ChannelKind
  name: string
  description: string
}

const CHANNEL_PROVIDERS: ChannelProviderDef[] = [
  { kind: 'slack',   name: 'Slack',   description: 'Post alerts to Slack via incoming webhook.' },
  { kind: 'discord', name: 'Discord', description: 'Post alerts to Discord via incoming webhook.' },
  { kind: 'email',   name: 'Email',   description: 'Email alerts to one or more addresses.' },
]

const COMING_SOON: { id: string; name: string; description: string }[] = [
  { id: 'pagerduty', name: 'PagerDuty', description: 'Route critical alerts to on-call engineers.' },
  { id: 'datadog',   name: 'Datadog',   description: 'Forward metrics and traces to your Datadog account.' },
]

/** Webhook URLs are partially secret and unreadable — show only a tail. */
function maskTarget(kind: ChannelKind, target: string): string {
  if (kind === 'email') return target
  const tail = target.length > 10 ? target.slice(-10) : target
  return `••••${tail}`
}

function ProviderChannelCard({
  provider,
  channels,
  onAdd,
  onDelete,
  deletingId,
}: {
  provider: ChannelProviderDef
  channels: NotificationChannelRow[]
  onAdd: () => void
  onDelete: (id: string) => void
  deletingId: string | null
}) {
  const mine = channels.filter((ch) => ch.kind === provider.kind)
  return (
    <div className="rounded-[8px] border border-border bg-bg-elev p-5 flex flex-col gap-3">
      <div className="flex items-start justify-between gap-3">
        <div>
          <div className="text-[15px] font-medium text-text mb-1">{provider.name}</div>
          <div className="text-[12.5px] text-text-muted leading-relaxed">{provider.description}</div>
        </div>
        {mine.length > 0
          ? <MonoPill variant="good" dot>{mine.length} connected</MonoPill>
          : <MonoPill variant="neutral" dot>Not connected</MonoPill>}
      </div>

      {mine.length > 0 && (
        <div className="rounded-[6px] border border-border overflow-hidden">
          {mine.map((ch) => (
            <div
              key={ch.id}
              className="flex items-center gap-3 px-[12px] py-2.5 border-b border-border last:border-0"
            >
              <div className="min-w-0 flex-1">
                {ch.label && (
                  <div className="font-mono text-[12px] text-text truncate">{ch.label}</div>
                )}
                <div className="font-mono text-[11px] text-text-faint truncate">{maskTarget(ch.kind, ch.target)}</div>
              </div>
              <PermissionGate need="edit">
                <button
                  type="button"
                  onClick={() => onDelete(ch.id)}
                  disabled={deletingId === ch.id}
                  title="Remove channel"
                  className="text-text-faint hover:text-bad transition-colors p-1 disabled:opacity-40 shrink-0"
                >
                  <Trash2 className="h-3.5 w-3.5" />
                </button>
              </PermissionGate>
            </div>
          ))}
        </div>
      )}

      <PermissionGate need="edit">
        <div className="mt-auto">
          <GhostBtn className="text-[12px]" onClick={onAdd}>
            <Plus className="w-3.5 h-3.5" /> Add {provider.name} channel
          </GhostBtn>
        </div>
      </PermissionGate>
    </div>
  )
}

function IntegrationsTab() {
  const { data: channels, isLoading } = useNotificationChannels()
  const deleteChannel = useDeleteChannel()
  const [addKind, setAddKind] = useState<ChannelKind | null>(null)

  const allChannels = channels ?? []

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Integrations"
        description="Connect Spanlens with the tools your team already uses. Channels here receive every alert you configure on the Alerts page."
      />

      {isLoading ? (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
          {[1, 2].map((i) => <div key={i} className="h-40 bg-bg-elev rounded-[8px] animate-pulse" />)}
        </div>
      ) : (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
          {CHANNEL_PROVIDERS.map((provider) => (
            <ProviderChannelCard
              key={provider.kind}
              provider={provider}
              channels={allChannels}
              onAdd={() => setAddKind(provider.kind)}
              onDelete={(id) => void deleteChannel.mutateAsync(id)}
              deletingId={deleteChannel.isPending ? (deleteChannel.variables ?? null) : null}
            />
          ))}

          {COMING_SOON.map((integration) => (
            <div
              key={integration.id}
              className="rounded-[8px] border border-border bg-bg-elev p-5 flex flex-col gap-3 opacity-75"
            >
              <div className="flex items-start justify-between gap-3">
                <div>
                  <div className="text-[15px] font-medium text-text mb-1">{integration.name}</div>
                  <div className="text-[12.5px] text-text-muted leading-relaxed">{integration.description}</div>
                </div>
                <MonoPill variant="faint">coming soon</MonoPill>
              </div>
            </div>
          ))}
        </div>
      )}

      <AddChannelDialog
        open={addKind !== null}
        onOpenChange={(open) => { if (!open) setAddKind(null) }}
        {...(addKind ? { fixedKind: addKind } : {})}
      />
    </div>
  )
}

// ─── DESTINATIONS tab ─────────────────────────────────────────────────────────

function DestinationsTab() {
  const destinations = [
    {
      id: 'bigquery',
      name: 'BigQuery',
      description: 'Export requests and traces to a Google BigQuery dataset for custom analytics.',
      placeholder: 'project-id.dataset_id',
      label: 'Dataset ID',
    },
    {
      id: 's3',
      name: 'Amazon S3',
      description: 'Archive raw request logs to an S3 bucket for long-term retention.',
      placeholder: 's3://my-bucket/spanlens-exports/',
      label: 'Bucket URL',
    },
    {
      id: 'snowflake',
      name: 'Snowflake',
      description: 'Sync data to a Snowflake table for your data warehouse pipelines.',
      placeholder: 'account.region.snowflakecomputing.com',
      label: 'Account URL',
    },
  ]

  return (
    <div className="max-w-[980px]">
      <TabHeader
        title="Destinations"
        description="Export data to external data warehouses and storage systems."
      />

      <div className="space-y-4">
        {destinations.map((dest) => (
          <Section key={dest.id} title={dest.name} description={dest.description} className="mb-0">
            <div className="px-6 pb-5 space-y-4">
              <FormRow label={dest.label}>
                <div className="flex items-center gap-3 w-full max-w-[460px]">
                  <NativeInput
                    placeholder={dest.placeholder}
                    className="flex-1 font-mono text-[12px]"
                  />
                  <GhostBtn>Save configuration</GhostBtn>
                </div>
              </FormRow>
            </div>
          </Section>
        ))}
      </div>
    </div>
  )
}

// ─── OPENTELEMETRY tab ────────────────────────────────────────────────────────

const OTEL_ENDPOINT = 'https://spanlens-server.vercel.app/ingest/traces'

const OTEL_CODE_EXAMPLE = `import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: '${OTEL_ENDPOINT}',
    headers: {
      Authorization: 'Bearer <your-api-key>',
    },
  }),
})

sdk.start()`

function CopyButton({ value }: { value: string }) {
  const [copied, setCopied] = useState(false)
  function handleCopy() {
    void navigator.clipboard.writeText(value).then(() => {
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    })
  }
  return (
    <button
      type="button"
      onClick={handleCopy}
      className="text-[11px] text-text-faint hover:text-text transition-colors shrink-0"
    >
      {copied ? 'Copied!' : 'Copy'}
    </button>
  )
}

function OpenTelemetryTab() {
  const [healthStatus, setHealthStatus] = useState<'idle' | 'ok' | 'error'>('idle')

  async function handleTestConnection() {
    setHealthStatus('idle')
    try {
      const res = await fetch('/health')
      setHealthStatus(res.ok ? 'ok' : 'error')
    } catch {
      setHealthStatus('error')
    }
  }

  return (
    <div className="max-w-[920px]">
      <TabHeader
        title="OpenTelemetry"
        description="Ingest OTLP traces directly from any OpenTelemetry-compatible SDK."
      />

      <Section title="OTLP endpoint" className="mb-5">
        <FormRow label="Endpoint URL" hint="Use this as the OTLP HTTP exporter endpoint.">
          <div className="flex items-center gap-3 w-full max-w-[560px]">
            <div className="flex-1 font-mono text-[12px] text-text bg-bg-muted px-3 py-2 rounded border border-border truncate">
              {OTEL_ENDPOINT}
            </div>
            <CopyButton value={OTEL_ENDPOINT} />
          </div>
        </FormRow>
        <FormRow label="Authentication" hint="Pass your Spanlens API key as a Bearer token.">
          <div className="font-mono text-[12px] text-text-muted">
            <span className="text-text">Authorization:</span> Bearer &lt;your-api-key&gt;
          </div>
        </FormRow>
      </Section>

      <Section title="SDK setup example" className="mb-5">
        <div className="px-6 pb-5">
          <div className="relative">
            <pre className="rounded-[6px] bg-bg-muted border border-border px-4 py-4 font-mono text-[11.5px] text-text-muted overflow-x-auto leading-relaxed whitespace-pre">
              {OTEL_CODE_EXAMPLE}
            </pre>
            <div className="absolute top-3 right-3">
              <CopyButton value={OTEL_CODE_EXAMPLE} />
            </div>
          </div>
        </div>
      </Section>

      <Section title="Connection" className="mb-5">
        <div className="px-6 py-4 flex items-center gap-4">
          <GhostBtn onClick={() => void handleTestConnection()}>
            Test connection
          </GhostBtn>
          {healthStatus === 'ok' && (
            <MonoPill variant="good" dot>Server reachable</MonoPill>
          )}
          {healthStatus === 'error' && (
            <MonoPill variant="faint" dot>Unreachable</MonoPill>
          )}
        </div>
      </Section>
    </div>
  )
}

// ─── tab renderer ─────────────────────────────────────────────────────────────

function TabContent({ tab }: { tab: TabId }) {
  switch (tab) {
    case 'general':       return <GeneralTab />
    case 'members':       return <MembersTab />
    case 'security':      return <SecurityTab />
    case 'audit-log':     return <AuditLogTab />
    case 'system':        return <SystemTab />
    case 'billing':       return <BillingTab />
    case 'plan':          return <PlanLimitsTab />
    case 'invoices':      return <InvoicesTab />
    case 'profile':       return <ProfileTab />
    case 'auth-methods':  return <SignInMethodsTab />
    case 'notifications': return <NotificationsTab />
    case 'preferences':   return <PreferencesTab />
    case 'integrations':  return <IntegrationsTab />
    case 'destinations':  return <DestinationsTab />
    case 'webhooks':      return <WebhooksTab />
    case 'opentelemetry': return <OpenTelemetryTab />
  }
}

// ─── page ─────────────────────────────────────────────────────────────────────

export function SettingsClient() {
  const searchParams = useSearchParams()
  const initialTab = (searchParams.get('tab') as TabId | null) ?? 'general'
  const [tab, setTab] = useState<TabId>(
    ALL_ITEMS.some((i) => i.id === initialTab) ? initialTab : 'general',
  )
  const active = ALL_ITEMS.find((i) => i.id === tab) ?? ALL_ITEMS[0]!

  // Inner-nav search/filter. Empty input shows the full grouped nav.
  const [navSearch, setNavSearch] = useState('')
  const filteredNav = useMemo(() => {
    if (!navSearch.trim()) return NAV
    const needle = navSearch.toLowerCase()
    return NAV
      .map((g) => ({
        ...g,
        items: g.items.filter((i) =>
          i.label.toLowerCase().includes(needle) ||
          g.group.toLowerCase().includes(needle),
        ),
      }))
      .filter((g) => g.items.length > 0)
  }, [navSearch])

  return (
    <div className="-mx-4 -my-4 md:-mx-8 md:-my-7 flex flex-col min-h-screen">
      {/* Sticky Topbar at the true viewport top. Mobile gets the tab
          picker in the right slot so there's no second header row. */}
      <div className="sticky top-0 z-30 bg-bg">
        <Topbar
          crumbs={active.crumbs}
          right={
            <div className="md:hidden min-w-[200px]">
              <Select value={tab} onValueChange={(v) => setTab(v as TabId)}>
                <SelectTrigger className="h-8 rounded-[6px] text-[12.5px] font-sans">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {NAV.map((group) => (
                    <SelectGroup key={group.group}>
                      <SelectLabel>{group.group}</SelectLabel>
                      {group.items.map((item) => (
                        <SelectItem key={item.id} value={item.id}>{item.label}</SelectItem>
                      ))}
                    </SelectGroup>
                  ))}
                </SelectContent>
              </Select>
            </div>
          }
        />
      </div>

      {/* Body: sidebar + content */}
      <div className="flex flex-1 min-h-0">
        {/* Settings inner nav (desktop only) */}
        <aside className="hidden md:flex md:flex-col w-[260px] shrink-0 border-r border-border bg-bg-elev sticky top-[52px] self-start max-h-[calc(100vh-52px)] overflow-y-auto">
          <div className="px-5 py-4 font-mono text-[10px] text-text-faint uppercase tracking-[0.05em] flex items-center justify-between">
            <span>Settings</span>
            <Link
              href="/docs/features/settings"
              className="text-[10px] text-text-faint hover:text-text-muted transition-colors normal-case tracking-normal"
              title="Settings docs"
            >
              Docs →
            </Link>
          </div>
          {/* Nav search */}
          <div className="px-3 pb-3">
            <input
              type="text"
              value={navSearch}
              onChange={(e) => setNavSearch(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Escape') setNavSearch('')
              }}
              placeholder="Filter settings…"
              className="w-full px-2 py-1.5 font-mono text-[11.5px] bg-bg border border-border rounded-[5px] text-text placeholder:text-text-faint focus:outline-none focus:border-border-strong"
            />
          </div>
          {filteredNav.length === 0 ? (
            <div className="px-5 py-2 font-mono text-[11px] text-text-faint">No matches.</div>
          ) : (
            filteredNav.map((group) => (
              <div key={group.group} className="mb-4">
                <div className="px-5 py-1.5 font-mono text-[9.5px] text-text-faint uppercase tracking-[0.05em]">
                  {group.group}
                </div>
                {group.items.map((item) => {
                  const isActive = item.id === tab
                  return (
                    <button
                      key={item.id}
                      type="button"
                      onClick={() => setTab(item.id)}
                      className={cn(
                        'w-full text-left px-5 py-2 text-[13px] transition-colors border-l-2 -ml-px',
                        isActive
                          ? 'border-accent bg-bg text-text font-medium'
                          : 'border-transparent text-text-muted hover:text-text hover:bg-bg/50',
                      )}
                    >
                      {item.label}
                    </button>
                  )
                })}
              </div>
            ))
          )}
        </aside>

        {/* Content area */}
        <main className="flex-1 min-w-0">
          <div className="bg-bg px-4 py-4 md:px-8 md:py-6">
            <TabContent tab={tab} />
          </div>
        </main>
      </div>
    </div>
  )
}
