#[derive(Debug, Clone)]
struct PeerCapabilitiesEntry {
    capabilities: PeerCapabilities,
    received_at: u64,
}

fn fips_peer_liveness(
    last_seen_at: Option<u64>,
    link_connected: bool,
    peer_error: Option<String>,
    now: u64,
) -> (bool, Option<String>) {
    if fips_peer_presence_fresh(last_seen_at, now) {
        return (true, None);
    }
    if fips_peer_presence_stale(last_seen_at, now) {
        return (
            false,
            peer_error.or_else(|| Some("fips participant stale".to_string())),
        );
    }
    if link_connected {
        return (true, None);
    }
    (
        false,
        peer_error.or_else(|| Some("fips link pending".to_string())),
    )
}

fn fips_peer_presence_fresh(last_seen_at: Option<u64>, now: u64) -> bool {
    last_seen_at.is_some_and(|last_seen_at| {
        fips_timestamp_within_grace(now, last_seen_at, FIPS_PEER_ONLINE_GRACE_SECS)
    })
}

fn fips_peer_presence_stale(last_seen_at: Option<u64>, now: u64) -> bool {
    last_seen_at.is_some_and(|last_seen_at| !fips_peer_presence_fresh(Some(last_seen_at), now))
}

fn fips_peer_ping_interval_secs(last_seen_at: Option<u64>, link_connected: bool, now: u64) -> u64 {
    if fips_peer_presence_fresh(last_seen_at, now) {
        FIPS_PEER_ACTIVE_PING_INTERVAL_SECS
    } else if link_connected {
        FIPS_PEER_LINK_PING_INTERVAL_SECS
    } else {
        FIPS_PEER_DISCOVERY_PROBE_INTERVAL_SECS
    }
}

fn fips_peer_ping_due(
    last_seen_at: Option<u64>,
    last_ping_sent_at: Option<u64>,
    link_connected: bool,
    now: u64,
) -> bool {
    let interval = fips_peer_ping_interval_secs(last_seen_at, link_connected, now);
    last_ping_sent_at.is_none_or(|sent_at| fips_elapsed_at_least(now, sent_at, interval))
}

fn mesh_status_from_endpoint_peer(
    pubkey: String,
    peer: &FipsEndpointPeer,
    now: u64,
) -> MeshPeerStatus {
    let connected = peer.connected;
    MeshPeerStatus {
        pubkey,
        connected,
        endpoint_npub: normalize_fips_endpoint_npub(&peer.npub),
        transport_addr: peer.transport_addr.clone(),
        transport_type: peer.transport_type.clone(),
        srtt_ms: peer.srtt_ms,
        srtt_age_ms: peer.srtt_age_ms,
        link_packets_sent: peer.packets_sent,
        link_packets_recv: peer.packets_recv,
        link_bytes_sent: peer.bytes_sent,
        link_bytes_recv: peer.bytes_recv,
        rekey_in_progress: peer.rekey_in_progress,
        rekey_draining: peer.rekey_draining,
        current_k_bit: peer.current_k_bit,
        direct_probe_pending: peer.direct_probe_pending,
        direct_probe_after_ms: peer.direct_probe_after_ms,
        direct_probe_retry_count: peer.direct_probe_retry_count,
        direct_probe_auto_reconnect: peer.direct_probe_auto_reconnect,
        direct_probe_expires_at_ms: peer.direct_probe_expires_at_ms,
        nostr_traversal_consecutive_failures: peer.nostr_traversal_consecutive_failures,
        nostr_traversal_in_cooldown: peer.nostr_traversal_in_cooldown,
        nostr_traversal_cooldown_until_ms: peer.nostr_traversal_cooldown_until_ms,
        nostr_traversal_last_observed_skew_ms: peer.nostr_traversal_last_observed_skew_ms,
        last_seen_at: connected.then_some(now),
        last_control_seen_at: None,
        last_data_seen_at: None,
        tx_bytes: 0,
        rx_bytes: 0,
        error: (!connected).then(|| "fips link pending".to_string()),
    }
}

fn endpoint_peer_status_pubkey(peer: &FipsEndpointPeer) -> Option<String> {
    normalize_nostr_pubkey(&peer.npub).ok()
}

fn other_endpoint_peer_statuses(
    other_link_status: &HashMap<String, FipsEndpointPeer>,
    now: u64,
) -> Vec<MeshPeerStatus> {
    let mut statuses = other_link_status
        .values()
        .filter_map(|peer| {
            endpoint_peer_status_pubkey(peer)
                .map(|pubkey| mesh_status_from_endpoint_peer(pubkey, peer, now))
        })
        .collect::<Vec<_>>();
    statuses.sort_by(|left, right| left.pubkey.cmp(&right.pubkey));
    statuses.dedup_by(|left, right| left.pubkey == right.pubkey);
    statuses
}

fn peer_endpoint_hint_addr(hint: &PeerEndpointHint) -> Option<String> {
    nostr_vpn_core::fips_control::peer_endpoint_hint_addr(hint)
}

fn endpoint_addr_ip(addr: &str) -> Option<IpAddr> {
    let (_transport, trimmed) = split_peer_transport_addr(addr);
    let trimmed = trimmed.trim();
    if let Ok(parsed) = trimmed.parse::<SocketAddr>() {
        return Some(parsed.ip());
    }

    let (host, _) = trimmed.rsplit_once(':')?;
    host.trim().parse::<IpAddr>().ok()
}

fn endpoint_uses_tunnel_ip(addr: &str, tunnel_ips: &HashSet<IpAddr>) -> bool {
    endpoint_addr_ip(addr).is_some_and(|ip| tunnel_ips.contains(&ip))
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Ipv4Subnet {
    network: Ipv4Addr,
    prefix_len: u8,
}

impl Ipv4Subnet {
    fn new(addr: Ipv4Addr, prefix_len: u8) -> Self {
        Self {
            network: mask_ipv4(addr, prefix_len),
            prefix_len,
        }
    }

    fn contains(&self, addr: Ipv4Addr) -> bool {
        mask_ipv4(addr, self.prefix_len) == self.network
    }
}

fn mask_ipv4(addr: Ipv4Addr, prefix_len: u8) -> Ipv4Addr {
    if prefix_len == 0 {
        return Ipv4Addr::UNSPECIFIED;
    }
    if prefix_len >= 32 {
        return addr;
    }
    Ipv4Addr::from(u32::from(addr) & (u32::MAX << (32 - u32::from(prefix_len))))
}

fn local_private_ipv4_subnets() -> Vec<Ipv4Subnet> {
    let mut subnets = local_private_ipv4_interface_subnets();
    subnets.extend(local_private_ipv4_route_subnets(&subnets));
    subnets.sort_by_key(|subnet| (u32::from(subnet.network), subnet.prefix_len));
    subnets.dedup();
    subnets
}

fn local_private_ipv4_interface_subnets() -> Vec<Ipv4Subnet> {
    let mut subnets = Vec::new();
    for iface in netdev::get_interfaces() {
        if iface.is_loopback() {
            continue;
        }
        for net in &iface.ipv4 {
            let addr = net.addr();
            if addr.is_loopback()
                || addr.is_unspecified()
                || addr.is_multicast()
                || !(addr.is_private() || ipv4_is_cgnat_addr(addr) || addr.is_link_local())
            {
                continue;
            }
            subnets.push(Ipv4Subnet::new(addr, net.prefix_len()));
        }
    }
    subnets.sort_by_key(|subnet| (u32::from(subnet.network), subnet.prefix_len));
    subnets.dedup();
    subnets
}

#[cfg(target_os = "linux")]
fn local_private_ipv4_route_subnets(interface_subnets: &[Ipv4Subnet]) -> Vec<Ipv4Subnet> {
    let Ok(output) = ProcessCommand::new("ip")
        .arg("-4")
        .arg("route")
        .arg("show")
        .output()
    else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    linux_private_ipv4_route_subnets_from_ip_route(&stdout, interface_subnets)
}

#[cfg(target_os = "macos")]
fn local_private_ipv4_route_subnets(interface_subnets: &[Ipv4Subnet]) -> Vec<Ipv4Subnet> {
    let Ok(output) = ProcessCommand::new("netstat")
        .arg("-rn")
        .arg("-f")
        .arg("inet")
        .output()
    else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    macos_private_ipv4_route_subnets_from_netstat(&stdout, interface_subnets)
}

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn local_private_ipv4_route_subnets(_interface_subnets: &[Ipv4Subnet]) -> Vec<Ipv4Subnet> {
    Vec::new()
}

fn private_route_gateway_is_local(gateway: Ipv4Addr, interface_subnets: &[Ipv4Subnet]) -> bool {
    interface_subnets
        .iter()
        .any(|subnet| subnet.contains(gateway))
}

#[cfg(any(target_os = "linux", test))]
fn linux_private_ipv4_route_subnets_from_ip_route(
    output: &str,
    interface_subnets: &[Ipv4Subnet],
) -> Vec<Ipv4Subnet> {
    let mut subnets = Vec::new();
    for line in output.lines().map(str::trim) {
        if line.is_empty()
            || line.starts_with("default ")
            || line.starts_with("blackhole ")
            || line.starts_with("unreachable ")
            || line.starts_with("throw ")
        {
            continue;
        }
        let tokens = line.split_whitespace().collect::<Vec<_>>();
        let Some(route) = parse_ipv4_route_target(tokens[0]) else {
            continue;
        };
        if !ipv4_static_hint_requires_local_subnet(route.network) {
            continue;
        }
        let gateway = tokens
            .windows(2)
            .find(|pair| pair[0] == "via")
            .and_then(|pair| pair[1].parse::<Ipv4Addr>().ok());
        let dev = tokens
            .windows(2)
            .find(|pair| pair[0] == "dev")
            .map(|pair| pair[1]);
        if gateway.is_none() && dev.is_some_and(route_interface_is_tunnel_like) {
            continue;
        }
        if gateway.is_none_or(|gateway| private_route_gateway_is_local(gateway, interface_subnets))
        {
            subnets.push(route);
        }
    }
    subnets.sort_by_key(|subnet| (u32::from(subnet.network), subnet.prefix_len));
    subnets.dedup();
    subnets
}

#[cfg(any(target_os = "linux", test))]
fn route_interface_is_tunnel_like(interface: &str) -> bool {
    interface.starts_with("utun")
        || interface.starts_with("tun")
        || interface.starts_with("wg")
        || interface.starts_with("tailscale")
        || interface == "lo"
}

#[cfg(any(target_os = "macos", test))]
fn macos_private_ipv4_route_subnets_from_netstat(
    output: &str,
    interface_subnets: &[Ipv4Subnet],
) -> Vec<Ipv4Subnet> {
    let mut subnets = Vec::new();
    for line in output.lines().map(str::trim) {
        let tokens = line.split_whitespace().collect::<Vec<_>>();
        if tokens.len() < 3 || tokens[0] == "default" {
            continue;
        }
        let Some(route) = parse_macos_ipv4_route_target(tokens[0]) else {
            continue;
        };
        if !ipv4_static_hint_requires_local_subnet(route.network) {
            continue;
        }
        let gateway = tokens[1].parse::<Ipv4Addr>().ok();
        let direct = tokens[1].starts_with("link#");
        if direct
            || gateway
                .is_some_and(|gateway| private_route_gateway_is_local(gateway, interface_subnets))
        {
            subnets.push(route);
        }
    }
    subnets.sort_by_key(|subnet| (u32::from(subnet.network), subnet.prefix_len));
    subnets.dedup();
    subnets
}

#[cfg(any(target_os = "linux", target_os = "macos", test))]
fn parse_ipv4_route_target(target: &str) -> Option<Ipv4Subnet> {
    let (addr, prefix_len) = target
        .split_once('/')
        .map(|(addr, prefix)| (addr, prefix.parse::<u8>().ok()))
        .unwrap_or((target, Some(32)));
    let prefix_len = prefix_len?;
    if prefix_len > 32 {
        return None;
    }
    Some(Ipv4Subnet::new(addr.parse().ok()?, prefix_len))
}

#[cfg(any(target_os = "macos", test))]
fn parse_macos_ipv4_route_target(target: &str) -> Option<Ipv4Subnet> {
    if target.contains('/') {
        return parse_ipv4_route_target(target);
    }
    let parts = target.split('.').collect::<Vec<_>>();
    let prefix_len = match parts.len() {
        1 => 8,
        2 => 16,
        3 => 24,
        4 => 32,
        _ => return None,
    };
    let mut octets = [0u8; 4];
    for (index, part) in parts.iter().enumerate() {
        octets[index] = part.parse::<u8>().ok()?;
    }
    Some(Ipv4Subnet::new(Ipv4Addr::from(octets), prefix_len))
}

fn ipv4_static_hint_requires_local_subnet(addr: Ipv4Addr) -> bool {
    addr.is_private() || ipv4_is_cgnat_addr(addr) || addr.is_link_local()
}

fn ipv4_is_cgnat_addr(addr: Ipv4Addr) -> bool {
    let octets = addr.octets();
    octets[0] == 100 && (64..=127).contains(&octets[1])
}

fn static_endpoint_allowed_on_current_underlay(addr: &str, local_subnets: &[Ipv4Subnet]) -> bool {
    match endpoint_addr_ip(addr) {
        Some(IpAddr::V4(ip)) if ipv4_static_hint_requires_local_subnet(ip) => {
            local_subnets.iter().any(|subnet| subnet.contains(ip))
        }
        _ => true,
    }
}

fn filter_static_tunnel_endpoints(
    groups: Vec<(String, Vec<String>)>,
    tunnel_ips: &HashSet<IpAddr>,
    local_subnets: &[Ipv4Subnet],
) -> Vec<(String, Vec<String>)> {
    filter_static_tunnel_endpoints_with_policy(groups, tunnel_ips, local_subnets, false)
}

fn filter_static_tunnel_endpoints_with_policy(
    groups: Vec<(String, Vec<String>)>,
    tunnel_ips: &HashSet<IpAddr>,
    local_subnets: &[Ipv4Subnet],
    allow_routed_private_endpoints: bool,
) -> Vec<(String, Vec<String>)> {
    groups
        .into_iter()
        .filter_map(|(participant, addrs)| {
            let addrs = addrs
                .into_iter()
                .filter(|addr| {
                    !endpoint_uses_tunnel_ip(addr, tunnel_ips)
                        && (allow_routed_private_endpoints
                            || static_endpoint_allowed_on_current_underlay(addr, local_subnets))
                })
                .collect::<Vec<_>>();
            (!addrs.is_empty()).then_some((participant, addrs))
        })
        .collect()
}

fn filter_stamped_tunnel_endpoints(
    groups: Vec<(String, Vec<(String, u64)>)>,
    tunnel_ips: &HashSet<IpAddr>,
    local_subnets: &[Ipv4Subnet],
) -> Vec<(String, Vec<(String, u64)>)> {
    groups
        .into_iter()
        .filter_map(|(participant, addrs)| {
            let addrs = addrs
                .into_iter()
                .filter(|(addr, _)| {
                    !endpoint_uses_tunnel_ip(addr, tunnel_ips)
                        && static_endpoint_allowed_on_current_underlay(addr, local_subnets)
                })
                .collect::<Vec<_>>();
            (!addrs.is_empty()).then_some((participant, addrs))
        })
        .collect()
}

fn freshest_seen_at_ms(addrs: &[(String, u64)]) -> u64 {
    addrs
        .iter()
        .map(|(_, seen_at_ms)| *seen_at_ms)
        .max()
        .unwrap_or(0)
}

fn recent_transit_endpoint_score(addr: &str) -> u8 {
    let (transport, host_port) = split_peer_transport_addr(addr);
    let Ok(socket_addr) = host_port.parse::<SocketAddr>() else {
        return 0;
    };

    let mut score = match socket_addr.ip() {
        IpAddr::V6(_) => 3,
        IpAddr::V4(_) => 2,
    };

    score += match socket_addr.port() {
        443 | 2121 | 8443 | 51820 => 4,
        1..=32767 => 2,
        32768..=49151 => 1,
        _ => 0,
    };

    match transport.to_ascii_lowercase().as_str() {
        "tcp" | "udp" => score + 1,
        _ => score,
    }
}

fn recent_transit_group_rank(addrs: &[(String, u64)]) -> (u8, u64) {
    let best_score = addrs
        .iter()
        .map(|(addr, _)| recent_transit_endpoint_score(addr))
        .max()
        .unwrap_or(0);
    (best_score, freshest_seen_at_ms(addrs))
}

fn static_transit_group_rank(addrs: &[String]) -> u8 {
    addrs
        .iter()
        .map(|addr| recent_transit_endpoint_score(addr))
        .max()
        .unwrap_or(0)
}

fn cap_static_non_roster_transit_endpoints(
    groups: Vec<(String, Vec<String>)>,
    roster_endpoint_npubs: &HashSet<String>,
    max_non_roster: usize,
) -> Vec<(String, Vec<String>)> {
    let mut roster = Vec::new();
    let mut non_roster = Vec::new();

    for (participant, addrs) in groups {
        if roster_endpoint_npubs.contains(&normalize_fips_endpoint_npub(&participant)) {
            roster.push((participant, addrs));
        } else {
            non_roster.push((participant, addrs));
        }
    }

    non_roster.sort_by(|left, right| {
        static_transit_group_rank(&right.1)
            .cmp(&static_transit_group_rank(&left.1))
            .then_with(|| left.0.cmp(&right.0))
    });
    non_roster.truncate(max_non_roster);

    roster.extend(non_roster);
    roster
}

fn cap_recent_non_roster_transit_endpoints(
    groups: Vec<(String, Vec<(String, u64)>)>,
    roster_endpoint_npubs: &HashSet<String>,
    max_non_roster: usize,
) -> Vec<(String, Vec<(String, u64)>)> {
    let mut roster = Vec::new();
    let mut non_roster = Vec::new();

    for (participant, addrs) in groups {
        if roster_endpoint_npubs.contains(&normalize_fips_endpoint_npub(&participant)) {
            roster.push((participant, addrs));
        } else {
            non_roster.push((participant, addrs));
        }
    }

    non_roster.sort_by(|left, right| {
        recent_transit_group_rank(&right.1)
            .cmp(&recent_transit_group_rank(&left.1))
            .then_with(|| left.0.cmp(&right.0))
    });
    non_roster.truncate(max_non_roster);

    roster.extend(non_roster);
    roster
}

fn non_roster_endpoint_group_count<T>(
    groups: &[(String, Vec<T>)],
    roster_endpoint_npubs: &HashSet<String>,
) -> usize {
    groups
        .iter()
        .filter(|entry| !entry.1.is_empty())
        .map(|(participant, _)| normalize_fips_endpoint_npub(participant))
        .filter(|npub| !roster_endpoint_npubs.contains(npub))
        .collect::<HashSet<_>>()
        .len()
}

fn open_discovery_limit_after_transit_seeds(static_non_roster_seeds: usize) -> usize {
    FIPS_NOSTR_OPEN_DISCOVERY_MAX_PENDING
        .saturating_sub(static_non_roster_seeds)
        .saturating_sub(FIPS_RECENT_NON_ROSTER_TRANSIT_MAX_SEEDS)
}

fn fips_tunnel_endpoint_hosts(app: &AppConfig, network_id: &str) -> HashSet<IpAddr> {
    let mut hosts = HashSet::new();
    if let Ok(ip) = strip_cidr(&app.node.tunnel_ip).parse::<IpAddr>() {
        hosts.insert(ip);
    }
    for participant in app.participant_pubkeys_hex() {
        if let Some(tunnel_ip) = derive_mesh_tunnel_ip(network_id, &participant)
            && let Ok(ip) = strip_cidr(&tunnel_ip).parse::<IpAddr>()
        {
            hosts.insert(ip);
        }
    }
    hosts
}

// The historical FIPS endpoint cache (`daemon.fips-cache.json`) persisted observed
// peer transport addresses across daemon restarts. For peers reached via NAT
// traversal the observed address is an ephemeral source port that closes when
// the session ends; replaying it on the next start makes fips-core dial a dead
// socket forever instead of falling back to udp:nat traversal. The cache is gone;
// peer endpoint discovery is delegated to fips-core's overlay (Nostr advert +
// udp:nat). Any stale cache file from older builds is removed at startup.
pub(crate) fn legacy_fips_endpoint_cache_file_path(config_path: &Path) -> PathBuf {
    let parent = config_path
        .parent()
        .map_or_else(|| Path::new(".").to_path_buf(), PathBuf::from);
    parent.join("daemon.fips-cache.json")
}

pub(crate) fn purge_legacy_fips_endpoint_cache(config_path: &Path) {
    let path = legacy_fips_endpoint_cache_file_path(config_path);
    if let Err(error) = fs::remove_file(&path)
        && error.kind() != std::io::ErrorKind::NotFound
    {
        eprintln!(
            "daemon: failed to remove legacy FIPS endpoint cache {}: {error}",
            path.display()
        );
    }
}

#[derive(Debug, Clone)]
pub(crate) struct FipsRelayStatus {
    pub(crate) url: String,
    pub(crate) status: String,
}

#[derive(Debug, Clone)]
pub(crate) enum FipsPrivateMeshEvent {
    Packet(Vec<u8>),
    Presence {
        participant_pubkey: String,
        last_seen_at: u64,
    },
    JoinRequest {
        sender_pubkey: String,
        requested_at: u64,
        request: MeshJoinRequest,
    },
    Roster {
        sender_pubkey: String,
        network_id: String,
        roster: NetworkRoster,
        signed_roster: Option<Box<SignedRoster>>,
    },
    Capabilities {
        sender_pubkey: String,
        network_id: String,
        capabilities: PeerCapabilities,
    },
}
