import { useCallback, useEffect, useMemo, useState } from "react"
import {
  CheckCircle2,
  Eye,
  Mail,
  Pause,
  Play,
  Plus,
  RefreshCw,
  Send,
  Trash2,
  UserPlus,
  X,
} from "lucide-react"

import { useArcadiaClient } from "@crema/arcadia-core-client"
import { useToast } from "@crema/notification-ui"
import {
  ActionsCell,
  BadgeCell,
  DataTable,
  DateCell,
  Pagination,
  useTable,
  type ActionItem,
  type BadgeTone,
  type Column,
} from "@crema/table-ui"
import { SearchInput } from "@crema/search-ui"
import { ConfirmDialog, EmptyState } from "@crema/feedback-ui"

import { AppShell } from "~/components/layout/app-shell"
import { DataState, DialogError } from "~/components/data-state"
import { errorMessage } from "~/lib/errors"
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardHeader } from "~/components/ui/card"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog"
import { Input } from "~/components/ui/input"
import { Label } from "~/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "~/components/ui/select"
import { Textarea } from "~/components/ui/textarea"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import {
  createInvitation,
  invitationStatus,
  listInvitations,
  resendInvitation,
  revokeInvitation,
  type Invitation,
  type InvitationStatus,
} from "~/lib/arcadia/invitations"
import {
  createRole,
  deleteRole,
  listRoles,
  updateRole,
  type Role,
  type RoleInput,
} from "~/lib/arcadia/roles"
import {
  createUser,
  deleteUser,
  listUsers,
  setUserStatus,
  updateUser,
  type User,
  type UserInput,
  type UserStatus,
} from "~/lib/arcadia/users"
import { pageTitle } from "~/lib/page-meta"
import { useSession } from "~/lib/session"
import { useRegisterContext } from "@crema/aifirst-ui/context"
import { UserDetailSheet } from "~/components/users/user-detail-sheet"

export const meta = () => pageTitle("Users")

type Tab = "users" | "invitations" | "roles"

export default function UsersRoute() {
  const session = useSession()
  const arcadia = useArcadiaClient()

  const [tab, setTab] = useState<Tab>("users")

  // Three independent lists, three independent load errors. A broken /roles
  // call must not make the users table claim there are no users.
  const [users, setUsers] = useState<User[]>([])
  const [usersLoading, setUsersLoading] = useState(true)
  const [usersError, setUsersError] = useState<unknown>(null)

  const [invitations, setInvitations] = useState<Invitation[]>([])
  const [invitationsLoading, setInvitationsLoading] = useState(true)
  const [invitationsError, setInvitationsError] = useState<unknown>(null)

  const [roles, setRoles] = useState<Role[]>([])
  const [rolesLoading, setRolesLoading] = useState(true)
  const [rolesError, setRolesError] = useState<unknown>(null)

  const refreshUsers = useCallback(async () => {
    setUsersError(null)
    setUsersLoading(true)
    try {
      setUsers(await listUsers(arcadia))
    } catch (err) {
      // Raw throw: describeError() reads the status off it.
      setUsersError(err)
    } finally {
      setUsersLoading(false)
    }
  }, [arcadia])

  const refreshInvitations = useCallback(async () => {
    setInvitationsError(null)
    setInvitationsLoading(true)
    try {
      setInvitations(await listInvitations(arcadia))
    } catch (err) {
      setInvitationsError(err)
    } finally {
      setInvitationsLoading(false)
    }
  }, [arcadia])

  const refreshRoles = useCallback(async () => {
    setRolesError(null)
    setRolesLoading(true)
    try {
      setRoles(await listRoles(arcadia))
    } catch (err) {
      setRolesError(err)
    } finally {
      setRolesLoading(false)
    }
  }, [arcadia])

  useEffect(() => {
    if (!session) return
    refreshUsers()
    refreshInvitations()
    refreshRoles()
  }, [session, refreshUsers, refreshInvitations, refreshRoles])

  const summary = useMemo(
    () => ({
      users: {
        total: users.length,
        byStatus: countBy(users, (u) => u.status),
        verified: users.filter((u) => u.email_verified).length,
      },
      invitations: {
        total: invitations.length,
        byStatus: countBy(invitations, invitationStatus),
      },
      roles: {
        total: roles.length,
        system: roles.filter((r) => r.is_system).length,
      },
    }),
    [users, invitations, roles],
  )
  useRegisterContext("users", summary)

  return (
    <AppShell>
      <div className="flex flex-col gap-4">
        <header className="flex flex-col gap-2">
          <h1 className="text-2xl font-semibold tracking-tight">Users</h1>
          <p className="text-sm text-muted-foreground">
            Members, pending invitations, and roles for the platform-admin tenant.
          </p>
        </header>

        <Tabs value={tab} onValueChange={(v) => setTab(v as Tab)}>
          <TabsList>
            <TabsTrigger value="users" data-action="users-tab-users">
              Users {users.length ? `(${users.length})` : ""}
            </TabsTrigger>
            <TabsTrigger value="invitations" data-action="users-tab-invitations">
              Invitations {invitations.length ? `(${invitations.length})` : ""}
            </TabsTrigger>
            <TabsTrigger value="roles" data-action="users-tab-roles">
              Roles {roles.length ? `(${roles.length})` : ""}
            </TabsTrigger>
          </TabsList>

          <TabsContent value="users">
            <UsersPanel
              users={users}
              roles={roles}
              loading={usersLoading}
              error={usersError}
              onRefresh={refreshUsers}
            />
          </TabsContent>
          <TabsContent value="invitations">
            <InvitationsPanel
              invitations={invitations}
              roles={roles}
              loading={invitationsLoading}
              error={invitationsError}
              onRefresh={refreshInvitations}
            />
          </TabsContent>
          <TabsContent value="roles">
            <RolesPanel
              roles={roles}
              loading={rolesLoading}
              error={rolesError}
              onRefresh={refreshRoles}
            />
          </TabsContent>
        </Tabs>
      </div>
    </AppShell>
  )
}

// --- Users tab ----------------------------------------------------------

function UsersPanel({
  users,
  roles,
  loading,
  error,
  onRefresh,
}: {
  users: User[]
  roles: Role[]
  loading: boolean
  error: unknown
  onRefresh: () => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const toast = useToast()
  const [search, setSearch] = useState("")
  const [statusFilter, setStatusFilter] = useState<"all" | UserStatus>("all")
  const [editor, setEditor] = useState<{ mode: "create" } | { mode: "edit"; user: User } | null>(null)
  const [pendingDelete, setPendingDelete] = useState<User | null>(null)
  const [detailUser, setDetailUser] = useState<User | null>(null)

  const filtered = useMemo(
    () => (statusFilter === "all" ? users : users.filter((u) => u.status === statusFilter)),
    [users, statusFilter],
  )

  const columns = useMemo<Column<User>[]>(
    () => [
      {
        id: "email",
        header: "Email",
        accessor: "email",
        sortable: true,
        cell: (u) => (
          <span className="flex items-center gap-2">
            <span className="font-medium">{u.email}</span>
            {u.email_verified ? (
              <CheckCircle2 className="size-3.5 text-emerald-500" aria-label="Verified" />
            ) : null}
          </span>
        ),
      },
      {
        id: "name",
        header: "Name",
        accessor: "full_name",
        sortable: true,
        cell: (u) => <span>{u.full_name || "—"}</span>,
      },
      {
        id: "status",
        header: "Status",
        accessor: "status",
        sortable: true,
        cell: (u) => <BadgeCell label={u.status} tone={userStatusTone(u.status)} />,
      },
      {
        id: "roles",
        header: "Roles",
        cell: (u) =>
          u.roles.length === 0 ? (
            <span className="text-muted-foreground">—</span>
          ) : (
            <div className="flex flex-wrap gap-1">
              {u.roles.map((r) => (
                <Badge key={r.id} variant="secondary" className="font-mono text-xs">
                  {r.slug}
                </Badge>
              ))}
            </div>
          ),
      },
      {
        id: "last_sign_in",
        header: "Last sign-in",
        accessor: "last_sign_in_at",
        sortable: true,
        cell: (u) =>
          u.last_sign_in_at ? (
            <DateCell value={u.last_sign_in_at} format="short" />
          ) : (
            <span className="text-muted-foreground">never</span>
          ),
      },
      {
        id: "created",
        header: "Created",
        accessor: "inserted_at",
        sortable: true,
        cell: (u) => <DateCell value={u.inserted_at} format="short" />,
      },
      {
        id: "actions",
        header: "",
        align: "right",
        cell: (u) => (
          <ActionsCell
            items={userRowActions(u, {
              arcadia,
              refresh: onRefresh,
              setEditor,
              setPendingDelete,
              setDetailUser,
              toast,
            })}
            triggerDataAction={`user-${u.id}-actions`}
          />
        ),
      },
    ],
    [arcadia, onRefresh, toast],
  )

  const table = useTable<User>({
    data: filtered,
    columns,
    getRowId: (u) => u.id,
    initialPageSize: 25,
    initialSearch: search,
  })
  useEffect(() => {
    table.setSearch(search)
  }, [search, table])

  return (
    <Card>
      <CardHeader className="flex flex-row flex-wrap items-center gap-3">
        <SearchInput
          value={search}
          onValueChange={setSearch}
          placeholder="Search by email or name"
          data-action="users-search"
          className="max-w-sm flex-1"
        />
        <Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as typeof statusFilter)}>
          <SelectTrigger className="w-40" data-action="users-status-filter">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="all">All statuses</SelectItem>
            <SelectItem value="active">Active</SelectItem>
            <SelectItem value="inactive">Inactive</SelectItem>
            <SelectItem value="suspended">Suspended</SelectItem>
          </SelectContent>
        </Select>
        <div className="ml-auto flex items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={onRefresh}
            disabled={loading}
            data-action="users-refresh"
          >
            <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
            Refresh
          </Button>
          <Button size="sm" onClick={() => setEditor({ mode: "create" })} data-action="users-create">
            <UserPlus className="size-4" />
            New user
          </Button>
        </div>
      </CardHeader>

      <CardContent className="relative p-0">
        <DataState
          loading={loading}
          error={error}
          isEmpty={table.total === 0}
          onRetry={onRefresh}
          loadingLabel="Loading users…"
          empty={
            <EmptyState
              title={
                search || statusFilter !== "all" ? "No users match those filters." : "No users yet."
              }
              description={
                search || statusFilter !== "all"
                  ? "Try a different search or status filter."
                  : "Invite your first user from the Invitations tab."
              }
              className="py-12"
            />
          }
        >
          <DataTable
            columns={columns}
            rows={table.pageRows}
            getRowId={(u) => u.id}
            sort={table.sort}
            onSortToggle={table.toggleSort}
            loading={loading && users.length > 0}
            stickyHeader
          />
          <Pagination
            page={table.page}
            pageSize={table.pageSize}
            total={table.total}
            onPageChange={table.setPage}
            onPageSizeChange={table.setPageSize}
          />
        </DataState>
      </CardContent>

      <ConfirmDialog
        open={pendingDelete !== null}
        onOpenChange={(o) => !o && setPendingDelete(null)}
        title="Delete user?"
        description={
          pendingDelete
            ? `${pendingDelete.email} will be permanently removed and can no longer sign in. Their objects and audit history are kept. Suspend instead if this is temporary.`
            : ""
        }
        confirmLabel="Delete"
        variant="danger"
        onConfirm={async () => {
          if (!pendingDelete) return
          const email = pendingDelete.email
          try {
            await deleteUser(arcadia, pendingDelete.id)
            setPendingDelete(null)
            await onRefresh()
            toast.success(`Deleted ${email}`)
          } catch (err) {
            setPendingDelete(null)
            toast.error(errorMessage(err, `delete ${email}`))
          }
        }}
      />

      <UserEditorDialog
        state={editor}
        roles={roles}
        onClose={() => setEditor(null)}
        onSaved={async (message) => {
          setEditor(null)
          await onRefresh()
          toast.success(message)
        }}
      />

      <UserDetailSheet
        user={detailUser}
        roles={roles}
        onClose={() => setDetailUser(null)}
        onChanged={async () => {
          await onRefresh()
          // Re-sync the open detail with the freshly fetched user list.
          setDetailUser((cur) =>
            cur ? users.find((u) => u.id === cur.id) ?? cur : cur,
          )
        }}
      />
    </Card>
  )
}

function userStatusTone(status: UserStatus): BadgeTone {
  if (status === "active") return "success"
  if (status === "suspended") return "danger"
  return "default"
}

function userRowActions(
  u: User,
  ctx: {
    arcadia: ReturnType<typeof useArcadiaClient>
    refresh: () => Promise<void>
    setEditor: (s: { mode: "edit"; user: User } | null) => void
    setPendingDelete: (u: User | null) => void
    setDetailUser: (u: User | null) => void
    toast: ReturnType<typeof useToast>
  },
): ActionItem[] {
  const { arcadia, refresh, setEditor, setPendingDelete, setDetailUser, toast } = ctx
  const items: ActionItem[] = []

  items.push({
    id: "view",
    label: "View",
    icon: <Eye className="size-4" />,
    dataAction: `user-${u.id}-view`,
    onSelect: () => setDetailUser(u),
  })

  items.push({
    id: "edit",
    label: "Edit",
    dataAction: `user-${u.id}-edit`,
    onSelect: () => setEditor({ mode: "edit", user: u }),
  })

  if (u.status === "active") {
    items.push({
      id: "suspend",
      label: "Suspend",
      icon: <Pause className="size-4" />,
      dataAction: `user-${u.id}-suspend`,
      onSelect: async () => {
        try {
          await setUserStatus(arcadia, u.id, "suspended")
          await refresh()
          toast.success(`Suspended ${u.email}`)
        } catch (err) {
          toast.error(errorMessage(err, `suspend ${u.email}`))
        }
      },
    })
  } else {
    items.push({
      id: "activate",
      label: "Activate",
      icon: <Play className="size-4" />,
      dataAction: `user-${u.id}-activate`,
      onSelect: async () => {
        try {
          await setUserStatus(arcadia, u.id, "active")
          await refresh()
          toast.success(`Activated ${u.email}`)
        } catch (err) {
          toast.error(errorMessage(err, `activate ${u.email}`))
        }
      },
    })
  }

  items.push({
    id: "delete",
    label: "Delete",
    icon: <Trash2 className="size-4" />,
    destructive: true,
    dataAction: `user-${u.id}-delete`,
    onSelect: () => setPendingDelete(u),
  })

  return items
}

function UserEditorDialog({
  state,
  roles,
  onClose,
  onSaved,
}: {
  state: { mode: "create" } | { mode: "edit"; user: User } | null
  roles: Role[]
  onClose: () => void
  onSaved: (message: string) => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const open = state !== null
  const isEdit = state?.mode === "edit"
  const initial = isEdit ? state.user : null

  const [email, setEmail] = useState("")
  const [firstName, setFirstName] = useState("")
  const [lastName, setLastName] = useState("")
  const [status, setStatus] = useState<UserStatus>("active")
  const [password, setPassword] = useState("")
  const [selectedRoleIds, setSelectedRoleIds] = useState<Set<string>>(new Set())
  const [saving, setSaving] = useState(false)
  // The failure belongs where the operator is looking — inside this dialog,
  // with the form still filled in — not behind the modal scrim.
  const [error, setError] = useState<unknown>(null)

  useEffect(() => {
    if (!open) return
    setError(null)
    setSaving(false)
    if (initial) {
      setEmail(initial.email)
      setFirstName(initial.first_name ?? "")
      setLastName(initial.last_name ?? "")
      setStatus(initial.status)
      setPassword("")
      setSelectedRoleIds(new Set(initial.roles.map((r) => r.id)))
    } else {
      setEmail("")
      setFirstName("")
      setLastName("")
      setStatus("active")
      setPassword("")
      setSelectedRoleIds(new Set())
    }
  }, [open, initial])

  const toggleRole = (id: string) => {
    setSelectedRoleIds((prev) => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id)
      else next.add(id)
      return next
    })
  }

  const submit = async () => {
    setError(null)
    setSaving(true)
    try {
      const input: UserInput = {
        email,
        first_name: firstName || null,
        last_name: lastName || null,
        status,
        role_ids: Array.from(selectedRoleIds),
      }
      if (password) input.password = password

      if (isEdit && initial) {
        await updateUser(arcadia, initial.id, input)
        await onSaved(`Saved ${initial.email}`)
      } else {
        await createUser(arcadia, input)
        await onSaved(`Created ${email.trim()}`)
      }
    } catch (err) {
      // Keep the dialog open and the form intact so it can be fixed and resubmitted.
      setError(err)
      setSaving(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>{isEdit ? "Edit user" : "New user"}</DialogTitle>
          <DialogDescription>
            {isEdit
              ? "Update the user's profile, status, and role assignments."
              : "Create a user directly. To send a sign-up email, use the Invitations tab instead."}
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          <div className="flex flex-col gap-1.5">
            <Label htmlFor="user-email">Email</Label>
            <Input
              id="user-email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="user@example.com"
              data-action="user-form-email"
              disabled={isEdit}
            />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="user-first-name">First name</Label>
              <Input
                id="user-first-name"
                value={firstName}
                onChange={(e) => setFirstName(e.target.value)}
                data-action="user-form-first-name"
              />
            </div>
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="user-last-name">Last name</Label>
              <Input
                id="user-last-name"
                value={lastName}
                onChange={(e) => setLastName(e.target.value)}
                data-action="user-form-last-name"
              />
            </div>
          </div>

          <div className="flex flex-col gap-1.5">
            <Label>Status</Label>
            <Select value={status} onValueChange={(v) => setStatus(v as UserStatus)}>
              <SelectTrigger data-action="user-form-status">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="active">Active</SelectItem>
                <SelectItem value="inactive">Inactive</SelectItem>
                <SelectItem value="suspended">Suspended</SelectItem>
              </SelectContent>
            </Select>
          </div>

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="user-password">
              {isEdit ? "Reset password (leave blank to keep current)" : "Password"}
            </Label>
            <Input
              id="user-password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder={isEdit ? "•••••• (unchanged)" : ""}
              data-action="user-form-password"
            />
          </div>

          <div className="flex flex-col gap-1.5">
            <Label>Roles</Label>
            {roles.length === 0 ? (
              <p className="text-xs text-muted-foreground">
                No roles defined. Create one in the Roles tab.
              </p>
            ) : (
              <div className="flex flex-wrap gap-1.5 rounded-md border p-2">
                {roles.map((r) => {
                  const selected = selectedRoleIds.has(r.id)
                  return (
                    <button
                      key={r.id}
                      type="button"
                      data-action={`user-form-role-${r.slug}`}
                      onClick={() => toggleRole(r.id)}
                      className={[
                        "rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
                        selected
                          ? "border-primary bg-primary/10 text-primary"
                          : "border-border text-muted-foreground hover:bg-accent",
                      ].join(" ")}
                    >
                      {r.name}
                    </button>
                  )
                })}
              </div>
            )}
          </div>
        </div>

        {error ? (
          <DialogError error={error} context={isEdit ? "save the user" : "create the user"} />
        ) : null}

        <DialogFooter>
          <Button variant="outline" onClick={onClose} disabled={saving} data-action="user-form-cancel">
            Cancel
          </Button>
          <Button
            onClick={submit}
            disabled={saving || email.trim() === "" || (!isEdit && password.length === 0)}
            data-action="user-form-save"
          >
            {saving ? <RefreshCw className="size-4 animate-spin" /> : <CheckCircle2 className="size-4" />}
            {isEdit ? "Save changes" : "Create user"}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// --- Invitations tab ----------------------------------------------------

function InvitationsPanel({
  invitations,
  roles,
  loading,
  error,
  onRefresh,
}: {
  invitations: Invitation[]
  roles: Role[]
  loading: boolean
  error: unknown
  onRefresh: () => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const toast = useToast()
  const [search, setSearch] = useState("")
  const [inviteOpen, setInviteOpen] = useState(false)
  const [pendingRevoke, setPendingRevoke] = useState<Invitation | null>(null)

  const columns = useMemo<Column<Invitation>[]>(
    () => [
      {
        id: "email",
        header: "Email",
        accessor: "email",
        sortable: true,
        cell: (i) => <span className="font-medium">{i.email}</span>,
      },
      {
        id: "role",
        header: "Role",
        accessor: (i) => i.role.name,
        sortable: true,
        cell: (i) => (
          <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{i.role.slug}</code>
        ),
      },
      {
        id: "invited_by",
        header: "Invited by",
        cell: (i) => (
          <span className="text-muted-foreground">{i.invited_by?.email ?? "—"}</span>
        ),
      },
      {
        id: "status",
        header: "Status",
        cell: (i) => {
          const s = invitationStatus(i)
          return <BadgeCell label={s} tone={invitationTone(s)} />
        },
      },
      {
        id: "expires_at",
        header: "Expires",
        accessor: "expires_at",
        sortable: true,
        cell: (i) =>
          i.expires_at ? <DateCell value={i.expires_at} format="short" /> : <span>—</span>,
      },
      {
        id: "actions",
        header: "",
        align: "right",
        cell: (i) => (
          <ActionsCell
            items={invitationRowActions(i, {
              arcadia,
              refresh: onRefresh,
              setPendingRevoke,
              toast,
            })}
            triggerDataAction={`invitation-${i.id}-actions`}
          />
        ),
      },
    ],
    [arcadia, onRefresh, toast],
  )

  const table = useTable<Invitation>({
    data: invitations,
    columns,
    getRowId: (i) => i.id,
    initialPageSize: 25,
    initialSearch: search,
  })
  useEffect(() => {
    table.setSearch(search)
  }, [search, table])

  return (
    <Card>
      <CardHeader className="flex flex-row items-center gap-3">
        <SearchInput
          value={search}
          onValueChange={setSearch}
          placeholder="Search by email or role"
          data-action="invitations-search"
          className="max-w-sm flex-1"
        />
        <div className="ml-auto flex items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={onRefresh}
            disabled={loading}
            data-action="invitations-refresh"
          >
            <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
            Refresh
          </Button>
          <Button
            size="sm"
            onClick={() => setInviteOpen(true)}
            disabled={roles.length === 0}
            data-action="invitations-create"
          >
            <Mail className="size-4" />
            Invite user
          </Button>
        </div>
      </CardHeader>

      <CardContent className="relative p-0">
        <DataState
          loading={loading}
          error={error}
          isEmpty={table.total === 0}
          onRetry={onRefresh}
          loadingLabel="Loading invitations…"
          empty={
            <EmptyState
              title={search ? "No invitations match." : "No invitations yet."}
              description={
                search
                  ? "Try a different search."
                  : roles.length === 0
                    ? "Create a role first, then invite users."
                    : "Invite your first user."
              }
              className="py-12"
            />
          }
        >
          <DataTable
            columns={columns}
            rows={table.pageRows}
            getRowId={(i) => i.id}
            sort={table.sort}
            onSortToggle={table.toggleSort}
            loading={loading && invitations.length > 0}
            stickyHeader
          />
          <Pagination
            page={table.page}
            pageSize={table.pageSize}
            total={table.total}
            onPageChange={table.setPage}
            onPageSizeChange={table.setPageSize}
          />
        </DataState>
      </CardContent>

      <ConfirmDialog
        open={pendingRevoke !== null}
        onOpenChange={(o) => !o && setPendingRevoke(null)}
        title="Revoke invitation?"
        description={
          pendingRevoke
            ? `The link sent to ${pendingRevoke.email} stops working immediately, and they can't accept it. You can invite them again later.`
            : ""
        }
        confirmLabel="Revoke"
        variant="danger"
        onConfirm={async () => {
          if (!pendingRevoke) return
          const email = pendingRevoke.email
          try {
            await revokeInvitation(arcadia, pendingRevoke.id)
            setPendingRevoke(null)
            await onRefresh()
            toast.success(`Revoked the invitation to ${email}`)
          } catch (err) {
            setPendingRevoke(null)
            toast.error(errorMessage(err, `revoke the invitation to ${email}`))
          }
        }}
      />

      <InviteDialog
        open={inviteOpen}
        roles={roles}
        onClose={() => setInviteOpen(false)}
        onSent={async (email) => {
          setInviteOpen(false)
          await onRefresh()
          toast.success(`Invitation sent to ${email}`, {
            description: "They'll pick their own password when they accept.",
          })
        }}
      />
    </Card>
  )
}

function invitationTone(status: InvitationStatus): BadgeTone {
  if (status === "pending") return "warning"
  if (status === "accepted") return "success"
  if (status === "expired") return "default"
  return "danger"
}

function invitationRowActions(
  inv: Invitation,
  ctx: {
    arcadia: ReturnType<typeof useArcadiaClient>
    refresh: () => Promise<void>
    setPendingRevoke: (i: Invitation | null) => void
    toast: ReturnType<typeof useToast>
  },
): ActionItem[] {
  const { arcadia, refresh, setPendingRevoke, toast } = ctx
  const status = invitationStatus(inv)
  const items: ActionItem[] = []

  if (status === "pending" || status === "expired") {
    items.push({
      id: "resend",
      label: "Resend",
      icon: <Send className="size-4" />,
      dataAction: `invitation-${inv.id}-resend`,
      onSelect: async () => {
        try {
          await resendInvitation(arcadia, inv.id)
          await refresh()
          toast.success(`Resent the invitation to ${inv.email}`)
        } catch (err) {
          toast.error(errorMessage(err, `resend the invitation to ${inv.email}`))
        }
      },
    })
  }

  if (status === "pending") {
    items.push({
      id: "revoke",
      label: "Revoke",
      icon: <X className="size-4" />,
      destructive: true,
      dataAction: `invitation-${inv.id}-revoke`,
      onSelect: () => setPendingRevoke(inv),
    })
  }

  return items
}

function InviteDialog({
  open,
  roles,
  onClose,
  onSent,
}: {
  open: boolean
  roles: Role[]
  onClose: () => void
  onSent: (email: string) => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const [email, setEmail] = useState("")
  const [roleId, setRoleId] = useState<string>("")
  const [saving, setSaving] = useState(false)
  const [error, setError] = useState<unknown>(null)

  useEffect(() => {
    if (!open) {
      setEmail("")
      setRoleId(roles[0]?.id ?? "")
      setError(null)
      setSaving(false)
    } else {
      setRoleId((prev) => prev || roles[0]?.id || "")
    }
  }, [open, roles])

  const submit = async () => {
    setError(null)
    setSaving(true)
    try {
      await createInvitation(arcadia, { email, role_id: roleId })
      await onSent(email.trim())
    } catch (err) {
      setError(err)
      setSaving(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>Invite user</DialogTitle>
          <DialogDescription>
            We'll email a sign-up link. The recipient picks their own password on accept.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          <div className="flex flex-col gap-1.5">
            <Label htmlFor="invite-email">Email</Label>
            <Input
              id="invite-email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="new.user@example.com"
              data-action="invite-form-email"
            />
          </div>
          <div className="flex flex-col gap-1.5">
            <Label>Role</Label>
            <Select value={roleId} onValueChange={setRoleId}>
              <SelectTrigger data-action="invite-form-role">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {roles.map((r) => (
                  <SelectItem key={r.id} value={r.id}>
                    {r.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
        </div>

        {error ? <DialogError error={error} context="send the invitation" /> : null}

        <DialogFooter>
          <Button variant="outline" onClick={onClose} disabled={saving} data-action="invite-form-cancel">
            Cancel
          </Button>
          <Button
            onClick={submit}
            disabled={saving || !email || !roleId}
            data-action="invite-form-send"
          >
            {saving ? <RefreshCw className="size-4 animate-spin" /> : <Send className="size-4" />}
            Send invite
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// --- Roles tab ----------------------------------------------------------

function RolesPanel({
  roles,
  loading,
  error,
  onRefresh,
}: {
  roles: Role[]
  loading: boolean
  error: unknown
  onRefresh: () => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const toast = useToast()
  const [search, setSearch] = useState("")
  const [editor, setEditor] = useState<{ mode: "create" } | { mode: "edit"; role: Role } | null>(null)
  const [pendingDelete, setPendingDelete] = useState<Role | null>(null)

  const columns = useMemo<Column<Role>[]>(
    () => [
      {
        id: "name",
        header: "Name",
        accessor: "name",
        sortable: true,
        cell: (r) => <span className="font-medium">{r.name}</span>,
      },
      {
        id: "slug",
        header: "Slug",
        accessor: "slug",
        sortable: true,
        cell: (r) => (
          <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{r.slug}</code>
        ),
      },
      {
        id: "permissions",
        header: "Permissions",
        cell: (r) =>
          r.permissions.length === 0 ? (
            <span className="text-muted-foreground">—</span>
          ) : (
            <span className="text-muted-foreground">{r.permissions.length}</span>
          ),
      },
      {
        id: "system",
        header: "System",
        accessor: "is_system",
        sortable: true,
        cell: (r) =>
          r.is_system ? (
            <CheckCircle2 className="size-4 text-emerald-500" />
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "updated",
        header: "Updated",
        accessor: "updated_at",
        sortable: true,
        cell: (r) => <DateCell value={r.updated_at} format="short" />,
      },
      {
        id: "actions",
        header: "",
        align: "right",
        cell: (r) => (
          <ActionsCell
            items={roleRowActions(r, { setEditor, setPendingDelete })}
            triggerDataAction={`role-${r.slug}-actions`}
          />
        ),
      },
    ],
    [],
  )

  const table = useTable<Role>({
    data: roles,
    columns,
    getRowId: (r) => r.id,
    initialPageSize: 25,
    initialSearch: search,
  })
  useEffect(() => {
    table.setSearch(search)
  }, [search, table])

  return (
    <Card>
      <CardHeader className="flex flex-row items-center gap-3">
        <SearchInput
          value={search}
          onValueChange={setSearch}
          placeholder="Search by name or slug"
          data-action="roles-search"
          className="max-w-sm flex-1"
        />
        <div className="ml-auto flex items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={onRefresh}
            disabled={loading}
            data-action="roles-refresh"
          >
            <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
            Refresh
          </Button>
          <Button size="sm" onClick={() => setEditor({ mode: "create" })} data-action="roles-create">
            <Plus className="size-4" />
            New role
          </Button>
        </div>
      </CardHeader>

      <CardContent className="relative p-0">
        <DataState
          loading={loading}
          error={error}
          isEmpty={table.total === 0}
          onRetry={onRefresh}
          loadingLabel="Loading roles…"
          empty={
            <EmptyState
              title={search ? "No roles match." : "No roles yet."}
              description={search ? "Try a different search." : "Create your first role."}
              className="py-12"
            />
          }
        >
          <DataTable
            columns={columns}
            rows={table.pageRows}
            getRowId={(r) => r.id}
            sort={table.sort}
            onSortToggle={table.toggleSort}
            loading={loading && roles.length > 0}
            stickyHeader
          />
          <Pagination
            page={table.page}
            pageSize={table.pageSize}
            total={table.total}
            onPageChange={table.setPage}
            onPageSizeChange={table.setPageSize}
          />
        </DataState>
      </CardContent>

      <ConfirmDialog
        open={pendingDelete !== null}
        onOpenChange={(o) => !o && setPendingDelete(null)}
        title="Delete role?"
        description={
          pendingDelete
            ? `${pendingDelete.name} is removed from every user who has it, and they immediately lose its ${pendingDelete.permissions.length} permission${pendingDelete.permissions.length === 1 ? "" : "s"}. This can't be undone.`
            : ""
        }
        confirmLabel="Delete"
        variant="danger"
        onConfirm={async () => {
          if (!pendingDelete) return
          const name = pendingDelete.name
          try {
            await deleteRole(arcadia, pendingDelete.id)
            setPendingDelete(null)
            await onRefresh()
            toast.success(`Deleted ${name}`)
          } catch (err) {
            setPendingDelete(null)
            toast.error(errorMessage(err, `delete ${name}`))
          }
        }}
      />

      <RoleEditorDialog
        state={editor}
        onClose={() => setEditor(null)}
        onSaved={async (message) => {
          setEditor(null)
          await onRefresh()
          toast.success(message)
        }}
      />
    </Card>
  )
}

function roleRowActions(
  r: Role,
  ctx: {
    setEditor: (s: { mode: "edit"; role: Role } | null) => void
    setPendingDelete: (r: Role | null) => void
  },
): ActionItem[] {
  const items: ActionItem[] = []
  items.push({
    id: "edit",
    label: r.is_system ? "View" : "Edit",
    dataAction: `role-${r.slug}-edit`,
    onSelect: () => ctx.setEditor({ mode: "edit", role: r }),
  })
  if (!r.is_system) {
    items.push({
      id: "delete",
      label: "Delete",
      icon: <Trash2 className="size-4" />,
      destructive: true,
      dataAction: `role-${r.slug}-delete`,
      onSelect: () => ctx.setPendingDelete(r),
    })
  }
  return items
}

function RoleEditorDialog({
  state,
  onClose,
  onSaved,
}: {
  state: { mode: "create" } | { mode: "edit"; role: Role } | null
  onClose: () => void
  onSaved: (message: string) => Promise<void>
}) {
  const arcadia = useArcadiaClient()
  const open = state !== null
  const isEdit = state?.mode === "edit"
  const initial = isEdit ? state.role : null
  const readOnly = !!initial?.is_system

  const [name, setName] = useState("")
  const [slug, setSlug] = useState("")
  const [description, setDescription] = useState("")
  const [permissionsText, setPermissionsText] = useState("")
  const [saving, setSaving] = useState(false)
  const [error, setError] = useState<unknown>(null)

  useEffect(() => {
    if (!open) return
    setError(null)
    setSaving(false)
    if (initial) {
      setName(initial.name)
      setSlug(initial.slug)
      setDescription(initial.description ?? "")
      setPermissionsText(initial.permissions.join("\n"))
    } else {
      setName("")
      setSlug("")
      setDescription("")
      setPermissionsText("")
    }
  }, [open, initial])

  const submit = async () => {
    setError(null)
    setSaving(true)
    try {
      const permissions = permissionsText
        .split(/\r?\n/)
        .map((s) => s.trim())
        .filter(Boolean)
      const input: RoleInput = { name, slug, description: description || null, permissions }
      if (isEdit && initial) {
        await updateRole(arcadia, initial.id, input)
        await onSaved(`Saved ${name.trim()}`)
      } else {
        await createRole(arcadia, input)
        await onSaved(`Created ${name.trim()}`)
      }
    } catch (err) {
      setError(err)
      setSaving(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>
            {readOnly ? `System role: ${initial?.name}` : isEdit ? "Edit role" : "New role"}
          </DialogTitle>
          <DialogDescription>
            {readOnly
              ? "System roles are read-only. Their permissions are managed by the platform."
              : "Permissions are free-text strings; add one per line (e.g. users:invite, storage:read)."}
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          <div className="grid grid-cols-2 gap-3">
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="role-name">Name</Label>
              <Input
                id="role-name"
                value={name}
                onChange={(e) => setName(e.target.value)}
                disabled={readOnly}
                data-action="role-form-name"
              />
            </div>
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="role-slug">Slug</Label>
              <Input
                id="role-slug"
                value={slug}
                onChange={(e) => setSlug(e.target.value)}
                placeholder="admin, editor, viewer"
                disabled={readOnly || isEdit}
                data-action="role-form-slug"
              />
            </div>
          </div>

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="role-description">Description</Label>
            <Input
              id="role-description"
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              disabled={readOnly}
              data-action="role-form-description"
            />
          </div>

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="role-permissions">Permissions (one per line)</Label>
            <Textarea
              id="role-permissions"
              value={permissionsText}
              onChange={(e) => setPermissionsText(e.target.value)}
              rows={8}
              disabled={readOnly}
              placeholder={"users:read\nusers:invite\nstorage:write"}
              data-action="role-form-permissions"
            />
          </div>
        </div>

        {error ? (
          <DialogError error={error} context={isEdit ? "save the role" : "create the role"} />
        ) : null}

        <DialogFooter>
          <Button variant="outline" onClick={onClose} data-action="role-form-cancel">
            {readOnly ? "Close" : "Cancel"}
          </Button>
          {!readOnly ? (
            <Button
              onClick={submit}
              disabled={saving || !name.trim() || !slug.trim()}
              data-action="role-form-save"
            >
              {saving ? <RefreshCw className="size-4 animate-spin" /> : <CheckCircle2 className="size-4" />}
              {isEdit ? "Save changes" : "Create role"}
            </Button>
          ) : null}
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// --- helpers ------------------------------------------------------------

function countBy<T>(arr: T[], key: (x: T) => string): Record<string, number> {
  return arr.reduce<Record<string, number>>((acc, x) => {
    const k = key(x)
    acc[k] = (acc[k] ?? 0) + 1
    return acc
  }, {})
}

export { RouteErrorBoundary as ErrorBoundary } from "~/components/route-error"
