import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Crown, Trash2, Loader2, KeyRound, RotateCcw, Copy, SquareTerminal as TerminalIcon } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { CreateUserForm } from './CreateUserForm';
import { copyToClipboard } from 'helpers/clipboard';

type ManagedUser = {
  id: number;
  email: string;
  name: string | null;
  username: string | null;
  status: string;
  role: string;
  createdAt: string;
  isOwner: boolean;
  osUser: string | null;
  osSshPublicKey: string | null;
};

type UsersResponse = {
  users: ManagedUser[];
  /** Every role, for displaying the owner's own value. */
  roles: string[];
  /** Roles the server will accept in a write. Excludes the owner role — both write paths refuse it. */
  assignableRoles: string[];
  ownerId: number;
};

const USERS_KEY = ['MANAGED_USERS'];

export const UsersSection = () => {
  const client = useClient();
  const queryClient = useQueryClient();
  const [pendingId, setPendingId] = useState<number | null>(null);
  const [confirmDelete, setConfirmDelete] = useState<ManagedUser | null>(null);
  const [confirmReset, setConfirmReset] = useState<ManagedUser | null>(null);
  /** The one and only sighting of a generated password. Cleared when the dialog closes, and gone for good. */
  const [newPassword, setNewPassword] = useState<{ email: string; password: string } | null>(null);

  const { data, isLoading, isError } = useQuery<UsersResponse>({
    queryKey: USERS_KEY,
    queryFn: () => client.get<UsersResponse>('/users'),
  });

  const changeRole = async (user: ManagedUser, role: string) => {
    if (role === user.role) return;
    setPendingId(user.id);
    try {
      await client.patch(`/users/${user.id}/role`, { role });
      await queryClient.invalidateQueries({ queryKey: USERS_KEY });
      toast.success(`${user.email} is now ${role}`);
    } catch (ex) {
      toast.error(ex instanceof Error ? ex.message : 'Could not change the role');
    } finally {
      setPendingId(null);
    }
  };

  /**
   * Create or repair the account's Linux side in place.
   *
   * Prompts for a key rather than putting a whole form on the row: replacing it is the rarer of the two
   * reasons to press this, and an empty answer means "leave authorized_keys alone" rather than "remove it".
   */
  const provisionLinux = async (user: ManagedUser) => {
    const key = window.prompt(
      `Linux account for ${user.email}.\n\n` +
        `Paste an SSH public key to allow them to SSH in, or leave empty to keep the current one.`,
      '',
    );
    // Cancel is null; empty string is a deliberate "no change".
    if (key === null) return;

    setPendingId(user.id);
    try {
      const result = await client.post<{ osUser: string | null; sshPublicKey: string | null; error: string | null }>(
        `/users/${user.id}/provision-linux`,
        { sshPublicKey: key.trim() },
      );
      await queryClient.invalidateQueries({ queryKey: USERS_KEY });
      // Partial success is the interesting case and must not read as a clean win: the account can exist and
      // be confined while the keys failed.
      if (result.error) {
        toast.warning(result.osUser ? `${result.osUser} created, but not finished` : 'Could not finish', {
          description: result.error,
          duration: 30_000,
        });
      } else {
        toast.success(`${result.osUser} is ready`);
      }
    } catch (ex) {
      toast.error(ex instanceof Error ? ex.message : 'Could not provision the Linux account');
    } finally {
      setPendingId(null);
    }
  };

  /**
   * A new platform password, generated by the server and shown once.
   *
   * Generated rather than typed because the failure this exists for is "I forgot to copy it down", and an
   * owner typing a replacement can lose it the same way twice. Only the argon2 hash is stored, so the
   * dialog below really is the only time anyone sees it — which is why it is a dialog and not a toast.
   */
  const resetPassword = async (user: ManagedUser) => {
    setPendingId(user.id);
    setConfirmReset(null);
    try {
      const result = await client.post<{ email: string; password: string }>(`/users/${user.id}/password`, {});
      setNewPassword(result);
      await queryClient.invalidateQueries({ queryKey: USERS_KEY });
    } catch (ex) {
      toast.error(ex instanceof Error ? ex.message : 'Could not reset the password');
    } finally {
      setPendingId(null);
    }
  };

  const remove = async (user: ManagedUser) => {
    setPendingId(user.id);
    setConfirmDelete(null);
    try {
      await client.delete(`/users/${user.id}`);
      await queryClient.invalidateQueries({ queryKey: USERS_KEY });
      toast.success(`${user.email} removed`);
    } catch (ex) {
      toast.error(ex instanceof Error ? ex.message : 'Could not remove the account');
    } finally {
      setPendingId(null);
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center gap-2 text-sm text-muted-foreground">
        <Loader2 className="h-4 w-4 animate-spin" /> Loading accounts…
      </div>
    );
  }

  if (isError || !data) {
    return <div className="text-sm text-destructive">Could not load the accounts.</div>;
  }

  return (
    <div className="space-y-4">
      <p className="text-sm text-muted-foreground">
        Every account on this server. The owner is fixed — the database itself refuses to demote or remove it — so that
        row cannot be changed from here, and no other account can be promoted into it.
      </p>

      <CreateUserForm roles={data.assignableRoles} usersKey={USERS_KEY} />

      <div className="rounded-lg border divide-y">
        {data.users.map((user) => {
          const busy = pendingId === user.id;
          return (
            <div key={user.id} className="flex items-center gap-3 p-3">
              <div className="min-w-0 flex-1">
                <div className="flex items-center gap-1.5">
                  <span className="truncate text-sm font-medium">{user.name || user.username || user.email}</span>
                  {user.isOwner && <Crown className="h-3.5 w-3.5 shrink-0 text-duck-teal" aria-label="Server owner" />}
                </div>
                <div className="truncate text-xs text-muted-foreground">
                  {user.email}
                  {user.status !== 'Active' && ` · ${user.status}`}
                  {/* Shown because "does this person have a Linux account" is otherwise invisible, and it
                      decides whether their terminal and agent run as them or not at all. */}
                  {user.osUser && ` · ${user.osUser}`}
                </div>
              </div>

              <Select
                value={user.role}
                disabled={user.isOwner || busy}
                onValueChange={(role) => void changeRole(user, role)}
              >
                <SelectTrigger className="w-40 shrink-0">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {/* The owner's row needs its own value present to render at all, and it is disabled
                      anyway. Every other row offers only what the server will accept. */}
                  {(user.isOwner ? data.roles : data.assignableRoles).map((role) => (
                    <SelectItem key={role} value={role}>
                      {role}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>

              {/* Create or repair the Linux side. Offered for anyone lacking an account (backfill) and for
                  anyone who has one (retry after fixing a host problem, or replace their key) — the
                  underlying operation is idempotent, so there is no state where pressing it is wrong. */}
              {!user.isOwner && (
                <Button
                  variant="ghost"
                  size="icon"
                  className={`shrink-0 ${user.osUser ? 'text-muted-foreground' : 'text-amber-500'}`}
                  disabled={busy}
                  aria-label={user.osUser ? `Repair ${user.email}'s Linux account` : `Create a Linux account`}
                  title={
                    user.osUser
                      ? `Linux account: ${user.osUser} — click to repair or replace their SSH key`
                      : 'No Linux account — click to create one'
                  }
                  onClick={() => void provisionLinux(user)}
                >
                  {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <TerminalIcon className="h-4 w-4" />}
                </Button>
              )}

              {/* The errand the create screen promised would still be here: this key has to end up on
                  their Gitea account, and nothing else will remind anyone. */}
              {user.osSshPublicKey && (
                <Button
                  variant="ghost"
                  size="icon"
                  className="shrink-0 text-muted-foreground"
                  aria-label={`Copy ${user.email}'s SSH public key`}
                  title="Copy their SSH public key (add it to their Gitea account)"
                  onClick={() => {
                    void copyToClipboard(user.osSshPublicKey!);
                    toast.success('Public key copied');
                  }}
                >
                  <KeyRound className="h-4 w-4" />
                </Button>
              )}

              {/* The owner is excluded because they have change-password, which asks for the current one —
                  and resetting themselves from here would sign them out of the session doing it. */}
              {!user.isOwner && (
                <Button
                  variant="ghost"
                  size="icon"
                  className="shrink-0 text-muted-foreground"
                  disabled={busy}
                  aria-label={`Reset ${user.email}'s password`}
                  title="Generate a new password — shown once, and signs them out everywhere"
                  onClick={() => setConfirmReset(user)}
                >
                  {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCcw className="h-4 w-4" />}
                </Button>
              )}

              <Button
                variant="ghost"
                size="icon"
                className="shrink-0 text-muted-foreground hover:text-destructive"
                disabled={user.isOwner || busy}
                onClick={() => setConfirmDelete(user)}
                aria-label={`Remove ${user.email}`}
              >
                {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
              </Button>
            </div>
          );
        })}
      </div>

      {/* Removal cascades — passkeys, dashboards, screens, email accounts, playlists — and there is no
          undo, so it is worth one deliberate confirmation. */}
      <AlertDialog open={!!confirmDelete} onOpenChange={(open) => !open && setConfirmDelete(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Remove {confirmDelete?.email}?</AlertDialogTitle>
            <AlertDialogDescription>
              This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, email
              accounts and playlists. It cannot be undone.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              onClick={() => confirmDelete && void remove(confirmDelete)}
            >
              Remove account
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

      {/* Confirmed rather than immediate: this ends every session the account has, including one they may
          be in the middle of using. Not destructive enough for the red button, so it keeps the default. */}
      <AlertDialog open={!!confirmReset} onOpenChange={(open) => !open && setConfirmReset(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Reset the password for {confirmReset?.email}?</AlertDialogTitle>
            <AlertDialogDescription>
              A new password is generated and shown to you once — it is not stored anywhere and cannot be looked up
              afterwards. Their existing password stops working immediately, and they are signed out everywhere.
              {confirmReset?.osUser ? (
                <>
                  {' '}
                  Their Linux account ({confirmReset.osUser}) is not affected: it has no password, and SSH keys are
                  unchanged.
                </>
              ) : null}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction onClick={() => confirmReset && void resetPassword(confirmReset)}>
              Generate a new password
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

      {/* The only time this password is ever visible. A dialog rather than a toast for exactly that reason:
          a toast that times out while somebody is finding a pen loses the thing they came for. */}
      <AlertDialog open={!!newPassword} onOpenChange={(open) => !open && setNewPassword(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>New password for {newPassword?.email}</AlertDialogTitle>
            <AlertDialogDescription>
              Copy it now and give it to them. Only its hash is stored, so closing this dialog is the last anyone sees
              of it — if it is lost, generate another one.
            </AlertDialogDescription>
          </AlertDialogHeader>

          <div className="flex items-center gap-2 rounded-md border bg-muted/50 p-3">
            <code className="flex-1 select-all break-all font-mono text-sm">{newPassword?.password}</code>
            <Button
              variant="ghost"
              size="icon"
              className="shrink-0"
              aria-label="Copy the new password"
              onClick={() => {
                if (!newPassword) return;
                void copyToClipboard(newPassword.password).then((ok) =>
                  ok ? toast.success('Password copied') : toast.error('Could not copy — select it and copy by hand'),
                );
              }}
            >
              <Copy className="h-4 w-4" />
            </Button>
          </div>

          <AlertDialogFooter>
            <AlertDialogAction onClick={() => setNewPassword(null)}>Done</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  );
};
