import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Loader2, UserPlus, Dices, Copy, X } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { copyToClipboard } from 'helpers/clipboard';

// The owner creating an account. Until this existed the only way to add one was an INSERT in Postgres.
//
// The password is visible, not masked, and that is the point: the owner has to be able to read it back
// to the person they are creating it for. Masking a value nobody has yet only makes it easy to typo
// twice. When there is an invite flow this whole field goes away.

type CreateUserFormProps = {
  /** Roles the server will actually accept. Excludes the owner role — see manage-users.ts. */
  roles: string[];
  /** Invalidated on success so the list below refreshes. */
  usersKey: readonly unknown[];
};

// Mirrors validatePassword on the server: length, both cases, a digit and a symbol. Generated rather
// than demanded so the owner is not sitting there inventing one that passes.
function generatePassword(): string {
  const lower = 'abcdefghijkmnopqrstuvwxyz';
  const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  const digits = '23456789';
  const symbols = '!@#$%^&*-_=+';
  const all = lower + upper + digits + symbols;

  const pick = (set: string, count: number) =>
    Array.from(crypto.getRandomValues(new Uint32Array(count)), (n) => set[n % set.length]!);

  // One of each class first, so the result cannot fail the server's rules by chance, then filled out.
  const chars = [...pick(lower, 4), ...pick(upper, 3), ...pick(digits, 3), ...pick(symbols, 2), ...pick(all, 8)];

  // Shuffled so the classes are not in fixed positions. Fisher-Yates with crypto randomness.
  const noise = crypto.getRandomValues(new Uint32Array(chars.length));
  for (let i = chars.length - 1; i > 0; i--) {
    const j = noise[i]! % (i + 1);
    [chars[i], chars[j]] = [chars[j]!, chars[i]!];
  }
  return chars.join('');
}

const EMPTY = { email: '', name: '', username: '', password: '', role: 'Member', sshPublicKey: '' };

/**
 * What the server did, shown after the fact.
 *
 * Kept on screen rather than announced in a toast because two of these values are only obtainable now: the
 * password is stored as an argon2 hash, and the generated public key sits in a 700 home. A toast that
 * carries something unrecoverable is a toast that gets dismissed by a stray click.
 */
type CreatedAccount = {
  email: string;
  password: string;
  osUser: string | null;
  osSshPublicKey: string | null;
  osUserError: string | null;
};

export const CreateUserForm = ({ roles, usersKey }: CreateUserFormProps) => {
  const client = useClient();
  const queryClient = useQueryClient();
  const [open, setOpen] = useState(false);
  const [saving, setSaving] = useState(false);
  const [form, setForm] = useState(EMPTY);
  const [created, setCreated] = useState<CreatedAccount | null>(null);

  const set = (key: keyof typeof EMPTY) => (value: string) => setForm((prev) => ({ ...prev, [key]: value }));

  const close = () => {
    setOpen(false);
    setForm(EMPTY);
    setCreated(null);
  };

  const submit = async (ev: React.FormEvent) => {
    ev.preventDefault();
    setSaving(true);
    try {
      const res = await client.post<{
        user: { osUser: string | null; osSshPublicKey: string | null };
        osUserError: string | null;
      }>('/users', form);
      await queryClient.invalidateQueries({ queryKey: usersKey });
      setCreated({
        email: form.email,
        password: form.password,
        osUser: res.user.osUser,
        osSshPublicKey: res.user.osSshPublicKey,
        osUserError: res.osUserError,
      });
    } catch (ex) {
      // The server's message is the useful one here — which field, and why.
      toast.error(ex instanceof Error ? ex.message : 'Could not create the account');
    } finally {
      setSaving(false);
    }
  };

  const copy = (value: string, what: string) => {
    void copyToClipboard(value);
    toast.success(`${what} copied`);
  };

  // ── After creation ──
  //
  // Deliberately a wall you have to dismiss. Both values below are unrecoverable once this closes, and the
  // public key has a job attached to it that nothing else will remind you to do.
  if (created) {
    return (
      <div className="space-y-4 rounded-lg border border-duck-teal/40 bg-duck-teal/5 p-4">
        <div>
          <h3 className="text-sm font-medium">{created.email} created</h3>
          <p className="text-xs text-muted-foreground">
            Copy what you need before closing — none of it can be shown again.
          </p>
        </div>

        <div className="space-y-1.5">
          <Label>Password</Label>
          <div className="flex gap-2">
            <Input readOnly value={created.password} className="font-mono" />
            <Button type="button" variant="outline" size="icon" onClick={() => copy(created.password, 'Password')}>
              <Copy className="h-4 w-4" />
            </Button>
          </div>
          <p className="text-xs text-muted-foreground">
            Stored as a hash — this is the only time it exists in readable form. They can change it from their own
            profile once signed in.
          </p>
        </div>

        {created.osUser && (
          <div className="space-y-1.5">
            <Label>Linux account</Label>
            <Input readOnly value={created.osUser} className="font-mono" />
          </div>
        )}

        {created.osSshPublicKey && (
          <div className="space-y-1.5">
            <Label>Their SSH public key</Label>
            <div className="flex gap-2">
              <Textarea readOnly value={created.osSshPublicKey} rows={3} className="font-mono text-xs" />
              <Button
                type="button"
                variant="outline"
                size="icon"
                onClick={() => copy(created.osSshPublicKey!, 'Public key')}
              >
                <Copy className="h-4 w-4" />
              </Button>
            </div>
            {/* The one action this screen cannot do for you. Without it their pushes fail with a
                permission error that says nothing about a missing key. */}
            <p className="text-xs text-muted-foreground">
              Generated on the machine; the private half never leaves it.{' '}
              <strong>Add this to their Gitea account</strong> so they can push. Retrievable later from their row.
            </p>
          </div>
        )}

        {created.osUserError && (
          <div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-xs">
            <div className="font-medium">The account works, but its Linux side did not finish</div>
            <p className="mt-1 whitespace-pre-wrap text-muted-foreground">{created.osUserError}</p>
          </div>
        )}

        <Button type="button" onClick={close}>
          Done
        </Button>
      </div>
    );
  }

  if (!open) {
    return (
      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
        <UserPlus className="mr-2 h-4 w-4" />
        Add account
      </Button>
    );
  }

  return (
    <form onSubmit={submit} className="space-y-4 rounded-lg border p-4">
      <div className="flex items-start justify-between gap-2">
        <div>
          <h3 className="text-sm font-medium">New account</h3>
          <p className="text-xs text-muted-foreground">
            Created active — they can sign in straight away. Tell them the password; it is not recoverable afterwards.
          </p>
        </div>
        <Button type="button" variant="ghost" size="icon" onClick={close} aria-label="Cancel">
          <X className="h-4 w-4" />
        </Button>
      </div>

      <div className="grid gap-4 sm:grid-cols-2">
        <div className="space-y-1.5">
          <Label htmlFor="new-user-email">Email</Label>
          <Input
            id="new-user-email"
            type="email"
            autoComplete="off"
            value={form.email}
            onChange={(ev) => set('email')(ev.target.value)}
            required
          />
        </div>

        <div className="space-y-1.5">
          <Label htmlFor="new-user-name">Name</Label>
          <Input
            id="new-user-name"
            autoComplete="off"
            value={form.name}
            onChange={(ev) => set('name')(ev.target.value)}
            required
          />
        </div>

        <div className="space-y-1.5">
          <Label htmlFor="new-user-username">Username</Label>
          <Input
            id="new-user-username"
            autoComplete="off"
            value={form.username}
            onChange={(ev) => set('username')(ev.target.value)}
            required
          />
          <p className="text-xs text-muted-foreground">Letters, numbers, dots, hyphens and underscores.</p>
        </div>

        <div className="space-y-1.5">
          <Label htmlFor="new-user-role">Role</Label>
          <Select value={form.role} onValueChange={set('role')}>
            <SelectTrigger id="new-user-role">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {roles.map((role) => (
                <SelectItem key={role} value={role}>
                  {role}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <p className="text-xs text-muted-foreground">
            What the role may reach is set under Permissions. A role with no grants can sign in and reach nothing but
            its own profile.
          </p>
        </div>

        <div className="space-y-1.5 sm:col-span-2">
          <Label htmlFor="new-user-password">Password</Label>
          <div className="flex gap-2">
            <Input
              id="new-user-password"
              // Deliberately visible — see the note at the top of this file.
              type="text"
              autoComplete="off"
              spellCheck={false}
              className="font-mono"
              value={form.password}
              onChange={(ev) => set('password')(ev.target.value)}
              required
            />
            <Button type="button" variant="outline" size="icon" onClick={() => set('password')(generatePassword())}>
              <Dices className="h-4 w-4" />
              <span className="sr-only">Generate a password</span>
            </Button>
            <Button
              type="button"
              variant="outline"
              size="icon"
              disabled={!form.password}
              onClick={() => {
                void copyToClipboard(form.password);
                toast.success('Password copied');
              }}
            >
              <Copy className="h-4 w-4" />
              <span className="sr-only">Copy the password</span>
            </Button>
          </div>
          <p className="text-xs text-muted-foreground">
            At least 12 characters, with upper and lower case, a number and a symbol.
          </p>
        </div>

        {/* Inbound only, and optional. The OUTBOUND key is generated either way — pasting one here does
            not replace it, because a key on a laptop is no use to an agent running on the server. */}
        <div className="space-y-1.5 sm:col-span-2">
          <Label htmlFor="new-user-ssh">
            Their SSH public key <span className="ml-1 text-xs opacity-60">(optional)</span>
          </Label>
          <Textarea
            id="new-user-ssh"
            rows={3}
            spellCheck={false}
            placeholder="ssh-ed25519 AAAAC3Nza… ana@laptop"
            className="font-mono text-xs"
            value={form.sshPublicKey}
            onChange={(ev) => set('sshPublicKey')(ev.target.value)}
          />
          <p className="text-xs text-muted-foreground">
            Lets them SSH into this machine as their own Linux user. Leave empty for platform-only access — either way
            they get a keypair of their own for pushing to Gitea, and you will be shown its public half next.
          </p>
        </div>
      </div>

      <div className="flex gap-2">
        <Button type="submit" disabled={saving}>
          {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
          Create account
        </Button>
        <Button type="button" variant="ghost" onClick={close} disabled={saving}>
          Cancel
        </Button>
      </div>
    </form>
  );
};
