import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
  AlertTriangle,
  Check,
  CircleCheck,
  CircleDashed,
  CircleX,
  Copy,
  KeyRound,
  RefreshCw,
  ShieldCheck,
  UserPlus,
  Users,
} from 'lucide-react';
import { TEAM_LABELS, TEAM_ROLES, TEAMS, type Team, type TeamRole } from '@pig/core';
import { api, get, patch, post, relativeTime } from '@/lib/api';
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  cn,
  EmptyState,
  Input,
  Label as MicroLabel,
  Section,
  Skeleton,
} from '@/components/ui';
import { Label } from '@/components/ui/label';
import { FormField } from '@/components/ui/form-field';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { IntegrationSettings } from './IntegrationSettings';
import { PiggyMark } from './PiggyMark';

/**
 * What the server can honestly say about Piggy, all of it derived from the
 * deployment environment or from a live probe of the chat server. Nothing here
 * comes from `platform_settings`, because nothing in `apps/piggy` reads it.
 */
interface PiggyRuntimeStatus {
  enabledByEnvironment: boolean;
  chatEnabled: boolean;
  internalUrlConfigured: boolean;
  internalTokenConfigured: boolean;
  model: string | null;
  inferenceBase: string | null;
  inferenceIsolated: boolean;
  /** Null means the API did not probe for this response, not "down". */
  reachable: boolean | null;
  reportedModel: string | null;
}

interface AdminRuntimeSettings {
  piggyEnabled: boolean;
  piggy: PiggyRuntimeStatus;
  primeComputeBase: string;
  primeApiKey: {
    configured: boolean;
    source: 'database' | 'environment' | null;
    updatedAt: string | null;
    encryptionReady: boolean;
  };
  primeSyncEnabled: boolean;
  primeSyncIntervalMinutes: number;
  updatedAt: string;
}

interface Invite {
  id: string;
  email: string | null;
  team: Team | null;
  role: TeamRole;
  usesRemaining: number;
  expiresAt: string | null;
  createdAt: string;
  status: 'active' | 'used' | 'expired' | 'revoked';
}

interface Member {
  id: string;
  name: string;
  email: string;
  title: string | null;
  isPlatformAdmin: boolean;
  adminSource: 'environment' | 'database' | null;
  memberships: { team: Team; role: TeamRole }[];
}

export function AdminSettings() {
  const { data, isLoading } = useQuery({
    queryKey: ['admin-settings'],
    queryFn: () => get<AdminRuntimeSettings>('/api/admin/settings'),
  });

  return (
    <section className="overflow-hidden rounded-2xl border border-border bg-surface">
      <div className="border-b border-border bg-surface-2 px-4 py-5 sm:px-6">
        <div className="flex items-start gap-3">
          <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary text-accent-on">
            <ShieldCheck aria-hidden />
          </div>
          <Section
            level={2}
            className="min-w-0"
            title="Platform control plane"
            description="Configure intelligence, inventory sync, workspace entry, and team authority."
            action={<Badge tone="warning">Admin only</Badge>}
          />
        </div>
      </div>

      <Tabs defaultValue="runtime" className="p-4 sm:p-6">
        <TabsList className="scroll-x flex w-full justify-start sm:inline-flex sm:w-auto">
          <TabsTrigger value="runtime" className="flex-1 sm:flex-none">Runtime</TabsTrigger>
          <TabsTrigger value="invites" className="flex-1 sm:flex-none">Invites</TabsTrigger>
          <TabsTrigger value="access" className="flex-1 sm:flex-none">Access</TabsTrigger>
          <TabsTrigger value="integrations" className="flex-1 sm:flex-none">Integrations</TabsTrigger>
        </TabsList>
        <TabsContent value="runtime">
          {isLoading || !data ? <p className="text-sm text-muted">Loading runtime settings…</p> : <RuntimeForm key={data.updatedAt} settings={data} />}
        </TabsContent>
        <TabsContent value="invites"><InviteManager /></TabsContent>
        <TabsContent value="access"><MemberManager /></TabsContent>
        <TabsContent value="integrations"><IntegrationSettings /></TabsContent>
      </Tabs>
    </section>
  );
}

function RuntimeForm({ settings }: { settings: AdminRuntimeSettings }) {
  const queryClient = useQueryClient();
  const [piggyEnabled, setPiggyEnabled] = useState(settings.piggyEnabled);
  const [syncEnabled, setSyncEnabled] = useState(settings.primeSyncEnabled);
  const [interval, setIntervalValue] = useState(String(settings.primeSyncIntervalMinutes));
  const [primeApiKey, setPrimeApiKey] = useState('');
  const [clearKey, setClearKey] = useState(false);
  const [message, setMessage] = useState<string | null>(null);

  const save = useMutation({
    mutationFn: () =>
      patch<AdminRuntimeSettings>('/api/admin/settings', {
        piggyEnabled,
        primeSyncEnabled: syncEnabled,
        primeSyncIntervalMinutes: Number(interval),
        ...(primeApiKey ? { primeApiKey } : {}),
        ...(clearKey ? { clearPrimeApiKey: true } : {}),
      }),
    onSuccess: () => {
      setPrimeApiKey('');
      setClearKey(false);
      setMessage('Runtime settings saved. Prime sync is reloading in the background.');
      void queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
    },
  });

  return (
    <form className="flex flex-col gap-5" onSubmit={(event) => { event.preventDefault(); setMessage(null); save.mutate(); }}>
      <div className="grid gap-4 xl:grid-cols-2">
        <PiggyCard status={settings.piggy} chatEnabled={piggyEnabled} onChatEnabledChange={setPiggyEnabled} />

        <Card>
          <CardHeader>
            <div className="flex items-center gap-2"><RefreshCw className="text-muted" aria-hidden /><CardTitle className="text-base">Prime inventory</CardTitle></div>
            <p className="break-all text-xs text-muted">Compute endpoint: {settings.primeComputeBase}</p>
          </CardHeader>
          <CardContent className="flex flex-col gap-4">
            <div className="flex flex-wrap items-center gap-2">
              <Badge tone={settings.primeApiKey.configured ? 'neutral' : 'warning'}>{settings.primeApiKey.configured ? 'Credential configured' : 'Credential missing'}</Badge>
              {settings.primeApiKey.source ? <Badge>{settings.primeApiKey.source} source</Badge> : null}
              {settings.primeApiKey.updatedAt ? <span className="text-xs text-muted">updated {relativeTime(settings.primeApiKey.updatedAt)}</span> : null}
            </div>
            <FormField label="Replace Prime API key" hint={settings.primeApiKey.encryptionReady ? 'Encrypted with AES-256-GCM before it reaches the database.' : 'Set PIG_SETTINGS_ENCRYPTION_KEY on the server to enable credential writes.'}>
              <Input id="prime-api-key" type="password" autoComplete="new-password" value={primeApiKey} onChange={(event) => { setPrimeApiKey(event.target.value); setClearKey(false); }} placeholder="Enter a new key; existing material is never shown" disabled={!settings.primeApiKey.encryptionReady} />
            </FormField>
            {settings.primeApiKey.source === 'database' ? <Button type="button" variant={clearKey ? 'danger' : 'outline'} size="sm" onClick={() => { setClearKey((value) => !value); setPrimeApiKey(''); }}>{clearKey ? 'Credential will be cleared' : 'Clear stored credential'}</Button> : null}
            <div className="grid gap-3 sm:grid-cols-[1fr_9rem] sm:items-end">
              <ToggleRow id="prime-sync" label="Inventory sync" description="Continuously refresh Prime availability and pricing." checked={syncEnabled} onCheckedChange={setSyncEnabled} />
              <FormField label="Every (minutes)"><Input id="sync-interval" type="number" min="1" max="1440" value={interval} onChange={(event) => setIntervalValue(event.target.value)} /></FormField>
            </div>
          </CardContent>
        </Card>
      </div>
      {save.error ? <p role="alert" className="text-sm text-danger">{save.error.message}</p> : null}
      {message ? <p className="flex items-center gap-2 text-sm text-positive"><Check aria-hidden />{message}</p> : null}
      <div><Button type="submit" variant="primary" disabled={save.isPending}>{save.isPending ? 'Saving…' : 'Save runtime settings'}</Button></div>
    </form>
  );
}

/**
 * Piggy's control panel, which for the most part controls nothing.
 *
 * This card used to offer an editable model and inference endpoint. Both saved
 * happily into `platform_settings`, and `apps/piggy` has never read that table:
 * it takes its model, its endpoint and its inference key from `process.env` at
 * boot. An admin could therefore change the model here, be told it was saved,
 * and watch the old one keep answering. They are reported as environment facts
 * now, and the only genuinely live control — the chat switch — is labelled with
 * what it actually gates.
 */
function PiggyCard({
  status,
  chatEnabled,
  onChatEnabledChange,
}: {
  status: PiggyRuntimeStatus;
  chatEnabled: boolean;
  onChatEnabledChange(value: boolean): void;
}) {
  const queryClient = useQueryClient();
  const [rechecking, setRechecking] = useState(false);
  const verdict = piggyVerdict(status);
  // Two containers, two copies of PIGGY_MODEL. When they disagree, the process
  // doing the inference wins, and the operator is looking at the wrong one.
  const modelDisagrees =
    status.reportedModel !== null && status.model !== null && status.reportedModel !== status.model;

  function recheck() {
    setRechecking(true);
    void queryClient
      .refetchQueries({ queryKey: ['admin-settings'] })
      .finally(() => setRechecking(false));
  }

  return (
    <Card>
      <CardHeader>
        <div className="flex flex-wrap items-center justify-between gap-2">
          <div className="flex items-center gap-2">
            <PiggyMark className="size-5 text-muted" />
            <CardTitle className="text-base">Piggy intelligence</CardTitle>
          </div>
          <Button type="button" size="sm" variant="outline" onClick={recheck} disabled={rechecking}>
            <RefreshCw className={cn('size-4', rechecking && 'animate-spin')} aria-hidden />
            {rechecking ? 'Checking…' : 'Recheck'}
          </Button>
        </div>
        <p className="text-sm text-muted">
          Piggy reads its model, endpoint and inference key from the deployment environment once, at boot. Nothing on this page can change them.
        </p>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <div className={cn('rounded-xl border p-3', VERDICT_SURFACE[verdict.tone])}>
          <p className={cn('text-sm font-medium', VERDICT_TEXT[verdict.tone])}>{verdict.title}</p>
          <p className="mt-1 text-xs text-muted">{verdict.detail}</p>
        </div>

        <ul className="grid gap-2 sm:grid-cols-2">
          <PiggyFact
            state={status.enabledByEnvironment ? 'ok' : 'bad'}
            label="Deployment gate"
            detail={status.enabledByEnvironment ? 'PIGGY_ENABLED is set' : 'PIGGY_ENABLED is unset'}
          />
          <PiggyFact
            state={status.internalUrlConfigured ? 'ok' : 'bad'}
            label="Relay address"
            detail={status.internalUrlConfigured ? 'PIGGY_INTERNAL_URL is set' : 'PIGGY_INTERNAL_URL is unset'}
          />
          <PiggyFact
            state={status.internalTokenConfigured ? 'ok' : 'bad'}
            label="Internal token"
            detail={status.internalTokenConfigured ? 'PIGGY_INTERNAL_TOKEN is set' : 'PIGGY_INTERNAL_TOKEN is unset'}
          />
          <PiggyFact
            state={status.reachable === null ? 'unknown' : status.reachable ? 'ok' : 'bad'}
            label="Chat server"
            detail={
              status.reachable === null
                ? 'Not probed'
                : status.reachable
                  ? 'Answering /internal/health'
                  : 'No answer on /internal/health'
            }
          />
        </ul>

        {status.inferenceIsolated ? null : (
          <p className="flex items-start gap-2 text-xs text-danger">
            <AlertTriangle className="mt-px size-4 shrink-0" aria-hidden />
            PIGGY_INFERENCE_BASE points at the Prime compute API host. Inference lives on a different host and no model call can succeed against this one.
          </p>
        )}
        {modelDisagrees ? (
          <p className="flex items-start gap-2 text-xs text-warning">
            <AlertTriangle className="mt-px size-4 shrink-0" aria-hidden />
            This API container is configured for {status.model}, but the Piggy process reports {status.reportedModel}. The two environments disagree; the one Piggy holds is the one being billed.
          </p>
        ) : null}

        <EnvironmentValue
          label="Model"
          variable="PIGGY_MODEL"
          value={status.reportedModel ?? status.model}
          note={
            status.reportedModel
              ? 'Reported by the running chat server, which is the copy that matters.'
              : 'From this API container. The Piggy process holds its own copy and only it can confirm what is in force.'
          }
        />
        <EnvironmentValue
          label="Inference endpoint"
          variable="PIGGY_INFERENCE_BASE"
          value={status.inferenceBase}
          note={
            status.inferenceIsolated
              ? 'A different host from the Prime compute API, as it must be. The inference key that goes with it never reaches this container.'
              : 'It should name an inference host. The inference key that goes with it never reaches this container.'
          }
        />

        {/* Locked rather than merely ineffective when the environment gate is
            shut: a switch that saves and changes nothing is the exact failure
            this card was rewritten to remove. */}
        <ToggleRow
          id="piggy-enabled"
          label="Interactive chat"
          description={
            status.enabledByEnvironment
              ? 'Lets people open Piggy and ask questions. The background task worker ignores this switch entirely — it runs whenever the Piggy process is up.'
              : 'Locked until PIGGY_ENABLED is set in the environment. It gates the chat panel only; the background task worker never reads it.'
          }
          checked={chatEnabled}
          disabled={!status.enabledByEnvironment}
          onCheckedChange={onChatEnabledChange}
        />
      </CardContent>
    </Card>
  );
}

/*
 * `positive` is deliberately the neutral treatment. A control plane that paints
 * a green panel every time everything is fine teaches an operator to stop
 * reading the panel, and the one state that matters here is the one that needs
 * them. Colour marks a problem; working is quiet.
 */
const VERDICT_SURFACE = {
  positive: 'border-border bg-surface-2',
  warning: 'border-warning bg-warning/10',
  danger: 'border-danger bg-danger/10',
  neutral: 'border-border bg-surface-2',
} as const;

const VERDICT_TEXT = {
  positive: 'text-fg',
  warning: 'text-warning',
  danger: 'text-danger',
  neutral: 'text-fg',
} as const;

interface PiggyVerdict {
  tone: keyof typeof VERDICT_SURFACE;
  title: string;
  detail: string;
}

/**
 * Ordered outermost gate first, because only the first unmet condition is
 * actionable: telling an operator their chat server is unreachable when
 * PIGGY_ENABLED is unset sends them to read container logs for a service they
 * never asked to run.
 *
 * Read from the saved status rather than the pending switch, so an unsaved
 * toggle cannot make the panel describe a state that is not in force.
 */
function piggyVerdict(status: PiggyRuntimeStatus): PiggyVerdict {
  if (!status.enabledByEnvironment) {
    return {
      tone: 'warning',
      title: 'Piggy is switched off in this deployment',
      detail:
        'PIGGY_ENABLED is unset, so chat is hidden for everyone and the switch below is locked. Set it in the environment and restart the API.',
    };
  }
  if (!status.internalUrlConfigured || !status.internalTokenConfigured) {
    return {
      tone: 'danger',
      title: 'Piggy is enabled but not wired up',
      detail:
        'The API has no authenticated route to the chat server. Chat stays unavailable until both PIGGY_INTERNAL_URL and PIGGY_INTERNAL_TOKEN are set on this container.',
    };
  }
  if (status.reachable === null) {
    return {
      tone: 'neutral',
      title: 'Chat server not checked',
      detail: 'Press Recheck to probe it.',
    };
  }
  if (!status.reachable) {
    return {
      tone: 'danger',
      title: 'The chat server is not answering',
      detail:
        'PIGGY_INFERENCE_API_KEY never reaches this container, and Piggy exits at boot without it — a missing key looks exactly like this. Check the Piggy container logs before anything else.',
    };
  }
  if (!status.chatEnabled) {
    return {
      tone: 'warning',
      title: 'Reachable, but chat is switched off',
      detail:
        'The chat server answered and the queued task worker is running, but nobody can open the chat panel until the switch below is on and saved.',
    };
  }
  return {
    tone: 'positive',
    title: 'Piggy is answering',
    detail: status.reportedModel
      ? `The chat server is up and running ${status.reportedModel}.`
      : 'The chat server is up.',
  };
}

function PiggyFact({ state, label, detail }: { state: 'ok' | 'bad' | 'unknown'; label: string; detail: string }) {
  const Icon = state === 'ok' ? CircleCheck : state === 'bad' ? CircleX : CircleDashed;
  return (
    <li className="flex items-start gap-2">
      <Icon
        className={cn(
          'mt-0.5 size-4 shrink-0',
          state === 'bad' ? 'text-danger' : 'text-muted',
        )}
        aria-hidden
      />
      <div className="min-w-0">
        <p className="text-sm font-medium leading-tight">{label}</p>
        <p className="mt-0.5 text-xs text-muted">{detail}</p>
      </div>
    </li>
  );
}

/**
 * A value an admin may need to read and quote, but must not be invited to edit.
 * Rendered as text rather than a disabled input on purpose: a greyed-out field
 * still reads as "editable later", and this one never will be.
 */
function EnvironmentValue({ label, variable, value, note }: { label: string; variable: string; value: string | null; note: string }) {
  return (
    <div className="flex min-w-0 flex-col gap-1.5">
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-sm font-medium">{label}</span>
        <Badge>Set by environment</Badge>
      </div>
      <p className="min-w-0 break-all rounded-lg border border-border bg-surface-2 px-3 py-2 font-mono text-xs">
        {value ?? 'unset'}
      </p>
      <span className="text-xs text-muted">
        <code className="font-mono">{variable}</code> · {note}
      </span>
    </div>
  );
}

function ToggleRow({ id, label, description, checked, disabled, onCheckedChange }: { id: string; label: string; description: string; checked: boolean; disabled?: boolean; onCheckedChange(value: boolean): void }) {
  return <div className="flex min-w-0 items-center justify-between gap-4 rounded-xl border border-border p-3"><div className="min-w-0"><Label htmlFor={id}>{label}</Label><p className="mt-1 text-xs text-muted">{description}</p></div><Switch id={id} checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} /></div>;
}

function InviteManager() {
  const queryClient = useQueryClient();
  const ledger = useQuery({ queryKey: ['admin-invites'], queryFn: () => get<Invite[]>('/api/admin/invites') });
  const [email, setEmail] = useState('');
  const [team, setTeam] = useState<Team | 'any'>('any');
  const [role, setRole] = useState<TeamRole>('member');
  const [uses, setUses] = useState('1');
  const [expiresAt, setExpiresAt] = useState('');
  const [issuedCode, setIssuedCode] = useState<string | null>(null);
  const create = useMutation({
    mutationFn: () => post<Invite & { code: string }>('/api/admin/invites', { ...(email ? { email } : {}), ...(team !== 'any' ? { team } : {}), role, usesRemaining: Number(uses), ...(expiresAt ? { expiresAt: new Date(expiresAt).toISOString() } : {}) }),
    onSuccess: (invite) => { setIssuedCode(invite.code); setEmail(''); void queryClient.invalidateQueries({ queryKey: ['admin-invites'] }); },
  });
  const revoke = useMutation({ mutationFn: (id: string) => api(`/api/admin/invites/${id}`, { method: 'DELETE' }), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin-invites'] }) });

  return <div className="grid gap-5 xl:grid-cols-[minmax(18rem,0.8fr)_minmax(0,1.2fr)]">
    <Card><CardHeader><div className="flex items-center gap-2"><UserPlus className="text-muted" aria-hidden /><CardTitle className="text-base">Issue an invite</CardTitle></div><p className="text-sm text-muted">Codes gate PIG registration. They never open registration on the shared identity provider.</p></CardHeader><CardContent><form className="flex flex-col gap-4" onSubmit={(event) => { event.preventDefault(); setIssuedCode(null); create.mutate(); }}>
      <FormField label="Email, optional"><Input type="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Pin to a known address" /></FormField>
      <div className="grid grid-cols-2 gap-3"><FormField label="Team"><Select value={team} onValueChange={(value) => setTeam(value as Team | 'any')}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="any">Choose at signup</SelectItem>{TEAMS.map((value) => <SelectItem key={value} value={value}>{TEAM_LABELS[value]}</SelectItem>)}</SelectGroup></SelectContent></Select></FormField><FormField label="Role"><Select value={role} onValueChange={(value) => setRole(value as TeamRole)}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectGroup>{TEAM_ROLES.map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectGroup></SelectContent></Select></FormField></div>
      <div className="grid grid-cols-2 gap-3"><FormField label="Uses"><Input type="number" min="1" max="100" value={uses} onChange={(event) => setUses(event.target.value)} /></FormField><FormField label="Expires, optional"><Input type="datetime-local" value={expiresAt} onChange={(event) => setExpiresAt(event.target.value)} /></FormField></div>
      {create.error ? <p role="alert" className="text-sm text-danger">{create.error.message}</p> : null}<Button type="submit" variant="primary" disabled={create.isPending}>{create.isPending ? 'Issuing…' : 'Issue invite'}</Button>
      {issuedCode ? <div className="rounded-xl border border-warning bg-warning/10 p-3"><p className="text-xs font-medium text-warning">Shown once. Send it through a secure channel.</p><div className="mt-2 flex min-w-0 items-center gap-2"><code className="min-w-0 flex-1 break-all text-xs">{issuedCode}</code><Button type="button" size="icon" variant="ghost" aria-label="Copy invite code" onClick={() => void navigator.clipboard.writeText(issuedCode)}><Copy aria-hidden /></Button></div></div> : null}
    </form></CardContent></Card>
    <Card><CardHeader><CardTitle className="text-base">Invite ledger</CardTitle><p className="text-sm text-muted">Only metadata remains visible after issuance.</p></CardHeader>{/* A failed ledger read must not render as "no invites issued": an admin who
        believes the workspace is empty issues a second code to someone who
        already has one. */}
    <CardContent className="flex flex-col gap-2">{ledger.isPending ? <div className="flex flex-col gap-2" aria-busy><span className="sr-only">Loading invites…</span>{[0, 1].map((row) => <Skeleton key={row} className="h-16 rounded-xl" />)}</div> : ledger.isError ? <EmptyState icon={<AlertTriangle aria-hidden />} title="Invite ledger unavailable" description={ledger.error.message} action={<Button type="button" variant="outline" onClick={() => void ledger.refetch()}><RefreshCw aria-hidden />Try again</Button>} /> : ledger.data.length === 0 ? <EmptyState size="inline" title="No invites issued yet" /> : ledger.data.map((invite) => <div key={invite.id} className="flex min-w-0 flex-col gap-3 rounded-xl border border-border p-3 sm:flex-row sm:items-center"><div className="min-w-0 flex-1"><div className="flex flex-wrap items-center gap-2"><p className="truncate text-sm font-medium">{invite.email ?? 'Workspace invite'}</p><Badge tone={invite.status === 'active' ? 'positive' : invite.status === 'expired' ? 'warning' : 'neutral'}>{invite.status}</Badge></div><p className="mt-1 text-xs text-muted">{invite.team ? TEAM_LABELS[invite.team] : 'Team chosen at signup'} · {invite.role} · {invite.usesRemaining} use{invite.usesRemaining === 1 ? '' : 's'} left</p></div>{invite.status === 'active' ? <Button type="button" size="sm" variant="outline" disabled={revoke.isPending} onClick={() => revoke.mutate(invite.id)}>Revoke</Button> : null}</div>)}</CardContent></Card>
  </div>;
}

/**
 * The tab an admin is sent to in order to grant someone access.
 *
 * It used to destructure `data = []` with no loading or error branch, so a slow
 * or failed request drew a heading over nothing — indistinguishable from a
 * workspace with no members, and no indication that anything had gone wrong.
 */
function MemberManager() {
  const query = useQuery({ queryKey: ['admin-members'], queryFn: () => get<Member[]>('/api/admin/members') });

  return (
    <div className="flex flex-col gap-3">
      <div className="flex items-start gap-2">
        <Users className="mt-0.5 shrink-0 text-muted" aria-hidden />
        <Section
          className="min-w-0"
          title="Team and role administration"
          description="Roles are team-scoped. Platform administration is a separate grant."
        />
      </div>
      {query.isPending ? (
        <div className="flex flex-col gap-3" aria-busy>
          <span className="sr-only">Loading members…</span>
          {/* Shaped like a member row rather than a plain bar, so the tab does
              not visibly reflow the moment the request lands. */}
          {[0, 1, 2].map((row) => (
            <Card key={row}>
              <CardContent className="flex flex-col gap-4 p-4 sm:p-5 xl:flex-row xl:items-center">
                <div className="flex flex-col gap-2 xl:w-64"><Skeleton className="h-4 w-32" /><Skeleton className="h-3 w-44" /></div>
                <div className="grid flex-1 gap-2 sm:grid-cols-3">{[0, 1, 2].map((column) => <Skeleton key={column} className="h-11" />)}</div>
                <Skeleton className="h-9 w-28" />
              </CardContent>
            </Card>
          ))}
        </div>
      ) : query.isError ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<AlertTriangle aria-hidden />}
              title="Access list unavailable"
              description={query.error.message}
              action={<Button type="button" variant="outline" onClick={() => void query.refetch()}><RefreshCw aria-hidden />Try again</Button>}
            />
          </CardContent>
        </Card>
      ) : query.data.length === 0 ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Users aria-hidden />}
              title="No active members"
              description="Everyone with an account has been deactivated. Issue an invite from the Invites tab to bring someone back in."
            />
          </CardContent>
        </Card>
      ) : (
        query.data.map((member) => <MemberAccess key={`${member.id}:${JSON.stringify(member.memberships)}:${member.isPlatformAdmin}`} member={member} />)
      )}
    </div>
  );
}

function MemberAccess({ member }: { member: Member }) {
  const queryClient = useQueryClient();
  const [isPlatformAdmin, setIsPlatformAdmin] = useState(member.isPlatformAdmin);
  const [roles, setRoles] = useState<Partial<Record<Team, TeamRole>>>(() => Object.fromEntries(member.memberships.map(({ team, role }) => [team, role])));
  const save = useMutation({ mutationFn: () => patch(`/api/admin/members/${member.id}/access`, { isPlatformAdmin, memberships: TEAMS.flatMap((team) => roles[team] ? [{ team, role: roles[team] }] : []) }), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin-members'] }) });
  return <Card><CardContent className="p-4 sm:p-5"><div className="flex flex-col gap-4 xl:flex-row xl:items-center"><div className="min-w-0 xl:w-64"><div className="flex flex-wrap items-center gap-2"><p className="truncate font-medium">{member.name}</p>{member.isPlatformAdmin ? <Badge tone="warning"><KeyRound aria-hidden />Platform admin</Badge> : null}</div><p className="truncate text-sm text-muted">{member.email}</p></div><div className="grid min-w-0 flex-1 gap-2 sm:grid-cols-3">{TEAMS.map((team) => <label key={team} className="flex flex-col gap-1.5"><MicroLabel as="span">{TEAM_LABELS[team]}</MicroLabel><Select value={roles[team] ?? 'none'} onValueChange={(value) => setRoles((current) => ({ ...current, [team]: value === 'none' ? undefined : value as TeamRole }))}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectGroup><SelectItem value="none">No access</SelectItem>{TEAM_ROLES.map((role) => <SelectItem key={role} value={role}>{role}</SelectItem>)}</SelectGroup></SelectContent></Select></label>)}</div><div className="flex items-center justify-between gap-3 xl:w-52"><div><Label htmlFor={`admin-${member.id}`}>Platform admin</Label>{member.adminSource === 'environment' ? <p className="text-xs text-muted">Pinned by environment</p> : null}</div><Switch id={`admin-${member.id}`} checked={isPlatformAdmin} disabled={member.adminSource === 'environment'} onCheckedChange={setIsPlatformAdmin} /></div><Button type="button" size="sm" variant="primary" disabled={save.isPending} onClick={() => save.mutate()}>{save.isPending ? 'Saving…' : 'Save access'}</Button></div>{save.error ? <p role="alert" className="mt-3 text-sm text-danger">{save.error.message}</p> : null}</CardContent></Card>;
}
