import { useState, useEffect, type FormEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeft, RefreshCw } from 'lucide-react';
import { create } from '@bufbuild/protobuf';
import { CreatePeerRequestSchema, UpdatePeerRequestSchema } from '../../lib/peerman_pb';
import { peerClient } from '../../lib/grpc';
import { usePeer, useGenerateKeypair } from '../../hooks/usePeers';
import { useNodes } from '../../hooks/useNodes';
export default function PeerForm() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const isEdit = Boolean(id);
const { peer: existingPeer, loading: loadingPeer } = usePeer(id);
const { generate, loading: genLoading } = useGenerateKeypair();
const { nodes } = useNodes();
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [asn, setAsn] = useState('0');
const [localAsn, setLocalAsn] = useState('4242420000');
const [originNodeId, setOriginNodeId] = useState('');
const [wgPrivateKey, setWgPrivateKey] = useState('');
const [wgPublicKey, setWgPublicKey] = useState('');
const [wgRemoteAddress, setWgRemoteAddress] = useState('');
const [wgRemotePort, setWgRemotePort] = useState('0');
const [wgListenPort, setWgListenPort] = useState('42420');
const [wgInterfaceName, setWgInterfaceName] = useState('');
const [ipv4TunnelLocal, setIpv4TunnelLocal] = useState('');
const [ipv4TunnelRemote, setIpv4TunnelRemote] = useState('');
const [ipv6TunnelLocal, setIpv6TunnelLocal] = useState('');
const [ipv6TunnelRemote, setIpv6TunnelRemote] = useState('');
const [multiprotocol, setMultiprotocol] = useState(true);
const [extendedNexthop, setExtendedNexthop] = useState(true);
const [sessions, setSessions] = useState(2);
const [passive, setPassive] = useState(false);
const [importMaxPrefix, setImportMaxPrefix] = useState('0');
const [exportMaxPrefix, setExportMaxPrefix] = useState('0');
useEffect(() => {
if (existingPeer) {
setName(existingPeer.name);
setDescription(existingPeer.description);
setAsn(existingPeer.asn.toString());
setLocalAsn(existingPeer.localAsn.toString());
setWgPrivateKey(existingPeer.wgPrivateKey);
setWgPublicKey(existingPeer.wgPublicKey);
setWgRemoteAddress(existingPeer.wgRemoteAddress);
setWgRemotePort(String(existingPeer.wgRemotePort));
setWgListenPort(String(existingPeer.wgListenPort));
setWgInterfaceName(existingPeer.wgInterfaceName);
setIpv4TunnelLocal(existingPeer.ipv4TunnelLocal);
setIpv4TunnelRemote(existingPeer.ipv4TunnelRemote);
setIpv6TunnelLocal(existingPeer.ipv6TunnelLocal);
setIpv6TunnelRemote(existingPeer.ipv6TunnelRemote);
setMultiprotocol(existingPeer.multiprotocol);
setExtendedNexthop(existingPeer.extendedNexthop);
setSessions(existingPeer.sessions);
setPassive(existingPeer.passive);
setImportMaxPrefix(String(existingPeer.importMaxPrefix || '0'));
setExportMaxPrefix(String(existingPeer.exportMaxPrefix || '0'));
setOriginNodeId(existingPeer.originNodeId);
}
}, [existingPeer]);
const handleGenerate = async () => {
const result = await generate();
if (result) {
setWgPrivateKey(result.privateKey);
setWgPublicKey(result.publicKey);
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setSaving(true);
setError('');
const base = {
name,
description,
asn: BigInt(asn || '0'),
localAsn: BigInt(localAsn || '0'),
wgPrivateKey,
wgPublicKey,
wgRemoteAddress,
wgRemotePort: Number(wgRemotePort || '0'),
wgListenPort: Number(wgListenPort || '0'),
wgInterfaceName,
ipv4TunnelLocal,
ipv4TunnelRemote,
ipv6TunnelLocal,
ipv6TunnelRemote,
multiprotocol,
extendedNexthop,
sessions,
passive,
importMaxPrefix: Number(importMaxPrefix || '0'),
exportMaxPrefix: Number(exportMaxPrefix || '0'),
originNodeId,
};
try {
if (isEdit && id) {
await peerClient.updatePeer(create(UpdatePeerRequestSchema, { id, ...base }));
} else {
await peerClient.createPeer(create(CreatePeerRequestSchema, base));
}
navigate('/');
} catch (e) {
setError(String(e));
} finally {
setSaving(false);
}
};
if (isEdit && loadingPeer) {
return <div className="p-xl text-body">Loading...</div>;
}
return (
<div className="max-w-2xl mx-auto animate-fade-in">
<div className="flex items-center gap-md mb-lg">
<button onClick={() => navigate(-1)} className="btn-ghost">
<ArrowLeft className="w-4 h-4" />
</button>
<h1 className="text-display-md text-ink">
{isEdit ? `Edit ${name}` : 'New Peer'}
</h1>
</div>
{error && (
<div className="bg-error-soft text-error-deep text-body-sm px-md py-sm rounded-sm mb-lg">{error}</div>
)}
<form onSubmit={handleSubmit} className="space-y-lg">
{/* Identity */}
<fieldset className="card">
<legend className="text-body-sm-strong text-ink mb-md">Identity</legend>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
<div className="flex flex-col gap-1">
<label className="text-caption text-mute">Target Node</label>
<select value={originNodeId} onChange={(e) => setOriginNodeId(e.target.value)} className="form-input">
<option value="">This node (local)</option>
{nodes.filter(n => n.online).map(n => (
<option key={n.id} value={n.id}>{n.name} ({n.listenAddr})</option>
))}
</select>
</div>
<Input label="Name" value={name} onChange={setName} required />
<Input label="Description" value={description} onChange={setDescription} />
<Input label="Remote ASN" value={asn} onChange={setAsn} placeholder="424242XXXX" />
<Input label="Local ASN" value={localAsn} onChange={setLocalAsn} />
</div>
</fieldset>
{/* WireGuard */}
<fieldset className="card">
<legend className="text-body-sm-strong text-ink mb-md">WireGuard</legend>
<div className="flex items-center gap-2 mb-sm">
<button type="button" onClick={handleGenerate} disabled={genLoading} className="btn-secondary-sm">
<RefreshCw className={`w-3.5 h-3.5 ${genLoading ? 'animate-spin' : ''}`} />
Generate Keypair
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
<Input label="Private Key" value={wgPrivateKey} onChange={setWgPrivateKey} mono />
<Input label="Public Key" value={wgPublicKey} onChange={setWgPublicKey} mono />
<Input label="Remote Address" value={wgRemoteAddress} onChange={setWgRemoteAddress} />
<Input label="Remote Port" value={wgRemotePort} onChange={setWgRemotePort} type="number" />
<Input label="Listen Port" value={wgListenPort} onChange={setWgListenPort} type="number" />
<Input label="Interface Name" value={wgInterfaceName} onChange={setWgInterfaceName} placeholder="wg-dn42-peer" />
</div>
</fieldset>
{/* Tunnel */}
<fieldset className="card">
<legend className="text-body-sm-strong text-ink mb-md">Tunnel Addressing</legend>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
<Input label="IPv4 Local" value={ipv4TunnelLocal} onChange={setIpv4TunnelLocal} />
<Input label="IPv4 Remote" value={ipv4TunnelRemote} onChange={setIpv4TunnelRemote} />
<Input label="IPv6 Local (link-local)" value={ipv6TunnelLocal} onChange={setIpv6TunnelLocal} placeholder="fe80::1" />
<Input label="IPv6 Remote (link-local)" value={ipv6TunnelRemote} onChange={setIpv6TunnelRemote} placeholder="fe80::2" />
</div>
</fieldset>
{/* BGP */}
<fieldset className="card">
<legend className="text-body-sm-strong text-ink mb-md">BGP Session</legend>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
<Toggle label="Multiprotocol" checked={multiprotocol} onChange={setMultiprotocol} />
<Toggle label="Extended Nexthop" checked={extendedNexthop} onChange={setExtendedNexthop} />
<Toggle label="Passive" checked={passive} onChange={setPassive} />
<div className="flex flex-col gap-1">
<label className="text-caption text-mute">Sessions</label>
<select value={sessions} onChange={(e) => setSessions(Number(e.target.value))} className="form-input">
<option value={2}>Both</option>
<option value={0}>IPv4</option>
<option value={1}>IPv6</option>
</select>
</div>
<Input label="Max Prefix (import)" value={importMaxPrefix} onChange={setImportMaxPrefix} type="number" />
<Input label="Max Prefix (export)" value={exportMaxPrefix} onChange={setExportMaxPrefix} type="number" />
</div>
</fieldset>
<div className="flex items-center gap-2">
<button type="submit" disabled={saving} className="btn-primary">
{saving ? 'Saving...' : isEdit ? 'Update Peer' : 'Create Peer'}
</button>
<button type="button" onClick={() => navigate(-1)} className="btn-secondary">
Cancel
</button>
</div>
</form>
</div>
);
}
function Input({
label, value, onChange, type = 'text', placeholder, required, mono,
}: {
label: string; value: string; onChange: (v: string) => void;
type?: string; placeholder?: string; required?: boolean; mono?: boolean;
}) {
return (
<div className="flex flex-col gap-1">
<label className="text-caption text-mute">{label}</label>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
className={`form-input ${mono ? 'font-mono' : ''}`}
/>
</div>
);
}
function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
return (
<div className="flex items-center justify-between py-1">
<span className="text-caption text-mute">{label}</span>
<button
type="button"
onClick={() => onChange(!checked)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
checked ? 'bg-primary' : 'bg-hairline-strong'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${
checked ? 'translate-x-[18px]' : 'translate-x-[3px]'
}`}
/>
</button>
</div>
);
}