/**
 * Settings — appearance, profile, agent credentials, and session.
 *
 * The appearance section is where the user picks the accent that re-tints the
 * whole product. It is saved server-side, so the choice follows them between
 * devices rather than being a per-browser quirk.
 */
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Check, Copy, KeyRound, LogOut, Monitor, Moon, Sun, Terminal } from 'lucide-react';
import { api, get, getSupabase, patch, post, relativeTime, shortDate } from '@/lib/api';
import { useTheme } from '@/lib/theme';
import { getAccent, THEME_MODES, type ThemeMode } from '@pig/core';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input } from '@/components/ui';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { useState, type ReactNode } from 'react';
import { usePageTitle } from '@/lib/title';
import { AdminSettings } from '@/components/AdminSettings';
import { toast } from 'sonner';

interface Me {
  id: string;
  name: string;
  email: string;
  isPlatformAdmin: boolean;
  teams: { team: string; role: string }[];
  via: string;
}

export function Settings() {
  usePageTitle('Settings');
  const { data: me } = useQuery({ queryKey: ['me'], queryFn: () => get<Me>('/api/me') });

  return (
    <div className="space-y-6 pb-4">
      <header className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
        <div>
          <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Settings</h1>
          <p className="mt-1 max-w-2xl text-sm text-muted">
            Personal preferences, agent credentials, and server-managed integration readiness.
          </p>
        </div>
        {me?.isPlatformAdmin ? <Badge tone="warning">Platform admin view</Badge> : null}
      </header>

      <Section title="Your account">
        <div className="grid gap-6 xl:grid-cols-2">
          <Appearance />
          {/*
            Profile and Session share a column so the page keeps two even
            columns instead of stranding a short card on a row of its own, and
            because signing out belongs with the identity it ends.
          */}
          <div className="flex min-w-0 flex-col gap-6">
            <Profile me={me} />
            <SessionCard />
          </div>
        </div>
      </Section>

      {me?.isPlatformAdmin ? <AdminSettings /> : null}

      {/*
        The credential card sits beside the snippet that tells you to create a
        key — on a phone, directly under it. It used to say "create one below"
        with nothing below, which is the dead end this section closes.
      */}
      <Section title="Agent access">
        {/* `items-start` because the instruction card is a third of the height
            of the credential list, and stretching it leaves a card that is
            mostly empty space. */}
        <div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1.25fr)]">
          <ConnectAgent />
          {me ? <ApiKeys me={me} /> : null}
        </div>
      </Section>
    </div>
  );
}

function Section({ title, children }: { title: string; children: ReactNode }) {
  return (
    <section className="space-y-3">
      <h2 className="text-xs font-semibold uppercase tracking-wide text-muted">{title}</h2>
      {children}
    </section>
  );
}

function Appearance() {
  const { mode, accent, resolved, setMode, setAccent, accents } = useTheme();

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base">Appearance</CardTitle>
        <p className="text-sm text-muted">
          Saved to your account, so it follows you between your laptop and your phone.
        </p>
      </CardHeader>
      <CardContent className="space-y-5">
        <div>
          <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted">Theme</p>
          <div className="inline-flex w-full rounded-lg bg-surface-2 p-1 sm:w-auto">
            {THEME_MODES.map((value) => {
              const Icon = value === 'light' ? Sun : value === 'dark' ? Moon : Monitor;
              return (
                <button
                  key={value}
                  type="button"
                  onClick={() => setMode(value as ThemeMode)}
                  aria-pressed={mode === value}
                  className={[
                    'tap flex flex-1 items-center justify-center gap-2 rounded-md px-4 text-sm font-medium capitalize transition-colors sm:flex-none',
                    mode === value ? 'bg-surface text-fg shadow-sm' : 'text-muted',
                  ].join(' ')}
                >
                  <Icon className="h-4 w-4" aria-hidden />
                  {value}
                </button>
              );
            })}
          </div>
        </div>

        <div>
          <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted">Accent</p>
          <div className="flex flex-wrap gap-2">
            {accents.map((option) => {
              const selected = option.key === accent;
              const definition = getAccent(option.key);
              return (
                <button
                  key={option.key}
                  type="button"
                  onClick={() => setAccent(option.key)}
                  aria-pressed={selected}
                  aria-label={option.label}
                  title={option.label}
                  className={[
                    'tap relative flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors',
                    selected ? 'border-primary bg-accent-subtle' : 'border-border hover:bg-surface-2',
                  ].join(' ')}
                >
                  {/*
                    The swatch previews the value for the CURRENT theme, because
                    each accent is tuned twice — a colour that reads well on
                    white is usually too dark on near-black. Showing the light
                    value in dark mode made the near-black "Pig" swatch
                    effectively invisible against a dark card, which is exactly
                    the misrepresentation this avoids.
                  */}
                  <span
                    className="h-4 w-4 rounded-full border border-fg/15"
                    style={{
                      backgroundColor: `hsl(${
                        resolved === 'dark' ? definition.dark.accent : definition.light.accent
                      })`,
                    }}
                    aria-hidden
                  />
                  {option.label}
                  {selected ? <Check className="h-3.5 w-3.5" aria-hidden /> : null}
                </button>
              );
            })}
          </div>
          <p className="mt-2 text-xs text-muted">
            Status colours — positive, warning, danger — stay fixed regardless of your accent,
            so a warning always looks like a warning.
          </p>
        </div>
      </CardContent>
    </Card>
  );
}

function Profile({ me }: { me: Me | undefined }) {
  const queryClient = useQueryClient();
  const [name, setName] = useState('');
  const [title, setTitle] = useState('');

  const save = useMutation({
    mutationFn: () =>
      patch('/api/me/preferences', {
        ...(name ? { name } : {}),
        ...(title ? { title } : {}),
      }),
    onSuccess: () => {
      void queryClient.invalidateQueries({ queryKey: ['me'] });
      setName('');
      setTitle('');
      // The form clears itself on success, which without confirmation reads as
      // though the input was discarded rather than saved.
      toast.success('Profile saved');
    },
    onError: () => toast.error('Could not save your profile'),
  });

  if (!me) return null;

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base">Profile</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <dl className="grid gap-2 text-sm sm:grid-cols-2">
          <div>
            <dt className="text-xs text-muted">Name</dt>
            <dd>{me.name}</dd>
          </div>
          <div>
            <dt className="text-xs text-muted">Email</dt>
            <dd className="break-all">{me.email}</dd>
          </div>
          <div className="sm:col-span-2">
            <dt className="text-xs text-muted">Teams</dt>
            <dd className="mt-1 flex flex-wrap gap-1.5">
              {me.teams.length === 0 ? (
                <span className="text-muted">No team membership</span>
              ) : (
                me.teams.map((t) => (
                  <Badge key={t.team} tone="accent">
                    {t.team} · {t.role}
                  </Badge>
                ))
              )}
              {me.isPlatformAdmin ? <Badge tone="warning">Platform admin</Badge> : null}
            </dd>
          </div>
        </dl>

        <form
          className="grid gap-3 sm:grid-cols-2"
          onSubmit={(event) => {
            event.preventDefault();
            save.mutate();
          }}
        >
          <label className="block" htmlFor="profile-display-name">
            <span className="mb-1 block text-xs font-medium text-muted">Display name</span>
            <Input id="profile-display-name" name="displayName" value={name} onChange={(e) => setName(e.target.value)} placeholder={me.name} />
          </label>
          <label className="block" htmlFor="profile-title">
            <span className="mb-1 block text-xs font-medium text-muted">Title</span>
            <Input
              id="profile-title"
              name="title"
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              placeholder="Head of Compute"
            />
          </label>
          <div className="sm:col-span-2">
            <Button
              type="submit"
              variant="primary"
              disabled={save.isPending || (!name && !title)}
            >
              {save.isPending ? 'Saving…' : 'Save profile'}
            </Button>
          </div>
        </form>
      </CardContent>
    </Card>
  );
}

function ConnectAgent() {
  const origin = typeof window !== 'undefined' ? window.location.origin : 'https://your-pig-host';

  return (
    <Card>
      <CardHeader>
        <div className="flex items-center gap-2">
          <Terminal className="h-4 w-4 text-accent-fg" aria-hidden />
          <CardTitle className="text-base">Connect your agent</CardTitle>
        </div>
        <p className="text-sm text-muted">
          PIG speaks MCP, so Claude Code, Codex, prime-agent and Buzz agents can all work with
          your pipeline directly from the terminal.
        </p>
      </CardHeader>
      <CardContent className="space-y-3">
        <div className="scroll-x rounded-lg bg-surface-2 p-3">
          <pre className="text-xs leading-relaxed">
            <code>{`export PIG_URL=${origin}
export PIG_API_KEY=pig_...      # create one in API keys

claude mcp add pig -- npx -y @pig/mcp`}</code>
          </pre>
        </div>
        <p className="text-xs text-muted">
          The agent authenticates as its own principal, separately revocable from your own
          session, and can never reach further than you can.
        </p>
      </CardContent>
    </Card>
  );
}

interface ApiKey {
  id: string;
  userId: string;
  name: string;
  keyPrefix: string;
  scopes: string[];
  lastUsedAt: string | null;
  expiresAt: string | null;
  revokedAt: string | null;
  createdAt: string;
}

/** The creation response is the only time the server ever discloses `key`. */
type IssuedApiKey = ApiKey & { key: string };

function keyStatus(key: ApiKey): { label: string; tone: 'positive' | 'warning' | 'neutral' } {
  if (key.revokedAt) return { label: 'revoked', tone: 'neutral' };
  if (key.expiresAt && new Date(key.expiresAt) <= new Date()) {
    return { label: 'expired', tone: 'warning' };
  }
  return { label: 'active', tone: 'positive' };
}

/**
 * Copying, with the failure modes it actually has.
 *
 * PIG is routinely opened from a phone on the LAN over plain http, where
 * `navigator.clipboard` is simply absent — and this is the one screen where a
 * silently failed copy costs the user a credential they can never see again.
 * Every path therefore reports itself, and the secret stays selectable on
 * screen so a refused clipboard is an inconvenience rather than a loss.
 */
async function copyToClipboard(value: string, success: string): Promise<boolean> {
  if (!navigator.clipboard) {
    toast.error('Copying needs a secure connection. Select the key and copy it by hand.');
    return false;
  }
  try {
    await navigator.clipboard.writeText(value);
  } catch {
    toast.error('The browser refused clipboard access. Select the key and copy it by hand.');
    return false;
  }
  toast.success(success);
  return true;
}

/**
 * API keys — the only mint path there is.
 *
 * `requireApiKeyManagement` on the server rejects any principal that
 * authenticated with an API key, so a credential can never mint a successor
 * that outlives its own revocation. That leaves a browser session as the only
 * possible caller, and there is no CLI equivalent: without this card, PIG's
 * MCP story is unreachable.
 *
 * Note what the endpoint does NOT require — no capability at all. Gating this
 * behind `settings:admin` would look prudent and would in fact deny every
 * ordinary member the keys the server is perfectly willing to give them, so
 * the gate here mirrors the server's real rule and nothing more.
 */
function ApiKeys({ me }: { me: Me }) {
  const queryClient = useQueryClient();
  const canManage = me.via !== 'api_key';

  const [name, setName] = useState('');
  const [scope, setScope] = useState<'read' | 'write'>('read');
  const [expiresAt, setExpiresAt] = useState('');
  const [issued, setIssued] = useState<IssuedApiKey | null>(null);
  const [pendingRevoke, setPendingRevoke] = useState<ApiKey | null>(null);

  const { data = [], isLoading } = useQuery({
    queryKey: ['api-keys'],
    queryFn: () => get<ApiKey[]>('/api/api-keys'),
    enabled: canManage,
  });

  const create = useMutation({
    mutationFn: () =>
      post<IssuedApiKey>('/api/api-keys', {
        name: name.trim(),
        scopes: scope === 'write' ? ['read', 'write'] : ['read'],
        ...(expiresAt ? { expiresAt: new Date(expiresAt).toISOString() } : {}),
      }),
    onSuccess: (key) => {
      setIssued(key);
      setName('');
      setExpiresAt('');
      void queryClient.invalidateQueries({ queryKey: ['api-keys'] });
    },
  });

  const revoke = useMutation({
    mutationFn: (key: ApiKey) => api(`/api/api-keys/${key.id}`, { method: 'DELETE' }),
    onSuccess: (_result, key) => {
      setPendingRevoke(null);
      toast.success(`“${key.name}” can no longer authenticate`);
      void queryClient.invalidateQueries({ queryKey: ['api-keys'] });
    },
    onError: () => toast.error('Could not revoke that key'),
  });

  return (
    <Card>
      <CardHeader>
        <div className="flex items-center gap-2">
          <KeyRound className="h-4 w-4 text-accent-fg" aria-hidden />
          <CardTitle className="text-base">API keys</CardTitle>
        </div>
        <p className="text-sm text-muted">
          A key lets an agent act as you over MCP and the HTTP API, never reaching further than
          your own permissions. Keys cannot manage keys, so this card is the only place to mint
          or revoke one.
        </p>
      </CardHeader>
      <CardContent className="space-y-5">
        {canManage ? (
          <>
            <form
              className="flex flex-col gap-4"
              onSubmit={(event) => {
                event.preventDefault();
                create.mutate();
              }}
            >
              <label className="flex flex-col gap-1.5" htmlFor="api-key-name">
                <span className="text-sm font-medium">Name</span>
                <Input
                  id="api-key-name"
                  value={name}
                  onChange={(event) => setName(event.target.value)}
                  placeholder="Claude Code on my laptop"
                  maxLength={120}
                />
                <span className="text-xs text-muted">
                  The name is all you will have to go on when deciding which key to revoke.
                </span>
              </label>
              <div className="grid gap-3 sm:grid-cols-2">
                <label className="flex flex-col gap-1.5">
                  <span className="text-sm font-medium">Access</span>
                  <Select value={scope} onValueChange={(value) => setScope(value as 'read' | 'write')}>
                    <SelectTrigger>
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      <SelectGroup>
                        <SelectItem value="read">Read only</SelectItem>
                        <SelectItem value="write">Read and write</SelectItem>
                      </SelectGroup>
                    </SelectContent>
                  </Select>
                </label>
                <label className="flex flex-col gap-1.5" htmlFor="api-key-expiry">
                  <span className="text-sm font-medium">Expires, optional</span>
                  <Input
                    id="api-key-expiry"
                    type="datetime-local"
                    value={expiresAt}
                    onChange={(event) => setExpiresAt(event.target.value)}
                  />
                </label>
              </div>
              {create.error ? (
                <p role="alert" className="text-sm text-danger">
                  {create.error.message}
                </p>
              ) : null}
              <Button type="submit" variant="primary" disabled={create.isPending || !name.trim()}>
                {create.isPending ? 'Creating…' : 'Create API key'}
              </Button>
            </form>

            <div className="flex flex-col gap-2">
              <p className="text-xs font-medium uppercase tracking-wide text-muted">Your keys</p>
              {isLoading ? (
                <p className="py-6 text-center text-sm text-muted">Loading keys…</p>
              ) : data.length === 0 ? (
                <p className="rounded-xl border border-dashed border-border px-4 py-8 text-center text-sm text-muted">
                  No keys yet. Create one above, then paste it into the snippet under “Connect
                  your agent”.
                </p>
              ) : (
                data.map((key) => (
                  <ApiKeyRow
                    key={key.id}
                    apiKey={key}
                    ownerName={me.name}
                    onRevoke={() => setPendingRevoke(key)}
                  />
                ))
              )}
            </div>
          </>
        ) : (
          <p className="text-sm text-muted">
            You are signed in with an API key, and a key may not create, list or revoke
            credentials. Open PIG in a browser session to manage keys.
          </p>
        )}
      </CardContent>

      {issued ? <IssuedKeyDialog issued={issued} onDismiss={() => setIssued(null)} /> : null}

      {/*
        A `window.confirm` here would block the whole tab — and on iOS it is
        dismissed by the same tap that opens it often enough to revoke a key by
        accident. The dialog names the credential instead, because "are you
        sure?" is not a question anyone can answer about a list of six keys.
      */}
      {pendingRevoke ? (
        <Dialog
          open
          onOpenChange={(open) => {
            if (!open) setPendingRevoke(null);
          }}
        >
          <DialogContent className="max-w-md">
            <DialogHeader>
              {/* The close button is absolutely positioned in the same corner,
                  so a long key name would otherwise run underneath it. */}
              <DialogTitle className="pr-10">Revoke “{pendingRevoke.name}”?</DialogTitle>
              <DialogDescription>
                Anything still holding {pendingRevoke.keyPrefix}… stops authenticating
                immediately, including agents running unattended. Revocation cannot be undone; a
                replacement is a new key.
              </DialogDescription>
            </DialogHeader>
            <DialogFooter className="gap-2">
              <Button type="button" variant="outline" onClick={() => setPendingRevoke(null)}>
                Keep it
              </Button>
              <Button
                type="button"
                variant="danger"
                disabled={revoke.isPending}
                onClick={() => revoke.mutate(pendingRevoke)}
              >
                {revoke.isPending ? 'Revoking…' : 'Revoke key'}
              </Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      ) : null}
    </Card>
  );
}

function ApiKeyRow({
  apiKey,
  ownerName,
  onRevoke,
}: {
  apiKey: ApiKey;
  ownerName: string;
  onRevoke(): void;
}) {
  const status = keyStatus(apiKey);

  return (
    <div 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">{apiKey.name}</p>
          <Badge tone={status.tone}>{status.label}</Badge>
          <Badge tone="accent">{apiKey.scopes.includes('write') ? 'read + write' : 'read'}</Badge>
        </div>
        <p className="mt-1 break-all font-mono text-xs text-muted">{apiKey.keyPrefix}…</p>
        <p className="mt-1 text-xs text-muted">
          <span title={new Date(apiKey.createdAt).toLocaleString()}>
            Created {relativeTime(apiKey.createdAt)}
          </span>{' '}
          by {ownerName} ·{' '}
          {apiKey.lastUsedAt ? `last used ${relativeTime(apiKey.lastUsedAt)}` : 'never used'}
          {apiKey.expiresAt ? ` · expires ${shortDate(apiKey.expiresAt)}` : ''}
        </p>
      </div>
      {apiKey.revokedAt ? null : (
        <Button type="button" size="sm" variant="outline" onClick={onRevoke}>
          Revoke
        </Button>
      )}
    </div>
  );
}

/**
 * The show-once secret.
 *
 * PIG stores only a hash, so this dialog holds the single copy of the key that
 * will ever exist. Escape and click-away are how a dialog gets dismissed by
 * accident, and here an accident destroys a credential — so both are refused,
 * and the close button says plainly what it will cost until the key has been
 * copied.
 */
function IssuedKeyDialog({ issued, onDismiss }: { issued: IssuedApiKey; onDismiss(): void }) {
  const [copied, setCopied] = useState(false);

  async function copy() {
    if (await copyToClipboard(issued.key, 'API key copied')) setCopied(true);
  }

  return (
    <Dialog
      open
      onOpenChange={(open) => {
        if (!open) onDismiss();
      }}
    >
      <DialogContent
        className="max-w-lg"
        onEscapeKeyDown={(event) => event.preventDefault()}
        onInteractOutside={(event) => event.preventDefault()}
      >
        <DialogHeader>
          <DialogTitle className="pr-10">Copy “{issued.name}” now</DialogTitle>
          <DialogDescription>
            This is the only time PIG will show this key — the server keeps nothing but a hash of
            it. Close this without copying and the key is unrecoverable; you would have to create
            another.
          </DialogDescription>
        </DialogHeader>

        <div className="rounded-xl border border-warning bg-warning/10 p-3">
          <div className="flex min-w-0 items-center gap-2">
            {/* `select-all` makes one tap select the whole key, which is the
                fallback that matters when the clipboard API is unavailable. */}
            <code className="min-w-0 flex-1 select-all break-all font-mono text-xs">
              {issued.key}
            </code>
            <Button
              type="button"
              size="icon"
              variant="ghost"
              aria-label="Copy API key"
              onClick={() => void copy()}
            >
              {copied ? <Check aria-hidden /> : <Copy aria-hidden />}
            </Button>
          </div>
        </div>

        <p className="text-xs text-muted">
          Set it as <code className="font-mono">PIG_API_KEY</code> where your agent runs. Scope:{' '}
          {issued.scopes.includes('write') ? 'read and write' : 'read only'}.
        </p>

        <DialogFooter className="gap-2">
          <Button type="button" variant={copied ? 'primary' : 'outline'} onClick={onDismiss}>
            {copied ? 'Done' : 'Close without copying'}
          </Button>
          <Button
            type="button"
            variant={copied ? 'outline' : 'primary'}
            onClick={() => void copy()}
          >
            <Copy className="h-4 w-4" aria-hidden />
            {copied ? 'Copy again' : 'Copy key'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

/**
 * Signing out.
 *
 * There was no way to do this at all until now, which is an easy omission to
 * make when developing signed in and a genuinely stranded feeling for anyone
 * on a shared machine.
 *
 * Clearing the identity provider's session is enough: PIG holds no session of
 * its own, and the auth listener in App.tsx notices and returns to sign-in
 * without a reload.
 */
function SessionCard() {
  const [busy, setBusy] = useState(false);

  async function signOut() {
    setBusy(true);
    try {
      await getSupabase()?.auth.signOut();
    } finally {
      // Belt and braces: if the provider call fails, a reload still lands on
      // the sign-in screen rather than leaving a half-signed-out interface.
      window.location.href = '/';
    }
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base">Session</CardTitle>
        <p className="text-sm text-muted">
          Signing out clears this browser only. API keys you have issued keep working —
          revoke them under Agent access.
        </p>
      </CardHeader>
      <CardContent>
        <Button type="button" variant="outline" onClick={signOut} disabled={busy}>
          <LogOut className="h-4 w-4" aria-hidden />
          {busy ? 'Signing out…' : 'Sign out'}
        </Button>
      </CardContent>
    </Card>
  );
}
