//! This houses various NAT hole punching strategies.
//!
//! Currently supported strategies:
//! - UPnP

use anyhow::{Context, Error, bail};
use igd_next::{PortMappingProtocol, aio::tokio as igd};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use tokio::time::sleep;
use tracing::debug;

/// The duration in seconds of a port mapping on the gateway.
const MAPPING_DURATION: u32 = 3600;

/// Renew the Mapping every half of `MAPPING_DURATION` to avoid the port being unmapped.
const MAPPING_TIMEOUT: u64 = MAPPING_DURATION as u64 / 2;

/// Attempts to map Discovery external port mappings with UPnP.
pub async fn construct_upnp_mappings(addr: Ipv4Addr, port: u16) -> Result<(), Error> {
    let gateway = igd::search_gateway(Default::default())
        .await
        .context("Gateway does not support UPnP")?;

    let external_address = gateway
        .get_external_ip()
        .await
        .context("Could not access gateway's external ip")?;

    let is_private = match external_address {
        IpAddr::V4(ipv4) => ipv4.is_private(),
        IpAddr::V6(ipv6) => ipv6.is_loopback() || ipv6.is_unspecified(),
    };

    if is_private {
        bail!(
            "Gateway's external address is a private address: {}",
            external_address
        );
    }

    loop {
        gateway
            .add_port(
                PortMappingProtocol::UDP,
                port,
                SocketAddr::new(IpAddr::V4(addr), port),
                MAPPING_DURATION,
                "Lighthouse Discovery port",
            )
            .await
            .with_context(|| format!("Could not UPnP map port: {} on the gateway", port))?;
        debug!(%port,"Discovery UPnP port mapped");
        sleep(Duration::from_secs(MAPPING_TIMEOUT)).await;
    }
}
