import type { BackendKind } from '../shared';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { Loader2, TriangleAlert } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
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 { KIND_LABELS, walletSectionPath, type WalletSectionId } from '../shared';
import { errorMessage, useWalletConfig, useWalletLifecycle } from '../useWalletData';
import { SeedBackupDialog } from './SeedBackupDialog';
import { Textarea } from '@/components/ui/textarea';
import {
  SeedPhraseInput,
  SEED_LENGTHS,
  findOrderFix,
  isAezeed,
  normalizePhrase,
  phraseProblem,
  type SeedLength,
} from './SeedPhraseInput';

// Add a wallet. Two genuinely different shapes behind one form:
//
//   onchain  — self-custodial. A seed is generated (or imported) and sealed under a passphrase that only
//              ever exists in this component's state and the request body. Generating returns the mnemonic
//              exactly once, which is why this dialog hands straight over to SeedBackupDialog and does not
//              close until the owner confirms they wrote it down.
//   everything else — a connection to somebody's node or account, so it is a config blob, no seed at all.
//
// Wallet creation is refused outright when the wallet's at-rest key is unreachable. The form says so up front
// rather than letting the submit fail with a crypto error.

type CreateWalletDialogProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  /** Where to land after creating — keeps the new wallet's id in the URL. */
  section: WalletSectionId;
};

const KINDS: BackendKind[] = ['onchain', 'lnd', 'cln-rest', 'lndhub', 'nwc'];

/** The connection fields each remote backend needs, spelled once. */
const CONFIG_FIELDS: Record<Exclude<BackendKind, 'onchain'>, { key: string; label: string; placeholder: string }[]> = {
  lnd: [
    { key: 'url', label: 'REST URL', placeholder: 'https://node.local:8080' },
    { key: 'macaroon', label: 'Admin macaroon (hex)', placeholder: '0201036c6e64…' },
  ],
  'cln-rest': [
    { key: 'url', label: 'CLNRest URL', placeholder: 'https://node.local:3010' },
    { key: 'rune', label: 'Rune', placeholder: 'ZW5jcnlwdGVkOg…' },
  ],
  lndhub: [
    { key: 'url', label: 'LNDHub URL', placeholder: 'https://lndhub.io' },
    { key: 'login', label: 'Login', placeholder: 'lndhub login' },
    { key: 'password', label: 'Password', placeholder: 'lndhub password' },
  ],
  nwc: [{ key: 'connectionString', label: 'Connection string', placeholder: 'nostr+walletconnect://…' }],
};

export const CreateWalletDialog = ({ open, onOpenChange, section }: CreateWalletDialogProps) => {
  const navigate = useNavigate();
  const { config } = useWalletConfig();
  const { create } = useWalletLifecycle();

  const [name, setName] = useState('');
  const [kind, setKind] = useState<BackendKind>('onchain');
  const [words, setWords] = useState<12 | 24>(24);
  const [importing, setImporting] = useState(false);
  // One entry per word rather than one string: the boxes ARE the state, so a wrong word is visible where
  // it is rather than somewhere in a wall of text. Joined back into a phrase only at submit.
  const [importLength, setImportLength] = useState<SeedLength>(24);
  const [importWords, setImportWords] = useState<string[]>(() => Array(24).fill(''));
  // A plain-text escape hatch alongside the boxes. Both write the same `importWords`, so validation and
  // submit never care which was used — but when a phrase the owner knows is right will not validate, this
  // is what separates "my per-box entry mangled it" from "the phrase really is different".
  const [asText, setAsText] = useState(false);
  const [importText, setImportText] = useState('');
  // Both of these are secrets in flight. They exist in this component's state, go into the request body,
  // and are cleared the moment the request settles — never a store, never a query key, never the URL.
  const [passphrase, setPassphrase] = useState('');
  const [confirmPassphrase, setConfirmPassphrase] = useState('');
  const [bip39Passphrase, setBip39Passphrase] = useState('');
  const [makeActive, setMakeActive] = useState(true);
  const [remoteConfig, setRemoteConfig] = useState<Record<string, string>>({});

  // The one mnemonic the sidecar will ever hand back. Held here only until the backup modal is confirmed.
  const [pendingSeed, setPendingSeed] = useState<{ mnemonic: string; walletName: string; walletId: number } | null>(
    null,
  );

  const storeKeyMissing = config != null && !config.storeKeyConfigured;
  const isOnchain = kind === 'onchain';

  /** Anything the owner would be upset to retype — guards the incidental-dismiss paths below. */
  const hasTypedSecret =
    importWords.some((w) => w.trim()) || passphrase.length > 0 || Object.values(remoteConfig).some((v) => v?.trim());

  /** Grow or shrink the grid, keeping whatever has already been typed. */
  const resizeImport = (n: SeedLength) => {
    setImportLength(n);
    setImportWords((prev) => Array.from({ length: n }, (_, i) => prev[i] ?? ''));
  };

  /** Text mode is the source of truth while it is open; the boxes mirror it word for word. */
  const applyText = (text: string) => {
    setImportText(text);
    const parts = normalizePhrase(text);
    if ((SEED_LENGTHS as readonly number[]).includes(parts.length)) setImportLength(parts.length as SeedLength);
    setImportWords(parts);
  };

  /** Switching modes must not silently drop or reorder anything the owner already entered. */
  const toggleTextMode = (next: boolean) => {
    if (next) setImportText(importWords.filter(Boolean).join(' '));
    else setImportWords((prev) => Array.from({ length: importLength }, (_, i) => prev[i] ?? ''));
    setAsText(next);
  };

  const clearSecrets = () => {
    setPassphrase('');
    setConfirmPassphrase('');
    setBip39Passphrase('');
    // Functional, so it blanks whatever length the grid currently is without reading stale state.
    setImportWords((prev) => prev.map(() => ''));
    setImportText('');
    setRemoteConfig({});
  };

  const reset = () => {
    setName('');
    setKind('onchain');
    setWords(24);
    setImporting(false);
    setMakeActive(true);
    clearSecrets();
    setAsText(false);
    setImportLength(24);
    setImportWords(Array(24).fill(''));
  };

  const close = () => {
    reset();
    onOpenChange(false);
  };

  const passphraseValid = !isOnchain || (passphrase.length >= 8 && passphrase === confirmPassphrase);
  const configValid =
    isOnchain || CONFIG_FIELDS[kind as Exclude<BackendKind, 'onchain'>].every((f) => remoteConfig[f.key]?.trim());
  // Checked locally so a bad phrase never costs a round trip. The sidecar still validates independently;
  // this only means the owner finds out instantly, and precisely, instead of via a generic 400.
  const seedProblem = isOnchain && importing ? phraseProblem(importWords) : null;
  // Only worth computing for the one failure a human review cannot catch: right words, wrong order.
  const orderFix = seedProblem?.kind === 'checksum' ? findOrderFix(importWords) : null;
  // A valid phrase can be either format, and the two are visually identical, so say which one this is.
  // It also changes what the passphrase field below means, which the owner has no way to guess.
  const aezeed = isOnchain && importing && !seedProblem && isAezeed(importWords.map((w) => w.trim().toLowerCase()));
  const canSubmit =
    !!name.trim() && passphraseValid && configValid && !seedProblem && !storeKeyMissing && !create.isPending;

  // A disabled button with no stated reason is a dead end — especially here, where the blocker is often
  // the passphrase fields further down while the owner is staring at the phrase.
  const blockReason = !name.trim()
    ? 'Give the wallet a name.'
    : seedProblem
      ? 'The recovery phrase above is not complete or not valid.'
      : !passphraseValid
        ? 'Set a passphrase of at least 8 characters, twice.'
        : !configValid
          ? 'Fill in the connection details.'
          : null;

  const submit = async (ev: React.FormEvent) => {
    ev.preventDefault();
    if (!canSubmit) return;

    let result;
    try {
      result = await create.mutateAsync(
        isOnchain
          ? {
              name: name.trim(),
              kind,
              passphrase,
              words,
              mnemonic: importing ? importWords.map((w) => w.trim()).join(' ') : undefined,
              bip39Passphrase: bip39Passphrase || undefined,
              makeActive,
            }
          : { name: name.trim(), kind, config: remoteConfig, makeActive },
      );
    } catch {
      // KEEP EVERYTHING TYPED. A rejected phrase is precisely the moment the owner needs their other 23
      // words back — clearing here means retyping the whole thing to fix one box, which is both miserable
      // and a good way to introduce a second mistake. `create.onError` already toasts, and the error is
      // rendered inline below. Nothing lingers regardless: cancel or close still runs clearSecrets().
      return;
    }

    const created = result.wallet;
    if (result.mnemonic) {
      // Generated seed: hold the words for the backup modal and keep this dialog mounted underneath.
      setPendingSeed({ mnemonic: result.mnemonic, walletName: created.name, walletId: created.id });
      clearSecrets();
    } else {
      navigate(walletSectionPath(section, created.id));
      close();
    }
  };

  const seedConfirmed = () => {
    const created = pendingSeed;
    setPendingSeed(null);
    close();
    if (created) navigate(walletSectionPath(section, created.walletId));
  };

  return (
    <>
      <Dialog open={open && pendingSeed == null} onOpenChange={(next) => (next ? onOpenChange(true) : close())}>
        {/* Wider only while importing — 24 word boxes at max-w-lg are two cramped columns. */}
        <DialogContent
          className={`max-h-[85vh] overflow-y-auto ${isOnchain && importing ? 'max-w-2xl' : 'max-w-lg'}`}
          // A half-typed recovery phrase must not be destroyed by a misplaced click on the overlay or a
          // stray Escape (dismissing an error toast, say). Those gestures are cheap and reversible for an
          // ordinary form; here they cost the owner all 24 words. Cancel and the X still close normally —
          // the point is that discarding becomes deliberate, not incidental.
          onInteractOutside={(ev) => {
            if (hasTypedSecret) ev.preventDefault();
          }}
          onEscapeKeyDown={(ev) => {
            if (hasTypedSecret) ev.preventDefault();
          }}
        >
          <form onSubmit={submit}>
            <DialogHeader>
              <DialogTitle>Add a wallet</DialogTitle>
              <DialogDescription>
                Self-custodial keys stay in the wallet sidecar; a remote node is only a stored connection.
              </DialogDescription>
            </DialogHeader>

            {storeKeyMissing && (
              <div className="mt-4 flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/5 p-3 text-xs">
                <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
                <span>
                  <span className="font-medium">The secret store is unreachable.</span> The sidecar refuses to store
                  wallet secrets without it, so creation is disabled until it is set.
                </span>
              </div>
            )}

            <div className="mt-4 space-y-4">
              <div className="space-y-1.5">
                <Label htmlFor="wallet-new-name">Name</Label>
                <Input
                  id="wallet-new-name"
                  value={name}
                  autoFocus
                  placeholder="Savings"
                  onChange={(ev) => setName(ev.target.value)}
                />
              </div>

              <div className="space-y-1.5">
                <Label htmlFor="wallet-new-kind">Type</Label>
                <Select value={kind} onValueChange={(v) => setKind(v as BackendKind)}>
                  <SelectTrigger id="wallet-new-kind">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {KINDS.map((k) => (
                      <SelectItem key={k} value={k}>
                        {KIND_LABELS[k]}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                {config && (
                  <p className="text-xs text-muted-foreground">
                    Network: <span className="font-medium">{config.network}</span>
                  </p>
                )}
              </div>

              {isOnchain ? (
                <>
                  <div className="flex gap-1.5">
                    <SegButton active={!importing} onClick={() => setImporting(false)} label="Generate a new seed" />
                    <SegButton active={importing} onClick={() => setImporting(true)} label="Import a phrase" />
                  </div>

                  {importing ? (
                    <div className="space-y-1.5">
                      <div className="flex flex-wrap items-center justify-between gap-2">
                        <Label>Recovery phrase</Label>
                        <SegButton
                          active={asText}
                          onClick={() => toggleTextMode(!asText)}
                          label={asText ? 'Use word boxes' : 'Paste as text'}
                        />
                      </div>

                      {!asText && (
                        <div className="flex flex-wrap gap-1.5">
                          {SEED_LENGTHS.map((n) => (
                            <SegButton
                              key={n}
                              active={importLength === n}
                              onClick={() => resizeImport(n)}
                              label={`${n}`}
                            />
                          ))}
                        </div>
                      )}

                      {/* Secret in flight: state → request body → cleared. Never stored on this side, and
                          never echoed back by the sidecar either. */}
                      {asText ? (
                        <Textarea
                          rows={4}
                          autoComplete="off"
                          spellCheck={false}
                          data-1p-ignore
                          data-lpignore="true"
                          value={importText}
                          placeholder="Paste or type the whole phrase. Numbering, commas and line breaks are ignored."
                          onChange={(ev) => applyText(ev.target.value)}
                          className="font-mono text-xs"
                        />
                      ) : (
                        <SeedPhraseInput
                          words={importWords}
                          onChange={setImportWords}
                          onLengthDetected={(n) => setImportLength(n as SeedLength)}
                        />
                      )}
                      {aezeed ? (
                        <div className="space-y-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/5 p-3">
                          <p className="text-xs">
                            <span className="font-medium text-emerald-600 dark:text-emerald-500">
                              Valid aezeed seed — checksum verified in this browser.
                            </span>{' '}
                            This is not a BIP39 phrase. It is an <em>aezeed</em> cipher seed, the format Zeus's embedded
                            LND node produces: the same 2048-word list, so it looks identical, but encrypted and
                            checksummed differently. Officer imports it.
                          </p>
                          <p className="text-xs text-muted-foreground">
                            This recovers the <span className="font-medium">on-chain</span> wallet. Lightning channels
                            are not in the seed — those need a channel backup file and a running LND.
                          </p>
                        </div>
                      ) : orderFix ? (
                        <div className="space-y-1.5 rounded-lg border border-amber-500/40 bg-amber-500/5 p-3">
                          <p className="text-xs">
                            <span className="font-medium">These are your words — the order is wrong.</span> Read down
                            the columns, they check out. Your backup is almost certainly printed as {orderFix.rows} rows
                            of {orderFix.cols}, running top-to-bottom, and was read left-to-right.
                          </p>
                          <Button
                            type="button"
                            size="sm"
                            variant="secondary"
                            onClick={() => {
                              setImportWords(orderFix.words);
                              setImportText(orderFix.words.join(' '));
                            }}
                          >
                            Reorder to the valid phrase
                          </Button>
                          <p className="text-xs text-muted-foreground">
                            Check the result against your backup before continuing.
                          </p>
                        </div>
                      ) : seedProblem?.kind === 'checksum' ? (
                        // The hardest case to act on, so it gets the longest explanation: every word is
                        // real, so nothing is marked red, and the only remaining faults are order or a
                        // word that is a near-miss for the right one.
                        <p className="text-xs text-destructive">
                          All {importWords.length} words are valid BIP39 words, but the phrase does not check out. A
                          word is in the wrong position, or one is a near-miss for the right one (<em>fan</em> where the
                          backup says <em>fancy</em>). Re-read it against your backup in order.
                        </p>
                      ) : seedProblem?.kind === 'unknown' ? (
                        <p className="text-xs text-destructive">
                          Not in the BIP39 word list: {seedProblem.boxes.length === 1 ? 'box' : 'boxes'}{' '}
                          {seedProblem.boxes.join(', ')}.
                        </p>
                      ) : seedProblem?.kind === 'incomplete' ? (
                        <p className="text-xs text-muted-foreground">
                          {seedProblem.boxes.length} {seedProblem.boxes.length === 1 ? 'word' : 'words'} still empty.
                        </p>
                      ) : seedProblem?.kind === 'length' ? (
                        <p className="text-xs text-destructive">
                          {seedProblem.count} words. A BIP39 phrase must be 12, 15, 18, 21 or 24.
                        </p>
                      ) : (
                        <p className="text-xs text-emerald-600 dark:text-emerald-500">
                          Valid phrase — checksum verified in this browser.
                        </p>
                      )}
                      <p className="text-xs text-muted-foreground">
                        Type a few letters and pick from the list, or paste the whole phrase into the first box.
                      </p>
                    </div>
                  ) : (
                    <div className="space-y-1.5">
                      <Label>Phrase length</Label>
                      <div className="flex gap-1.5">
                        <SegButton active={words === 12} onClick={() => setWords(12)} label="12 words" />
                        <SegButton active={words === 24} onClick={() => setWords(24)} label="24 words" />
                      </div>
                      <p className="text-xs text-muted-foreground">
                        The phrase is shown once, immediately after creation, and never again without this passphrase.
                      </p>
                    </div>
                  )}

                  <div className="grid gap-3 sm:grid-cols-2">
                    <div className="space-y-1.5">
                      <Label htmlFor="wallet-new-pass">Passphrase</Label>
                      {/* Seals the seed. Straight into the request body, cleared on settle — no store. */}
                      <Input
                        id="wallet-new-pass"
                        type="password"
                        autoComplete="new-password"
                        value={passphrase}
                        onChange={(ev) => setPassphrase(ev.target.value)}
                      />
                    </div>
                    <div className="space-y-1.5">
                      <Label htmlFor="wallet-new-pass2">Confirm</Label>
                      <Input
                        id="wallet-new-pass2"
                        type="password"
                        autoComplete="new-password"
                        value={confirmPassphrase}
                        onChange={(ev) => setConfirmPassphrase(ev.target.value)}
                      />
                    </div>
                  </div>
                  {passphrase.length > 0 && passphrase.length < 8 && (
                    <p className="text-xs text-destructive">Use at least 8 characters.</p>
                  )}
                  {confirmPassphrase.length > 0 && passphrase !== confirmPassphrase && (
                    <p className="text-xs text-destructive">The two passphrases do not match.</p>
                  )}

                  {/* One field, two meanings, because the two seed formats each have exactly one such
                      secret and which one applies is decided by the phrase itself. Mislabelling it for an
                      aezeed would be actively harmful: leaving it blank there is correct and normal, while
                      typing the wallet passphrase into it fails the AEZ tag and looks like a bad seed. */}
                  <div className="space-y-1.5">
                    <Label htmlFor="wallet-new-bip39">
                      {aezeed ? 'Seed passphrase (optional)' : 'BIP39 passphrase (optional)'}
                    </Label>
                    {/* Part of the seed itself — lose it and the coins are gone. Same handling: no store. */}
                    <Input
                      id="wallet-new-bip39"
                      type="password"
                      autoComplete="off"
                      value={bip39Passphrase}
                      onChange={(ev) => setBip39Passphrase(ev.target.value)}
                    />
                    <p className="text-xs text-muted-foreground">
                      {aezeed
                        ? 'Only if you set one when the aezeed seed was created. Almost nobody does — leave it blank.'
                        : 'A 25th word. It is part of the key, not a lock on it — without it the phrase alone recovers a different, empty wallet.'}
                    </p>
                  </div>
                </>
              ) : (
                <div className="space-y-3">
                  {CONFIG_FIELDS[kind as Exclude<BackendKind, 'onchain'>].map((field) => (
                    <div key={field.key} className="space-y-1.5">
                      <Label htmlFor={`wallet-new-${field.key}`}>{field.label}</Label>
                      {/* Node credentials are stored server-side, encrypted, and never returned to the
                          browser — so this input is write-only and holds nothing after submit. */}
                      <Input
                        id={`wallet-new-${field.key}`}
                        autoComplete="off"
                        spellCheck={false}
                        value={remoteConfig[field.key] ?? ''}
                        placeholder={field.placeholder}
                        onChange={(ev) => setRemoteConfig((prev) => ({ ...prev, [field.key]: ev.target.value }))}
                        className="font-mono text-xs"
                      />
                    </div>
                  ))}
                </div>
              )}

              <label className="flex items-center gap-2.5 text-sm">
                <Checkbox checked={makeActive} onCheckedChange={(v) => setMakeActive(v === true)} />
                Make this the active wallet
              </label>
            </div>

            {/* The toast disappears; this does not. With the form no longer clearing on failure, the
                owner needs the reason to stay put next to the boxes they are about to correct. */}
            {/* Only while the current input still matches what was rejected. Without this the box from a
                previous attempt hangs around after the phrase is corrected, which reads as "still broken"
                when nothing is broken — and the mutation's error state does not clear itself. */}
            {create.isError && !seedProblem && (
              <div className="mt-4 flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/5 p-3 text-xs">
                <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
                <span>{errorMessage(create.error, 'Could not create the wallet')}</span>
              </div>
            )}

            {blockReason && !storeKeyMissing && (
              <p className="mt-4 text-right text-xs text-muted-foreground">{blockReason}</p>
            )}

            <DialogFooter className="mt-6">
              <Button type="button" variant="ghost" onClick={close}>
                Cancel
              </Button>
              <Button type="submit" disabled={!canSubmit}>
                {create.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                Create wallet
              </Button>
            </DialogFooter>
          </form>
        </DialogContent>
      </Dialog>

      {pendingSeed && (
        <SeedBackupDialog
          open
          mnemonic={pendingSeed.mnemonic}
          walletName={pendingSeed.walletName}
          onConfirmed={seedConfirmed}
        />
      )}
    </>
  );
};

const SegButton = ({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) => (
  <button
    type="button"
    onClick={onClick}
    className={`rounded-md px-2.5 py-1 text-xs transition-colors ${
      active ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted/70'
    }`}
  >
    {label}
  </button>
);
