'use client'

import * as Dialog from '@radix-ui/react-dialog'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {
  Users as UsersIcon,
  Mail,
  ShieldCheck,
  Ticket,
  Search,
  Plus,
  X,
  Sparkles,
  ShieldAlert,
  MoreHorizontal,
  ChevronDown,
  Send,
  RefreshCw,
  Ban,
  AlertTriangle,
  Check,
  SlidersHorizontal,
} from 'lucide-react'
import { useState, useMemo, useCallback, type FormEvent } from 'react'
import { toast } from 'sonner'
import { useApiData } from '../lib/useApiData.js'
import { cmsApi } from '../lib/api.js'
import { Button } from '../components/ui/Button.js'
import { EmptyState } from '../components/ui/EmptyState.js'
import { Skeleton } from '../components/ui/Skeleton.js'
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
import { UserDetailDrawer } from './users/UserDetailDrawer.js'

// ---------------------------------------------------------------------------
// Types (mirror the cms-core serialized shapes)
// ---------------------------------------------------------------------------

interface ApiMember {
  id: string
  name: string
  email: string
  initials: string
  role: string
  roleName: 'Owner' | 'Admin' | 'Editor' | 'Viewer'
  status: 'active' | 'suspended' | 'locked' | 'deactivated'
  mfaEnabled: boolean
  externalDomain: boolean
  lastLoginAt: string | null
  lastActiveAt: string | null
  acceptedInviteAt: string | null
  createdAt: string
  isCurrentUser: boolean
}

interface ApiInvite {
  id: string
  email: string
  role: string
  roleName: 'Owner' | 'Admin' | 'Editor' | 'Viewer'
  status: string
  expiresAt: string
  createdAt: string
  expired: boolean
}

interface ApiStats {
  members: number
  pendingInvites: number
  admins: number
  seatsUsed: number
  seatLimit: number | null
  seatsAvailable: number | null
  emailConfigured: boolean
}

interface ApiRecommendation {
  id: string
  userId: string | null
  inviteId?: string | null
  severity: 'low' | 'medium' | 'high' | 'critical'
  type: string
  title: string
  description: string
  recommendedAction: string
}

type RowStatus = ApiMember['status'] | 'invited' | 'expired'

interface Row {
  kind: 'member' | 'invite'
  id: string
  name: string
  email: string
  initials: string
  role: string
  roleName: 'Owner' | 'Admin' | 'Editor' | 'Viewer'
  status: RowStatus
  mfaEnabled: boolean
  externalDomain: boolean
  /** ISO timestamp used for the "Last active" column. */
  activityAt: string | null
  isCurrentUser: boolean
}

// ---------------------------------------------------------------------------
// Presentation constants (token-driven; brand = violet, matches screenshots)
// ---------------------------------------------------------------------------

const ROLE_PILL: Record<string, string> = {
  Owner: 'bg-brand/10 text-brand',
  Admin: 'bg-info/10 text-info',
  Editor: 'bg-accent text-accent-foreground',
  Viewer: 'bg-muted text-muted-foreground',
}

const STATUS_META: Record<RowStatus, { dot: string; text: string; label: string }> = {
  active: { dot: 'bg-success', text: 'text-success', label: 'Active' },
  invited: { dot: 'bg-warning', text: 'text-warning', label: 'Invited' },
  expired: { dot: 'bg-destructive', text: 'text-destructive', label: 'Expired invite' },
  suspended: { dot: 'bg-muted-foreground', text: 'text-muted-foreground', label: 'Suspended' },
  locked: { dot: 'bg-destructive', text: 'text-destructive', label: 'Locked' },
  deactivated: { dot: 'bg-muted-foreground', text: 'text-muted-foreground', label: 'Deactivated' },
}

const SEVERITY_DOT: Record<ApiRecommendation['severity'], string> = {
  critical: 'bg-destructive',
  high: 'bg-destructive',
  medium: 'bg-warning',
  low: 'bg-info',
}

const AVATAR_TINTS = ['bg-chart-1', 'bg-chart-2', 'bg-chart-3', 'bg-chart-4', 'bg-chart-5'] as const

interface RoleOption {
  value: string
  label: string
  desc: string
}

const BASE_ROLE_OPTIONS: RoleOption[] = [
  {
    value: 'ADMIN',
    label: 'Admin',
    desc: 'Full access except billing and deleting the workspace.',
  },
  {
    value: 'EDITOR',
    label: 'Editor',
    desc: 'Create, edit and publish content. No settings access.',
  },
  { value: 'VIEWER', label: 'Viewer', desc: 'Read-only access to content and analytics.' },
]

const OWNER_ROLE_OPTION: RoleOption = {
  value: 'OWNER',
  label: 'Owner',
  desc: 'Full workspace control, including billing and Owner management.',
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function avatarTint(id: string): string {
  let h = 0
  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0
  return AVATAR_TINTS[h % AVATAR_TINTS.length]!
}

function relativeTime(iso: string | null): string {
  if (!iso) return 'Never'
  const then = new Date(iso).getTime()
  if (Number.isNaN(then)) return '—'
  const diffMs = Date.now() - then
  const mins = Math.floor(diffMs / 60000)
  if (mins < 5) return 'Active now'
  if (mins < 60) return `${mins}m ago`
  const hours = Math.floor(mins / 60)
  if (hours < 24) return `${hours}h ago`
  const days = Math.floor(hours / 24)
  if (days === 1) return 'Yesterday'
  return `${days} days ago`
}

// ---------------------------------------------------------------------------
// Metric cards
// ---------------------------------------------------------------------------

function MetricCard({
  icon,
  label,
  value,
  sub,
}: {
  icon: React.ReactNode
  label: string
  value: React.ReactNode
  sub?: React.ReactNode
}) {
  return (
    <div className="border-border bg-card rounded-lg border p-4">
      <div className="text-muted-foreground flex items-center gap-2">
        {icon}
        <span className="text-sm">{label}</span>
      </div>
      <div className="text-card-foreground mt-2 text-2xl font-medium">{value}</div>
      {sub && <div className="text-muted-foreground mt-0.5 text-xs">{sub}</div>}
    </div>
  )
}

function MetricCards({ stats, loading }: { stats: ApiStats | null; loading: boolean }) {
  if (loading || !stats) {
    return (
      <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
        {Array.from({ length: 4 }, (_, i) => (
          <Skeleton key={i} variant="card" />
        ))}
      </div>
    )
  }
  const seatsValue =
    stats.seatLimit == null ? 'Unlimited' : `${stats.seatsUsed} / ${stats.seatLimit}`
  return (
    <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
      <MetricCard
        icon={<UsersIcon size={16} aria-hidden />}
        label="Members"
        value={stats.members}
      />
      <MetricCard
        icon={<Mail size={16} aria-hidden />}
        label="Pending invites"
        value={stats.pendingInvites}
      />
      <MetricCard
        icon={<ShieldCheck size={16} aria-hidden />}
        label="Admins"
        value={stats.admins}
      />
      <MetricCard
        icon={<Ticket size={16} aria-hidden />}
        label="Seats used"
        value={seatsValue}
        sub={stats.seatLimit == null ? 'No seat limit set' : undefined}
      />
    </div>
  )
}

// ---------------------------------------------------------------------------
// Access review banner
// ---------------------------------------------------------------------------

function AccessReviewBanner({
  recommendations,
  onAct,
  onDismiss,
}: {
  recommendations: ApiRecommendation[]
  onAct: (rec: ApiRecommendation) => void
  onDismiss: (rec: ApiRecommendation) => void
}) {
  if (recommendations.length === 0) return null
  const top = recommendations[0]!
  const more = recommendations.length - 1
  return (
    <div
      className="border-brand/20 bg-brand/5 flex items-start gap-3 rounded-lg border p-4"
      role="status"
      aria-live="polite"
    >
      <span className="bg-brand text-brand-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full">
        <ShieldAlert size={16} aria-hidden />
      </span>
      <div className="min-w-0 flex-1">
        <div className="flex items-center gap-2">
          <span className="text-card-foreground text-sm font-medium">{top.title}</span>
          <span className="bg-brand/10 text-brand inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium">
            <Sparkles size={10} aria-hidden />
            AI
          </span>
          <span className="text-muted-foreground inline-flex items-center gap-1 text-[10px]">
            <span className={`h-1.5 w-1.5 rounded-full ${SEVERITY_DOT[top.severity]}`} />
            {top.severity}
          </span>
        </div>
        <p className="text-muted-foreground mt-0.5 text-sm">{top.description}</p>
        {more > 0 && (
          <p className="text-muted-foreground mt-1 text-xs">
            +{more} more recommendation{more > 1 ? 's' : ''}
          </p>
        )}
      </div>
      <div className="flex shrink-0 items-center gap-2">
        <Button size="sm" variant="primary" onClick={() => onAct(top)}>
          {top.recommendedAction}
        </Button>
        <button
          type="button"
          onClick={() => onDismiss(top)}
          className="text-muted-foreground hover:bg-accent rounded-md p-1.5 transition-colors"
          aria-label="Dismiss recommendation"
        >
          <X size={16} aria-hidden />
        </button>
      </div>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Avatar + row pieces
// ---------------------------------------------------------------------------

function Avatar({ row }: { row: Row }) {
  return (
    <span
      className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-medium text-white ${avatarTint(row.id)}`}
      aria-hidden
    >
      {row.initials}
    </span>
  )
}

function RoleDropdown({
  row,
  options,
  disabled,
  onChange,
}: {
  row: Row
  options: RoleOption[]
  disabled: boolean
  onChange: (role: string) => void
}) {
  const pill = ROLE_PILL[row.roleName] ?? ROLE_PILL.Viewer
  if (disabled) {
    return (
      <span
        className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${pill}`}
      >
        {row.roleName}
      </span>
    )
  }
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button
          type="button"
          className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium transition-opacity hover:opacity-80 ${pill}`}
          aria-label={`Change role for ${row.name}, current role ${row.roleName}`}
        >
          {row.roleName}
          <ChevronDown size={12} aria-hidden />
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content
          align="start"
          sideOffset={4}
          className="border-border bg-card z-50 min-w-44 rounded-lg border p-1 shadow-md"
        >
          {options.map((opt) => (
            <DropdownMenu.Item
              key={opt.value}
              onSelect={() => onChange(opt.value)}
              className="text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
            >
              {opt.label}
              {row.roleName.toUpperCase() === opt.value && (
                <Check size={14} className="text-brand" aria-hidden />
              )}
            </DropdownMenu.Item>
          ))}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

function StatusBadge({ status }: { status: RowStatus }) {
  const meta = STATUS_META[status]
  return (
    <span className={`inline-flex items-center gap-1.5 text-xs ${meta.text}`}>
      <span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
      {meta.label}
    </span>
  )
}

// ---------------------------------------------------------------------------
// Invite modal
// ---------------------------------------------------------------------------

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

function InviteModal({
  open,
  onOpenChange,
  roleOptions,
  emailConfigured,
  seatsFull,
  onInvited,
}: {
  open: boolean
  onOpenChange: (v: boolean) => void
  roleOptions: RoleOption[]
  emailConfigured: boolean
  seatsFull: boolean
  onInvited: () => void
}) {
  const [email, setEmail] = useState('')
  const [role, setRole] = useState('EDITOR')
  const [message, setMessage] = useState('')
  const [expiresInDays, setExpiresInDays] = useState(7)
  const [showAdvanced, setShowAdvanced] = useState(false)
  const [sending, setSending] = useState(false)
  const [emailError, setEmailError] = useState<string | null>(null)

  const reset = useCallback(() => {
    setEmail('')
    setRole('EDITOR')
    setMessage('')
    setExpiresInDays(7)
    setShowAdvanced(false)
    setEmailError(null)
  }, [])

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault()
    if (sending) return
    const trimmed = email.trim()
    if (!EMAIL_RE.test(trimmed)) {
      setEmailError('Enter a valid email address.')
      return
    }
    setSending(true)
    try {
      const res = await cmsApi<{ emailed?: boolean; inviteUrl?: string }>('/users/invite', {
        method: 'POST',
        body: JSON.stringify({
          email: trimmed,
          role,
          message: message.trim() || undefined,
          expiresInDays,
        }),
      })
      if (res.error) {
        toast.error(res.error)
        return
      }
      if (res.data?.emailed) {
        toast.success(`Invitation sent to ${trimmed}`)
      } else if (res.data?.inviteUrl) {
        await navigator.clipboard?.writeText(res.data.inviteUrl).catch(() => {})
        toast.success(
          'Invite created. Link copied — email isn’t configured, so share it manually.',
          {
            duration: 8000,
          },
        )
      } else {
        toast.success('Invitation created.')
      }
      onOpenChange(false)
      reset()
      onInvited()
    } catch {
      toast.error('Could not create the invitation. Please try again.')
    } finally {
      setSending(false)
    }
  }

  return (
    <Dialog.Root
      open={open}
      onOpenChange={(v) => {
        onOpenChange(v)
        if (!v) reset()
      }}
    >
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" />
        <Dialog.Content className="border-border bg-card fixed top-1/2 left-1/2 z-50 max-h-[90vh] w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border p-6 shadow-lg focus:outline-none">
          <div className="mb-4 flex items-center justify-between">
            <Dialog.Title className="text-card-foreground text-lg font-medium">
              Invite a user
            </Dialog.Title>
            <Dialog.Close asChild>
              <button
                type="button"
                className="text-muted-foreground hover:bg-accent rounded-md p-1 transition-colors"
                aria-label="Close"
              >
                <X size={18} aria-hidden />
              </button>
            </Dialog.Close>
          </div>

          {seatsFull && (
            <div className="border-warning/30 bg-warning/10 text-warning mb-4 flex items-start gap-2 rounded-md border p-3 text-sm">
              <AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden />
              <span>
                All seats are in use. Remove a member or raise the seat limit before inviting.
              </span>
            </div>
          )}
          {!emailConfigured && !seatsFull && (
            <div className="border-border bg-muted/50 text-muted-foreground mb-4 flex items-start gap-2 rounded-md border p-3 text-sm">
              <Mail size={16} className="mt-0.5 shrink-0" aria-hidden />
              <span>
                Email isn’t configured — you’ll get a shareable invite link to send manually.
              </span>
            </div>
          )}

          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label
                htmlFor="invite-email"
                className="text-card-foreground mb-1 block text-sm font-medium"
              >
                Email address
              </label>
              <input
                id="invite-email"
                type="email"
                value={email}
                onChange={(e) => {
                  setEmail(e.target.value)
                  if (emailError) setEmailError(null)
                }}
                placeholder="name@company.com"
                autoComplete="off"
                aria-invalid={!!emailError}
                aria-describedby={emailError ? 'invite-email-error' : undefined}
                className="border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
                required
              />
              {emailError && (
                <p id="invite-email-error" className="text-destructive mt-1 text-xs" role="alert">
                  {emailError}
                </p>
              )}
            </div>

            <fieldset>
              <legend className="text-card-foreground mb-2 block text-sm font-medium">Role</legend>
              <div className="space-y-2">
                {roleOptions.map((opt) => {
                  const active = role === opt.value
                  return (
                    <label
                      key={opt.value}
                      className={`flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors ${
                        active ? 'border-brand bg-brand/5' : 'border-border hover:bg-accent/50'
                      }`}
                    >
                      <input
                        type="radio"
                        name="invite-role"
                        value={opt.value}
                        checked={active}
                        onChange={() => setRole(opt.value)}
                        className="accent-brand mt-0.5"
                      />
                      <span className="min-w-0">
                        <span className="text-card-foreground block text-sm font-medium">
                          {opt.label}
                        </span>
                        <span className="text-muted-foreground block text-xs">{opt.desc}</span>
                      </span>
                    </label>
                  )
                })}
              </div>
            </fieldset>

            <div>
              <button
                type="button"
                onClick={() => setShowAdvanced((v) => !v)}
                className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs font-medium transition-colors"
                aria-expanded={showAdvanced}
              >
                <ChevronDown
                  size={14}
                  className={`transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
                  aria-hidden
                />
                Advanced options
              </button>
              {showAdvanced && (
                <div className="mt-3 space-y-3">
                  <div>
                    <label
                      htmlFor="invite-message"
                      className="text-card-foreground mb-1 block text-sm font-medium"
                    >
                      Custom message{' '}
                      <span className="text-muted-foreground font-normal">(optional)</span>
                    </label>
                    <textarea
                      id="invite-message"
                      value={message}
                      onChange={(e) => setMessage(e.target.value)}
                      rows={2}
                      placeholder="Add a personal note to the invitation email."
                      className="border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
                    />
                  </div>
                  <div>
                    <label
                      htmlFor="invite-expiry"
                      className="text-card-foreground mb-1 block text-sm font-medium"
                    >
                      Invite expires in
                    </label>
                    <select
                      id="invite-expiry"
                      value={expiresInDays}
                      onChange={(e) => setExpiresInDays(Number(e.target.value))}
                      className="border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
                    >
                      <option value={7}>7 days</option>
                      <option value={14}>14 days</option>
                      <option value={30}>30 days</option>
                    </select>
                  </div>
                </div>
              )}
            </div>

            <div className="flex items-center justify-end gap-3 pt-2">
              <Dialog.Close asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </Dialog.Close>
              <Button
                type="submit"
                variant="primary"
                loading={sending}
                disabled={sending || seatsFull || !email.trim()}
                leftIcon={!sending ? <Send size={16} aria-hidden /> : undefined}
              >
                {sending ? 'Sending…' : 'Send invite'}
              </Button>
            </div>
          </form>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

// ---------------------------------------------------------------------------
// Main view
// ---------------------------------------------------------------------------

const ROLE_PILLS: Array<'All' | 'Owner' | 'Admin' | 'Editor' | 'Viewer'> = [
  'All',
  'Owner',
  'Admin',
  'Editor',
  'Viewer',
]

export interface UsersProps {
  onNavigate?: (path: string) => void
  embedded?: boolean
}

export function Users({ embedded = false }: UsersProps = {}) {
  const members = useApiData<ApiMember[]>('/users')
  const invites = useApiData<ApiInvite[]>('/invites')
  const stats = useApiData<ApiStats>('/users/stats')
  const review = useApiData<ApiRecommendation[]>('/access-review')

  const [search, setSearch] = useState('')
  const [roleFilter, setRoleFilter] = useState<(typeof ROLE_PILLS)[number]>('All')
  const [showInvite, setShowInvite] = useState(false)
  const [detailUserId, setDetailUserId] = useState<string | null>(null)
  const [confirm, setConfirm] = useState<{
    title: string
    description: string
    confirmLabel: string
    destructive?: boolean
    onConfirm: () => void
  } | null>(null)

  const refetchAll = useCallback(() => {
    members.refetch()
    invites.refetch()
    stats.refetch()
    review.refetch()
  }, [members, invites, stats, review])

  const currentUserRole = useMemo(
    () => (members.data ?? []).find((m) => m.isCurrentUser)?.roleName ?? null,
    [members.data],
  )
  const isOwner = currentUserRole === 'Owner'

  const roleOptions = useMemo(
    () => (isOwner ? [OWNER_ROLE_OPTION, ...BASE_ROLE_OPTIONS] : BASE_ROLE_OPTIONS),
    [isOwner],
  )

  const rows: Row[] = useMemo(() => {
    const memberRows: Row[] = (members.data ?? []).map((m) => ({
      kind: 'member',
      id: m.id,
      name: m.name,
      email: m.email,
      initials: m.initials,
      role: m.role,
      roleName: m.roleName,
      status: m.status,
      mfaEnabled: m.mfaEnabled,
      externalDomain: m.externalDomain,
      activityAt: m.lastActiveAt ?? m.lastLoginAt,
      isCurrentUser: m.isCurrentUser,
    }))
    const inviteRows: Row[] = (invites.data ?? [])
      .filter((i) => i.status === 'PENDING' || i.status === 'EXPIRED')
      .map((i) => ({
        kind: 'invite',
        id: i.id,
        name: i.email.split('@')[0] ?? i.email,
        email: i.email,
        initials: (i.email.slice(0, 2) || '?').toUpperCase(),
        role: i.role,
        roleName: i.roleName,
        status: i.expired || i.status === 'EXPIRED' ? 'expired' : 'invited',
        mfaEnabled: false,
        externalDomain: false,
        activityAt: i.createdAt,
        isCurrentUser: false,
      }))
    return [...memberRows, ...inviteRows]
  }, [members.data, invites.data])

  const filteredRows = useMemo(() => {
    let r = rows
    if (roleFilter !== 'All') r = r.filter((row) => row.roleName === roleFilter)
    if (search.trim()) {
      const q = search.trim().toLowerCase()
      r = r.filter(
        (row) =>
          row.name.toLowerCase().includes(q) ||
          row.email.toLowerCase().includes(q) ||
          row.roleName.toLowerCase().includes(q) ||
          row.status.includes(q),
      )
    }
    return r
  }, [rows, roleFilter, search])

  // -------------------------------------------------------------------------
  // Actions
  // -------------------------------------------------------------------------

  const changeMemberRole = useCallback(
    async (row: Row, role: string) => {
      if (role.toUpperCase() === row.role.toUpperCase()) return
      const endpoint = row.kind === 'invite' ? `/invites/${row.id}` : `/users/${row.id}/role`
      const res = await cmsApi(endpoint, { method: 'PATCH', body: JSON.stringify({ role }) })
      if (res.error) {
        toast.error(res.error)
        return
      }
      toast.success(`Role updated to ${role.charAt(0) + role.slice(1).toLowerCase()}`)
      refetchAll()
    },
    [refetchAll],
  )

  const requestRoleChange = useCallback(
    (row: Row, role: string) => {
      if (role === 'OWNER' || row.roleName === 'Owner') {
        setConfirm({
          title: role === 'OWNER' ? 'Assign Owner role' : 'Change Owner’s role',
          description:
            role === 'OWNER'
              ? `Owners have full control of the workspace, including billing and managing other Owners. Make ${row.name} an Owner?`
              : `This will change ${row.name}'s role away from Owner. Continue?`,
          confirmLabel: 'Change role',
          onConfirm: () => changeMemberRole(row, role),
        })
        return
      }
      changeMemberRole(row, role)
    },
    [changeMemberRole],
  )

  const revokeMember = useCallback(
    (row: Row) => {
      setConfirm({
        title: 'Revoke access',
        description: `${row.name} will lose access immediately and all their sessions will be revoked. Their content and history are preserved. This can be reversed later.`,
        confirmLabel: 'Revoke access',
        destructive: true,
        onConfirm: async () => {
          const res = await cmsApi(`/users/${row.id}`, { method: 'DELETE' })
          if (res.error) {
            toast.error(res.error)
            return
          }
          toast.success(`Access revoked for ${row.name}`)
          refetchAll()
        },
      })
    },
    [refetchAll],
  )

  const cancelInvite = useCallback(
    (row: Row) => {
      setConfirm({
        title: 'Cancel invitation',
        description: `The invitation to ${row.email} will be cancelled and the link will stop working.`,
        confirmLabel: 'Cancel invite',
        destructive: true,
        onConfirm: async () => {
          const res = await cmsApi(`/invites/${row.id}/cancel`, { method: 'POST' })
          if (res.error) {
            toast.error(res.error)
            return
          }
          toast.success('Invitation cancelled')
          refetchAll()
        },
      })
    },
    [refetchAll],
  )

  const resendInvite = useCallback(
    async (row: Row) => {
      const res = await cmsApi<{ emailed?: boolean; inviteUrl?: string }>(
        `/invites/${row.id}/resend`,
        { method: 'POST' },
      )
      if (res.error) {
        toast.error(res.error)
        return
      }
      if (res.data?.emailed) {
        toast.success(`Invitation re-sent to ${row.email}`)
      } else if (res.data?.inviteUrl) {
        await navigator.clipboard?.writeText(res.data.inviteUrl).catch(() => {})
        toast.success('New invite link copied — email isn’t configured, so share it manually.', {
          duration: 8000,
        })
      } else {
        toast.success('Invitation re-sent')
      }
      refetchAll()
    },
    [refetchAll],
  )

  const actOnRecommendation = useCallback(
    (rec: ApiRecommendation) => {
      const target = rows.find((r) => r.id === rec.userId || r.id === rec.inviteId)
      if (rec.type === 'stale_invite' && target) {
        cancelInvite(target)
        return
      }
      if (target) {
        revokeMember(target)
        return
      }
      toast.info('Open the member to action this recommendation.')
    },
    [rows, cancelInvite, revokeMember],
  )

  const dismissRecommendation = useCallback(
    async (rec: ApiRecommendation) => {
      const res = await cmsApi(`/access-review/${encodeURIComponent(rec.id)}/dismiss`, {
        method: 'POST',
      })
      if (res.error) {
        toast.error(res.error)
        return
      }
      toast.success('Recommendation dismissed')
      review.refetch()
    },
    [review],
  )

  // -------------------------------------------------------------------------
  // Render
  // -------------------------------------------------------------------------

  const rootPadding = embedded ? '' : 'p-3 pr-6 sm:p-4 sm:pr-8'
  const loading = members.loading || invites.loading
  const loadError = members.error || invites.error
  const seatsFull = stats.data?.seatsAvailable != null && stats.data.seatsAvailable <= 0
  const peopleCount = (members.data?.length ?? 0) + rows.filter((r) => r.kind === 'invite').length

  return (
    <div className={`space-y-4 ${rootPadding}`}>
      <MetricCards stats={stats.data} loading={stats.loading} />

      {!review.loading && review.data && review.data.length > 0 && (
        <AccessReviewBanner
          recommendations={review.data}
          onAct={actOnRecommendation}
          onDismiss={dismissRecommendation}
        />
      )}

      <div className="border-border bg-card rounded-lg border">
        {/* Header */}
        <div className="border-border flex flex-col gap-3 border-b p-4 lg:flex-row lg:items-center lg:justify-between">
          <div>
            <h2 className="text-card-foreground text-lg font-medium">Team members</h2>
            <p className="text-muted-foreground text-sm">
              {peopleCount} {peopleCount === 1 ? 'person has' : 'people have'} access to this
              workspace
            </p>
          </div>
          <div className="flex items-center gap-2">
            <div className="relative">
              <Search
                size={16}
                className="text-muted-foreground absolute top-1/2 left-3 -translate-y-1/2"
                aria-hidden
              />
              <input
                type="text"
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Search by name or email…"
                aria-label="Search team members"
                className="border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none lg:w-64"
              />
            </div>
            <Button
              variant="primary"
              onClick={() => setShowInvite(true)}
              disabled={seatsFull}
              title={seatsFull ? 'All seats are in use' : undefined}
              leftIcon={<Plus size={16} aria-hidden />}
            >
              Invite user
            </Button>
          </div>
        </div>

        {/* Role filter pills */}
        <div
          className="border-border flex flex-wrap gap-2 border-b px-4 py-3"
          role="group"
          aria-label="Filter by role"
        >
          {ROLE_PILLS.map((pill) => {
            const active = roleFilter === pill
            return (
              <button
                key={pill}
                type="button"
                onClick={() => setRoleFilter(pill)}
                aria-pressed={active}
                className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
                  active
                    ? 'bg-brand text-brand-foreground'
                    : 'bg-muted text-muted-foreground hover:bg-accent'
                }`}
              >
                {pill}
              </button>
            )
          })}
        </div>

        {/* Table / states */}
        {loading ? (
          <Skeleton variant="table-row" lines={5} className="p-2" />
        ) : loadError ? (
          <div className="flex items-center gap-3 p-4">
            <AlertTriangle size={18} className="text-destructive shrink-0" aria-hidden />
            <span className="text-destructive flex-1 text-sm">{loadError}</span>
            <Button variant="outline" size="sm" onClick={refetchAll}>
              Retry
            </Button>
          </div>
        ) : rows.length === 0 ? (
          <EmptyState
            icon={<UsersIcon size={24} aria-hidden />}
            title="No team members yet"
            description="Invite your first teammate to collaborate in this workspace."
            actionLabel="Invite user"
            onAction={() => setShowInvite(true)}
          />
        ) : filteredRows.length === 0 ? (
          <EmptyState
            icon={<Search size={24} aria-hidden />}
            title="No users match your filters"
            description="Try a different search or role filter."
            actionLabel="Clear filters"
            onAction={() => {
              setSearch('')
              setRoleFilter('All')
            }}
          />
        ) : (
          <div className="overflow-x-auto">
            <table className="w-full min-w-[640px]">
              <thead>
                <tr className="border-border text-muted-foreground border-b text-left text-xs">
                  <th className="px-4 py-2 font-medium">Member</th>
                  <th className="px-4 py-2 font-medium">Role</th>
                  <th className="px-4 py-2 font-medium">Status</th>
                  <th className="px-4 py-2 font-medium">Last active</th>
                  <th className="px-4 py-2 font-medium">
                    <span className="sr-only">Actions</span>
                  </th>
                </tr>
              </thead>
              <tbody className="divide-border divide-y">
                {filteredRows.map((row) => {
                  const isInvite = row.kind === 'invite'
                  // Last Owner / self protections are enforced server-side; here we
                  // only disable the obviously-invalid affordances.
                  const roleChangeDisabled =
                    (row.roleName === 'Owner' && !isOwner) ||
                    (row.roleName === 'Owner' && row.isCurrentUser)
                  const openDetail = () => {
                    if (!isInvite) setDetailUserId(row.id)
                  }
                  return (
                    <tr
                      key={`${row.kind}-${row.id}`}
                      className={`hover:bg-accent/30 transition-colors ${
                        isInvite ? '' : 'cursor-pointer'
                      }`}
                      {...(isInvite
                        ? {}
                        : {
                            role: 'button',
                            tabIndex: 0,
                            'aria-label': `Open details for ${row.name}`,
                            onClick: openDetail,
                            onKeyDown: (e: React.KeyboardEvent) => {
                              if (e.key === 'Enter' || e.key === ' ') {
                                e.preventDefault()
                                openDetail()
                              }
                            },
                          })}
                    >
                      <td className="px-4 py-3">
                        <div className="flex items-center gap-3">
                          <Avatar row={row} />
                          <div className="min-w-0">
                            <div className="text-card-foreground flex items-center gap-1.5 text-sm font-medium">
                              <span className="truncate">{row.name}</span>
                              {row.isCurrentUser && (
                                <span className="bg-muted text-muted-foreground rounded px-1 text-[10px]">
                                  You
                                </span>
                              )}
                              {row.externalDomain && (
                                <span className="bg-warning/10 text-warning rounded px-1 text-[10px]">
                                  External
                                </span>
                              )}
                            </div>
                            <div className="text-muted-foreground truncate text-xs">
                              {row.email}
                            </div>
                          </div>
                        </div>
                      </td>
                      <td
                        className="px-4 py-3"
                        onClick={(e) => e.stopPropagation()}
                        role="presentation"
                      >
                        <RoleDropdown
                          row={row}
                          options={roleOptions}
                          disabled={roleChangeDisabled}
                          onChange={(role) => requestRoleChange(row, role)}
                        />
                      </td>
                      <td className="px-4 py-3">
                        <StatusBadge status={row.status} />
                      </td>
                      <td className="text-muted-foreground px-4 py-3 text-sm">
                        {isInvite
                          ? `Invited ${relativeTime(row.activityAt)}`
                          : relativeTime(row.activityAt)}
                      </td>
                      <td
                        className="px-4 py-3 text-right"
                        onClick={(e) => e.stopPropagation()}
                        role="presentation"
                      >
                        <RowActions
                          row={row}
                          isInvite={isInvite}
                          onResend={() => resendInvite(row)}
                          onCancelInvite={() => cancelInvite(row)}
                          onRevoke={() => revokeMember(row)}
                          onViewDetails={isInvite ? undefined : () => setDetailUserId(row.id)}
                          canRevoke={!row.isCurrentUser}
                        />
                      </td>
                    </tr>
                  )
                })}
              </tbody>
            </table>
          </div>
        )}
      </div>

      <InviteModal
        open={showInvite}
        onOpenChange={setShowInvite}
        roleOptions={roleOptions}
        emailConfigured={stats.data?.emailConfigured ?? true}
        seatsFull={!!seatsFull}
        onInvited={refetchAll}
      />

      {confirm && (
        <ConfirmDialog
          open={!!confirm}
          onClose={() => setConfirm(null)}
          onConfirm={confirm.onConfirm}
          title={confirm.title}
          description={confirm.description}
          confirmLabel={confirm.confirmLabel}
          destructive={confirm.destructive}
        />
      )}

      <UserDetailDrawer
        userId={detailUserId}
        open={detailUserId !== null}
        onOpenChange={(v) => {
          if (!v) setDetailUserId(null)
        }}
        onChanged={refetchAll}
      />
    </div>
  )
}

function RowActions({
  row,
  isInvite,
  onResend,
  onCancelInvite,
  onRevoke,
  onViewDetails,
  canRevoke,
}: {
  row: Row
  isInvite: boolean
  onResend: () => void
  onCancelInvite: () => void
  onRevoke: () => void
  onViewDetails?: () => void
  canRevoke: boolean
}) {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button
          type="button"
          className="text-muted-foreground hover:bg-accent rounded-md p-1.5 transition-colors"
          aria-label={`Actions for ${row.name}`}
        >
          <MoreHorizontal size={16} aria-hidden />
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content
          align="end"
          sideOffset={4}
          className="border-border bg-card z-50 min-w-44 rounded-lg border p-1 shadow-md"
        >
          {isInvite ? (
            <>
              <DropdownMenu.Item
                onSelect={onResend}
                className="text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
              >
                <RefreshCw size={14} aria-hidden />
                Resend invite
              </DropdownMenu.Item>
              <DropdownMenu.Item
                onSelect={onCancelInvite}
                className="text-destructive data-highlighted:bg-destructive/10 flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
              >
                <Ban size={14} aria-hidden />
                Cancel invite
              </DropdownMenu.Item>
            </>
          ) : (
            <>
              {onViewDetails && (
                <DropdownMenu.Item
                  onSelect={onViewDetails}
                  className="text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
                >
                  <SlidersHorizontal size={14} aria-hidden />
                  View details
                </DropdownMenu.Item>
              )}
              <DropdownMenu.Item
                onSelect={canRevoke ? onRevoke : undefined}
                disabled={!canRevoke}
                className="text-destructive data-highlighted:bg-destructive/10 flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none data-disabled:cursor-not-allowed data-disabled:opacity-50"
              >
                <Ban size={14} aria-hidden />
                Revoke access
              </DropdownMenu.Item>
            </>
          )}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
