fn run_invite_broadcast(args: InviteBroadcastArgs) -> Result<()> {
    use nostr_vpn_core::lan_pairing::{
        LAN_PAIRING_DURATION, LanPairingAnnouncement, spawn_lan_pairing_worker,
    };

    let config_path = args.config.unwrap_or_else(default_config_path);
    let app = load_or_default_config(&config_path)?;

    let own_npub_hex = app.own_nostr_pubkey_hex()?;
    let own_npub = nostr_vpn_core::invite::to_npub(&own_npub_hex);
    let invite = active_network_invite_code(&app)?;
    let endpoint = app.node.endpoint.trim().to_string();
    let node_name = app.node_name.trim().to_string();

    let duration = Duration::from_secs(
        args.duration_secs
            .unwrap_or_else(|| LAN_PAIRING_DURATION.as_secs()),
    );
    let expires_at = SystemTime::now()
        .checked_add(duration)
        .ok_or_else(|| anyhow::anyhow!("broadcast duration overflows SystemTime"))?;

    let mut worker = spawn_lan_pairing_worker(LanPairingAnnouncement {
        npub: own_npub.clone(),
        node_name: node_name.clone(),
        endpoint: endpoint.clone(),
        invite: invite.clone(),
    })?;
    worker.set_broadcast_until(expires_at);

    println!("network_id={}", app.effective_network_id());
    println!("npub={own_npub}");
    println!("endpoint={endpoint}");
    println!("invite={invite}");
    println!(
        "broadcasting on 239.255.73.73:38911 for {} seconds (Ctrl-C to stop)",
        duration.as_secs()
    );

    let deadline = Instant::now() + duration;
    while Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(500));
    }
    worker.clear_broadcast();
    worker.stop();
    println!("broadcast stopped");
    Ok(())
}

fn run_discover(args: DiscoverArgs) -> Result<()> {
    use nostr_vpn_core::lan_pairing::{
        LAN_PAIRING_DURATION, LanPairingAnnouncement, LanPairingSignal, spawn_lan_pairing_worker,
    };

    let config_path = args.config.unwrap_or_else(default_config_path);
    let mut app = load_or_default_config(&config_path)?;

    let own_npub_hex = app.own_nostr_pubkey_hex()?;
    let own_npub = nostr_vpn_core::invite::to_npub(&own_npub_hex);
    let endpoint = app.node.endpoint.trim().to_string();
    let node_name = app.node_name.trim().to_string();
    let local_invite = active_network_invite_code(&app).unwrap_or_default();

    let duration = Duration::from_secs(
        args.duration_secs
            .unwrap_or_else(|| LAN_PAIRING_DURATION.as_secs()),
    );
    let expires_at = SystemTime::now()
        .checked_add(duration)
        .ok_or_else(|| anyhow::anyhow!("discover duration overflows SystemTime"))?;

    let mut worker = spawn_lan_pairing_worker(LanPairingAnnouncement {
        npub: own_npub.clone(),
        node_name,
        endpoint,
        invite: local_invite,
    })?;
    worker.set_listen_until(expires_at);

    println!(
        "listening on 239.255.73.73:38911 for {} seconds (Ctrl-C to stop)",
        duration.as_secs()
    );

    let mut accepted: Option<LanPairingSignal> = None;
    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
    let deadline = Instant::now() + duration;
    while Instant::now() < deadline {
        for signal in worker.drain() {
            if !seen.insert(signal.npub.clone()) {
                continue;
            }
            if args.json {
                println!(
                    "{}",
                    serde_json::to_string(&json!({
                        "npub": signal.npub,
                        "node_name": signal.node_name,
                        "endpoint": signal.endpoint,
                        "network_id": signal.network_id,
                        "network_name": signal.network_name,
                        "invite": signal.invite,
                    }))?
                );
            } else {
                println!(
                    "npub={} node={} endpoint={} network_id={} network_name={}",
                    signal.npub,
                    if signal.node_name.is_empty() {
                        "?"
                    } else {
                        signal.node_name.as_str()
                    },
                    if signal.endpoint.is_empty() {
                        "?"
                    } else {
                        signal.endpoint.as_str()
                    },
                    signal.network_id,
                    if signal.network_name.is_empty() {
                        "-"
                    } else {
                        signal.network_name.as_str()
                    },
                );
            }
            if args.accept {
                accepted = Some(signal);
                break;
            }
        }
        if accepted.is_some() {
            break;
        }
        std::thread::sleep(Duration::from_millis(250));
    }
    worker.clear_listen();
    worker.stop();

    if let Some(signal) = accepted {
        let invite = parse_network_invite(&signal.invite)?;
        apply_network_invite_to_active_network(&mut app, &invite)?;
        let join_request_queued = queue_active_network_join_request(&mut app)?;
        app.ensure_defaults();
        maybe_autoconfigure_node(&mut app);
        app.save(&config_path)?;
        maybe_reload_running_daemon(&config_path);
        if args.json {
            println!("{}", serde_json::to_string_pretty(&app)?);
        } else {
            println!("saved {}", config_path.display());
            println!("network_id={}", app.effective_network_id());
            println!("invite_imported={}", app.active_network().name);
            println!("join_request_queued={join_request_queued}");
        }
    }
    Ok(())
}
