//! Best-effort UPnP port mapping for NAT traversal.
//! Skipped entirely on mobile platforms where UPnP is unsupported.

use std::net::SocketAddr;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use tracing::{info, debug};

/// Result of a successful UPnP port mapping.
pub struct UpnpMapping {
    pub external_addr: SocketAddr,
    pub lease_secs: u32,
    pub local_port: u16,
}

/// Best-effort UPnP port mapping.
/// 3s gateway discovery timeout, 1800s (30 min) lease, UDP protocol.
/// Returns None on any failure (no router, unsupported, timeout, port conflict).
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn try_upnp_mapping(local_port: u16) -> Option<UpnpMapping> {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await {
        Ok(gw) => gw,
        Err(e) => {
            debug!("UPnP gateway discovery failed (expected behind non-UPnP router): {}", e);
            return None;
        }
    };

    let external_ip = match gateway.get_external_ip().await {
        Ok(ip) => ip,
        Err(e) => {
            debug!("UPnP: could not get external IP: {}", e);
            return None;
        }
    };

    // Local address for the mapping — bind to all interfaces
    let local_addr = SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port);
    let lease_secs: u32 = 1800; // 30 minutes

    // Try mapping the same external port first
    let result = gateway.add_port(
        igd_next::PortMappingProtocol::UDP,
        local_port,
        local_addr,
        lease_secs,
        "itsgoin",
    ).await;

    let external_port = match result {
        Ok(()) => local_port,
        Err(_) => {
            // Port taken — try any available port
            match gateway.add_any_port(
                igd_next::PortMappingProtocol::UDP,
                local_addr,
                lease_secs,
                "itsgoin",
            ).await {
                Ok(port) => port,
                Err(e) => {
                    debug!("UPnP: port mapping failed: {}", e);
                    return None;
                }
            }
        }
    };

    let external_addr = SocketAddr::new(external_ip, external_port);
    info!("UPnP: mapped {}:{} → :{}", external_ip, external_port, local_port);

    Some(UpnpMapping {
        external_addr,
        lease_secs,
        local_port,
    })
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn try_upnp_mapping(_local_port: u16) -> Option<UpnpMapping> {
    None
}

/// Renew an existing UPnP lease. Returns true on success.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn renew_upnp_mapping(local_port: u16, external_port: u16) -> bool {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await {
        Ok(gw) => gw,
        Err(_) => return false,
    };

    let local_addr = SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port);
    gateway.add_port(
        igd_next::PortMappingProtocol::UDP,
        external_port,
        local_addr,
        1800,
        "itsgoin",
    ).await.is_ok()
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn renew_upnp_mapping(_local_port: u16, _external_port: u16) -> bool {
    false
}

/// Remove UPnP mapping on shutdown. Best-effort, errors are silently ignored.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn remove_upnp_mapping(external_port: u16) {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    if let Ok(gateway) = igd_next::aio::tokio::search_gateway(search_opts).await {
        let _ = gateway.remove_port(igd_next::PortMappingProtocol::UDP, external_port).await;
        info!("UPnP: removed port mapping for external port {}", external_port);
    }
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn remove_upnp_mapping(_external_port: u16) {}

// --- TCP port mapping (for HTTP post delivery) ---

/// Best-effort UPnP TCP port mapping on the same port as QUIC UDP.
/// Returns true on success. Reuses the already-discovered gateway.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn try_upnp_tcp_mapping(local_port: u16, external_port: u16) -> bool {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await {
        Ok(gw) => gw,
        Err(_) => return false,
    };

    let local_addr = SocketAddr::new(
        std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
        local_port,
    );

    match gateway
        .add_port(
            igd_next::PortMappingProtocol::TCP,
            external_port,
            local_addr,
            1800,
            "itsgoin-http",
        )
        .await
    {
        Ok(()) => {
            info!("UPnP: TCP port {} mapped for HTTP serving", external_port);
            true
        }
        Err(e) => {
            debug!("UPnP: TCP port mapping failed (non-fatal): {}", e);
            false
        }
    }
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn try_upnp_tcp_mapping(_local_port: u16, _external_port: u16) -> bool {
    false
}

/// Renew an existing UPnP TCP lease. Returns true on success.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn renew_upnp_tcp_mapping(local_port: u16, external_port: u16) -> bool {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await {
        Ok(gw) => gw,
        Err(_) => return false,
    };

    let local_addr = SocketAddr::new(
        std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
        local_port,
    );
    gateway
        .add_port(
            igd_next::PortMappingProtocol::TCP,
            external_port,
            local_addr,
            1800,
            "itsgoin-http",
        )
        .await
        .is_ok()
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn renew_upnp_tcp_mapping(_local_port: u16, _external_port: u16) -> bool {
    false
}

/// Remove UPnP TCP mapping on shutdown.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn remove_upnp_tcp_mapping(external_port: u16) {
    use igd_next::SearchOptions;

    let search_opts = SearchOptions {
        timeout: Some(std::time::Duration::from_secs(3)),
        ..Default::default()
    };

    if let Ok(gateway) = igd_next::aio::tokio::search_gateway(search_opts).await {
        let _ = gateway
            .remove_port(igd_next::PortMappingProtocol::TCP, external_port)
            .await;
        info!("UPnP: removed TCP port mapping for port {}", external_port);
    }
}

#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn remove_upnp_tcp_mapping(_external_port: u16) {}
