fn wireguard_exit_status_json(app: &AppConfig) -> serde_json::Value {
    json!({
        "enabled": app.wireguard_exit.enabled,
        "configured": app.wireguard_exit.configured(),
        "interface": &app.wireguard_exit.interface,
        "address": &app.wireguard_exit.address,
        "endpoint": &app.wireguard_exit.endpoint,
        "allowed_ips": &app.wireguard_exit.allowed_ips,
        "dns": &app.wireguard_exit.dns,
        "mtu": app.wireguard_exit.mtu,
        "persistent_keepalive_secs": app.wireguard_exit.persistent_keepalive_secs,
    })
}

fn runtime_local_tunnel_ip(app: &AppConfig, network_id: &str) -> String {
    if let Ok(own_pubkey) = app.own_nostr_pubkey_hex()
        && let Some(tunnel_ip) = derive_mesh_tunnel_ip(network_id, &own_pubkey)
    {
        return tunnel_ip;
    }
    app.node.tunnel_ip.clone()
}

fn runtime_peer_tunnel_ips(app: &AppConfig, network_id: &str) -> Vec<String> {
    let own_pubkey = app.own_nostr_pubkey_hex().ok();
    let mut ips = app
        .participant_pubkeys_hex()
        .into_iter()
        .filter(|participant| Some(participant) != own_pubkey.as_ref())
        .filter_map(|participant| derive_mesh_tunnel_ip(network_id, &participant))
        .collect::<Vec<_>>();
    ips.sort();
    ips.dedup();
    ips
}

fn configured_fips_peer_announcements(app: &AppConfig, network_id: &str) -> Vec<PeerAnnouncement> {
    let own_pubkey = app.own_nostr_pubkey_hex().ok();
    let mut peers = app
        .participant_pubkeys_hex()
        .into_iter()
        .filter(|participant| Some(participant) != own_pubkey.as_ref())
        .filter_map(|participant| {
            let tunnel_ip = derive_mesh_tunnel_ip(network_id, &participant)?;
            let node_id = app
                .magic_dns_name_for_participant(&participant)
                .or_else(|| app.peer_alias(&participant))
                .unwrap_or_else(|| participant.clone());
            Some(PeerAnnouncement {
                node_id,
                public_key: participant,
                endpoint: "fips".to_string(),
                local_endpoint: None,
                public_endpoint: None,
                tunnel_ip,
                advertised_routes: Vec::new(),
                timestamp: 0,
            })
        })
        .collect::<Vec<_>>();
    peers.sort_by(|left, right| left.node_id.cmp(&right.node_id));
    peers
}

pub(crate) fn shared_roster_publish_allowed(
    app: &AppConfig,
    network_id: &str,
    own_pubkey: &str,
    signed_by: &str,
) -> bool {
    let Ok(own_pubkey) = normalize_nostr_pubkey(own_pubkey) else {
        return false;
    };
    if !app.is_network_admin(network_id, &own_pubkey) {
        return false;
    }

    let signed_by = normalize_nostr_pubkey(signed_by).unwrap_or_default();
    signed_by.is_empty() || signed_by == own_pubkey
}

fn network_roster_from_shared(shared: &SharedNetworkRoster) -> NetworkRoster {
    NetworkRoster {
        network_name: shared.name.clone(),
        devices: shared.devices.clone(),
        admins: shared.admins.clone(),
        aliases: shared.aliases.clone(),
        signed_at: if shared.updated_at > 0 {
            shared.updated_at
        } else {
            unix_timestamp()
        },
    }
}

fn signed_roster_matches_shared(
    signed_roster: &SignedRoster,
    shared: &SharedNetworkRoster,
) -> bool {
    let Ok(signed_network_id) = signed_roster.network_id() else {
        return false;
    };
    if normalize_runtime_network_id(&signed_network_id)
        != normalize_runtime_network_id(&shared.network_id)
    {
        return false;
    }

    let Ok(signed_by) = signed_roster.signer_pubkey_hex() else {
        return false;
    };
    let shared_signed_by = normalize_nostr_pubkey(&shared.signed_by).unwrap_or_default();
    if shared_signed_by.is_empty() || shared_signed_by != signed_by {
        return false;
    }

    let Ok(roster) = signed_roster.roster() else {
        return false;
    };
    roster == network_roster_from_shared(shared)
}

fn active_signed_roster_for_sync(
    app: &AppConfig,
    config_path: &Path,
    allow_forwarded: bool,
) -> Result<Option<SignedRoster>> {
    let Some(network) = app.active_network_opt() else {
        return Ok(None);
    };
    let shared = app.shared_network_roster(&network.id)?;
    let store_path = signed_rosters_file_path(config_path);
    let own_pubkey = app.own_nostr_pubkey_hex().ok();
    if let Some(stored) = load_signed_rosters(&store_path)?
        .latest_for(&shared.network_id)
        .filter(|stored| signed_roster_matches_shared(stored, &shared))
        .filter(|stored| {
            allow_forwarded
                || own_pubkey.as_deref().is_some_and(|own_pubkey| {
                    stored.signer_pubkey_hex().ok().as_deref() == Some(own_pubkey)
                })
        })
        .cloned()
    {
        return Ok(Some(stored));
    }

    let Some(own_pubkey) = own_pubkey else {
        return Ok(None);
    };
    if !shared_roster_publish_allowed(app, &network.id, &own_pubkey, &shared.signed_by) {
        return Ok(None);
    }

    let signed_roster = SignedRoster::sign(
        &shared.network_id,
        network_roster_from_shared(&shared),
        &app.nostr_keys()?,
    )?;
    upsert_signed_roster(&store_path, signed_roster.clone())?;
    Ok(Some(signed_roster))
}

fn signed_roster_is_current_for_app(
    app: &AppConfig,
    network_id: &str,
    signed_roster: &SignedRoster,
) -> bool {
    let Ok(signed_by) = signed_roster.signer_pubkey_hex() else {
        return false;
    };
    app.networks.iter().any(|network| {
        normalize_runtime_network_id(&network.network_id)
            == normalize_runtime_network_id(network_id)
            && network.shared_roster_updated_at == signed_roster.signed_at()
            && normalize_nostr_pubkey(&network.shared_roster_signed_by)
                .is_ok_and(|value| value == signed_by)
    })
}

#[cfg(feature = "embedded-fips")]
const FIPS_ROSTER_RESEND_SECS: u64 = 10;

#[cfg(feature = "embedded-fips")]
#[derive(Default)]
struct FipsRosterSyncState {
    sent_by_peer: HashMap<String, FipsRosterSentState>,
}

#[cfg(feature = "embedded-fips")]
struct FipsRosterSentState {
    hash: String,
    sent_at: u64,
}

#[cfg(feature = "embedded-fips")]
async fn sync_fips_roster_with_connected_peers(
    runtime: &crate::fips_private_mesh::FipsPrivateTunnelRuntime,
    app: &AppConfig,
    config_path: &Path,
    state: &mut FipsRosterSyncState,
) -> Result<usize> {
    let Some(signed_roster) = active_signed_roster_for_sync(app, config_path, true)? else {
        return Ok(0);
    };
    let now = unix_timestamp();
    let roster_hash = signed_roster.content_hash();
    let own_pubkey = app.own_nostr_pubkey_hex().ok();
    let roster_peers = app
        .active_network_signal_pubkeys_hex()
        .into_iter()
        .collect::<HashSet<_>>();
    let connected = runtime
        .peer_statuses()
        .into_iter()
        .filter(|status| status.connected)
        .filter(|status| own_pubkey.as_deref() != Some(status.pubkey.as_str()))
        .filter(|status| roster_peers.contains(&status.pubkey))
        .map(|status| status.pubkey)
        .collect::<HashSet<_>>();

    state
        .sent_by_peer
        .retain(|peer, _| connected.contains(peer));

    let mut sent = 0usize;
    for peer in connected {
        if state.sent_by_peer.get(&peer).is_some_and(|sent| {
            sent.hash == roster_hash && now.saturating_sub(sent.sent_at) < FIPS_ROSTER_RESEND_SECS
        }) {
            continue;
        }
        runtime.send_roster(&peer, signed_roster.clone()).await?;
        state.sent_by_peer.insert(
            peer,
            FipsRosterSentState {
                hash: roster_hash.clone(),
                sent_at: now,
            },
        );
        sent += 1;
    }
    Ok(sent)
}
