use std::{
    net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
    sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        Arc,
    },
    time::Duration,
    u16,
};

use anyhow::Context;
use crossbeam::atomic::AtomicCell;
use dashmap::{DashMap, DashSet};
use rand::{seq::SliceRandom, Rng};
use tokio::{
    net::UdpSocket,
    sync::{Mutex, Notify},
    task::JoinSet,
};
use tracing::{instrument, Instrument, Level};
use zerocopy::FromBytes;

use crate::{
    common::{
        error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS,
        scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId,
    },
    defer,
    peers::peer_manager::PeerManager,
    proto::{
        common::NatType,
        peer_rpc::{
            TryPunchHoleRequest, TryPunchHoleResponse, TryPunchSymmetricRequest,
            TryPunchSymmetricResponse, UdpHolePunchRpc, UdpHolePunchRpcClientFactory,
            UdpHolePunchRpcServer,
        },
        rpc_types::{self, controller::BaseController},
    },
    tunnel::{
        common::setup_sokcet2,
        packet_def::{UDPTunnelHeader, UdpPacketType, UDP_TUNNEL_HEADER_SIZE},
        udp::{new_hole_punch_packet, UdpTunnelConnector, UdpTunnelListener},
        Tunnel, TunnelConnCounter, TunnelListener,
    },
};

use super::direct::PeerManagerForDirectConnector;

const HOLE_PUNCH_PACKET_BODY_LEN: u16 = 16;

fn generate_shuffled_port_vec() -> Vec<u16> {
    let mut rng = rand::thread_rng();
    let mut port_vec: Vec<u16> = (1..=65535).collect();
    port_vec.shuffle(&mut rng);
    port_vec
}

// used for symmetric hole punching, binding to multiple ports to increase the chance of success
struct UdpSocketArray {
    sockets: Arc<DashMap<SocketAddr, Arc<UdpSocket>>>,
    max_socket_count: usize,
    net_ns: NetNS,
    tasks: Arc<std::sync::Mutex<JoinSet<()>>>,

    intreast_tids: Arc<DashSet<u32>>,
    tid_to_socket: Arc<DashMap<u32, Vec<Arc<UdpSocket>>>>,
}

impl UdpSocketArray {
    pub fn new(max_socket_count: usize, net_ns: NetNS) -> Self {
        let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new()));
        join_joinset_background(tasks.clone(), "UdpSocketArray".to_owned());

        Self {
            sockets: Arc::new(DashMap::new()),
            max_socket_count,
            net_ns,
            tasks,

            intreast_tids: Arc::new(DashSet::new()),
            tid_to_socket: Arc::new(DashMap::new()),
        }
    }

    pub fn started(&self) -> bool {
        !self.sockets.is_empty()
    }

    async fn add_new_socket(&self) -> Result<(), anyhow::Error> {
        let socket = {
            let _g = self.net_ns.guard();
            Arc::new(UdpSocket::bind("0.0.0.0:0").await?)
        };
        let local_addr = socket.local_addr()?;
        self.sockets.insert(local_addr, socket.clone());

        let intreast_tids = self.intreast_tids.clone();
        let tid_to_socket = self.tid_to_socket.clone();
        self.tasks.lock().unwrap().spawn(
            async move {
                let mut buf = [0u8; UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize];
                tracing::trace!(?local_addr, "udp socket added");
                loop {
                    let Ok((len, addr)) = socket.recv_from(&mut buf).await else {
                        break;
                    };

                    tracing::debug!(?len, ?addr, "got raw packet");

                    if len != UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize {
                        continue;
                    }

                    let Some(p) = UDPTunnelHeader::ref_from_prefix(&buf) else {
                        continue;
                    };

                    tracing::debug!(?p, ?addr, "got udp hole punch packet");

                    if p.msg_type != UdpPacketType::HolePunch as u8
                        || p.len.get() != HOLE_PUNCH_PACKET_BODY_LEN
                    {
                        continue;
                    }

                    let tid = p.conn_id.get();
                    if intreast_tids.contains(&tid) {
                        tracing::info!(?addr, "got hole punching packet with intreast tid");
                        tid_to_socket
                            .entry(tid)
                            .or_insert_with(Vec::new)
                            .push(socket);
                        break;
                    }
                }
                tracing::debug!(?local_addr, "udp socket recv loop end");
            }
            .instrument(tracing::info_span!("udp array socket recv loop")),
        );
        Ok(())
    }

    #[instrument(err)]
    pub async fn start(&self) -> Result<(), anyhow::Error> {
        if self.started() {
            return Ok(());
        }

        tracing::info!("starting udp socket array");

        while self.sockets.len() < self.max_socket_count {
            self.add_new_socket().await?;
        }

        Ok(())
    }

    #[instrument(err)]
    pub async fn send_with_all(&self, data: &[u8], addr: SocketAddr) -> Result<(), anyhow::Error> {
        tracing::info!(?addr, "sending hole punching packet");

        for socket in self.sockets.iter() {
            let socket = socket.value();
            socket.send_to(data, addr).await?;
        }

        Ok(())
    }

    #[instrument(ret(level = Level::DEBUG))]
    pub fn try_fetch_punched_socket(&self, tid: u32) -> Option<Arc<UdpSocket>> {
        tracing::debug!(?tid, "try fetch punched socket");
        self.tid_to_socket.get_mut(&tid)?.value_mut().pop()
    }

    pub fn add_intreast_tid(&self, tid: u32) {
        self.intreast_tids.insert(tid);
    }

    pub fn remove_intreast_tid(&self, tid: u32) {
        self.intreast_tids.remove(&tid);
        self.tid_to_socket.remove(&tid);
    }
}

impl std::fmt::Debug for UdpSocketArray {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("UdpSocketArray")
            .field("sockets", &self.sockets.len())
            .field("max_socket_count", &self.max_socket_count)
            .field("started", &self.started())
            .field("intreast_tids", &self.intreast_tids.len())
            .field("tid_to_socket", &self.tid_to_socket.len())
            .finish()
    }
}

#[derive(Debug)]
struct UdpHolePunchListener {
    socket: Arc<UdpSocket>,
    tasks: JoinSet<()>,
    running: Arc<AtomicCell<bool>>,
    mapped_addr: SocketAddr,
    conn_counter: Arc<Box<dyn TunnelConnCounter>>,

    listen_time: std::time::Instant,
    last_select_time: AtomicCell<std::time::Instant>,
    last_active_time: Arc<AtomicCell<std::time::Instant>>,
}

impl UdpHolePunchListener {
    async fn get_avail_port() -> Result<u16, Error> {
        let socket = UdpSocket::bind("0.0.0.0:0").await?;
        Ok(socket.local_addr()?.port())
    }

    #[instrument(err)]
    pub async fn new(peer_mgr: Arc<PeerManager>) -> Result<Self, Error> {
        let port = Self::get_avail_port().await?;
        let listen_url = format!("udp://0.0.0.0:{}", port);

        let gctx = peer_mgr.get_global_ctx();
        let stun_info_collect = gctx.get_stun_info_collector();
        let mapped_addr = stun_info_collect.get_udp_port_mapping(port).await?;

        let mut listener = UdpTunnelListener::new(listen_url.parse().unwrap());

        {
            let _g = peer_mgr.get_global_ctx().net_ns.guard();
            listener.listen().await?;
        }
        let socket = listener.get_socket().unwrap();

        let running = Arc::new(AtomicCell::new(true));
        let running_clone = running.clone();

        let conn_counter = listener.get_conn_counter();
        let mut tasks = JoinSet::new();

        tasks.spawn(async move {
            while let Ok(conn) = listener.accept().await {
                tracing::warn!(?conn, "udp hole punching listener got peer connection");
                let peer_mgr = peer_mgr.clone();
                tokio::spawn(async move {
                    if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await {
                        tracing::error!(
                            ?e,
                            "failed to add tunnel as server in hole punch listener"
                        );
                    }
                });
            }

            running_clone.store(false);
        });

        let last_active_time = Arc::new(AtomicCell::new(std::time::Instant::now()));
        let conn_counter_clone = conn_counter.clone();
        let last_active_time_clone = last_active_time.clone();
        tasks.spawn(async move {
            loop {
                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                if conn_counter_clone.get() != 0 {
                    last_active_time_clone.store(std::time::Instant::now());
                }
            }
        });

        tracing::warn!(?mapped_addr, ?socket, "udp hole punching listener started");

        Ok(Self {
            tasks,
            socket,
            running,
            mapped_addr,
            conn_counter,

            listen_time: std::time::Instant::now(),
            last_select_time: AtomicCell::new(std::time::Instant::now()),
            last_active_time,
        })
    }

    pub async fn get_socket(&self) -> Arc<UdpSocket> {
        self.last_select_time.store(std::time::Instant::now());
        self.socket.clone()
    }
}

struct UdpHolePunchConnectorData {
    global_ctx: ArcGlobalCtx,
    peer_mgr: Arc<PeerManager>,
    listeners: Arc<Mutex<Vec<UdpHolePunchListener>>>,
    shuffled_port_vec: Arc<Vec<u16>>,

    udp_array: Arc<Mutex<Option<Arc<UdpSocketArray>>>>,
    try_direct_connect: AtomicBool,
    punch_predicablely: AtomicBool,
    punch_randomly: AtomicBool,
    udp_array_size: AtomicUsize,
}

impl std::fmt::Debug for UdpHolePunchConnectorData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // print peer id listener count
        let peer_id = self.peer_mgr.my_peer_id();
        f.debug_struct("UdpHolePunchConnectorData")
            .field("peer_id", &peer_id)
            .finish()
    }
}

impl UdpHolePunchConnectorData {
    fn my_nat_type(&self) -> NatType {
        let stun_info = self.global_ctx.get_stun_info_collector().get_stun_info();
        NatType::try_from(stun_info.udp_nat_type).unwrap()
    }
}

#[derive(Clone)]
struct UdpHolePunchRpcService {
    data: Arc<UdpHolePunchConnectorData>,

    tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
}

#[async_trait::async_trait]
impl UdpHolePunchRpc for UdpHolePunchRpcService {
    type Controller = BaseController;

    #[tracing::instrument(skip(self))]
    async fn try_punch_hole(
        &self,
        _: BaseController,
        request: TryPunchHoleRequest,
    ) -> Result<TryPunchHoleResponse, rpc_types::error::Error> {
        let local_mapped_addr = request.local_mapped_addr.ok_or(anyhow::anyhow!(
            "try_punch_hole request missing local_mapped_addr"
        ))?;
        let local_mapped_addr = std::net::SocketAddr::from(local_mapped_addr);
        // local mapped addr will be unspecified if peer is symmetric
        let peer_is_symmetric = local_mapped_addr.ip().is_unspecified();
        let (socket, mapped_addr) =
            self.select_listener(peer_is_symmetric)
                .await
                .ok_or(anyhow::anyhow!(
                    "failed to select listener for hole punching"
                ))?;
        tracing::warn!(?local_mapped_addr, ?mapped_addr, "start hole punching");

        if !peer_is_symmetric {
            let my_udp_nat_type = self
                .data
                .global_ctx
                .get_stun_info_collector()
                .get_stun_info()
                .udp_nat_type;

            // if we are cone, we need to send hole punching resp to client
            if my_udp_nat_type == NatType::PortRestricted as i32
                || my_udp_nat_type == NatType::Restricted as i32
                || my_udp_nat_type == NatType::FullCone as i32
            {
                let notifier = Arc::new(Notify::new());

                let n = notifier.clone();
                // send punch msg to local_mapped_addr for 3 seconds, 3.3 packet per second
                self.tasks.lock().unwrap().spawn(async move {
                    for i in 0..10 {
                        tracing::info!(?local_mapped_addr, "sending hole punching packet");

                        let udp_packet = new_hole_punch_packet(100, HOLE_PUNCH_PACKET_BODY_LEN);
                        let _ = socket
                            .send_to(&udp_packet.into_bytes(), local_mapped_addr)
                            .await;
                        let sleep_ms = if i < 4 { 10 } else { 500 };
                        tokio::time::sleep(std::time::Duration::from_millis(sleep_ms)).await;
                        if i == 3 {
                            n.notify_one();
                        }
                    }
                });

                notifier.notified().await;
            }
        }

        Ok(TryPunchHoleResponse {
            remote_mapped_addr: Some(mapped_addr.into()),
        })
    }

    #[instrument(skip(self))]
    async fn try_punch_symmetric(
        &self,
        _: BaseController,
        request: TryPunchSymmetricRequest,
    ) -> Result<TryPunchSymmetricResponse, rpc_types::error::Error> {
        let listener_addr = request.listener_addr.ok_or(anyhow::anyhow!(
            "try_punch_symmetric request missing listener_addr"
        ))?;
        let listener_addr = std::net::SocketAddr::from(listener_addr);
        let port = request.port as u16;
        let public_ips = request
            .public_ips
            .into_iter()
            .map(|ip| std::net::Ipv4Addr::from(ip))
            .collect::<Vec<_>>();
        let mut min_port = request.min_port as u16;
        let mut max_port = request.max_port as u16;
        let transaction_id = request.transaction_id;
        let round = request.round;
        let last_port_index = request.last_port_index as usize;

        tracing::info!("try_punch_symmetric start");

        let punch_predictablely = self.data.punch_predicablely.load(Ordering::Relaxed);
        let punch_randomly = self.data.punch_randomly.load(Ordering::Relaxed);
        let total_port_count = self.data.shuffled_port_vec.len();
        let listener = self
            .find_listener(&listener_addr)
            .await
            .ok_or(anyhow::anyhow!(
                "try_punch_symmetric failed to find listener"
            ))?;
        let ip_count = public_ips.len();
        if ip_count == 0 {
            tracing::warn!("try_punch_symmetric got zero len public ip");
            return Err(anyhow::anyhow!("try_punch_symmetric got zero len public ip").into());
        }

        min_port = std::cmp::max(1, min_port);
        if max_port == 0 {
            max_port = u16::MAX;
        }
        if max_port < min_port {
            std::mem::swap(&mut min_port, &mut max_port);
        }

        // send max k1 packets if we are predicting the dst port
        let max_k1 = 60;
        // send max k2 packets if we are sending to random port
        let max_k2 = rand::thread_rng().gen_range(600..800);

        // this means the NAT is allocating port in a predictable way
        if max_port.abs_diff(min_port) <= 3 * max_k1 && round <= 6 && punch_predictablely {
            let (min_port, max_port) = {
                // round begin from 0. if round is even, we guess port in increasing order
                let port_delta = (max_k1 as u32) / ip_count as u32;
                let port_diff_for_min = std::cmp::min((round / 2) * port_delta, u16::MAX as u32);
                if round % 2 == 0 {
                    let lower = std::cmp::max(1, port.saturating_add(port_diff_for_min as u16));
                    let upper = lower.saturating_add(port_delta as u16);
                    (lower, upper)
                } else {
                    let upper = std::cmp::max(1, port.saturating_sub(port_diff_for_min as u16));
                    let lower = std::cmp::max(1, upper.saturating_sub(port_delta as u16));
                    (lower, upper)
                }
            };
            let mut ports = (min_port..=max_port).collect::<Vec<_>>();
            ports.push(max_port);
            ports.shuffle(&mut rand::thread_rng());
            self.send_symmetric_hole_punch_packet(
                listener.clone(),
                transaction_id,
                &public_ips,
                &ports,
            )
            .await
            .with_context(|| "failed to send symmetric hole punch packet predict")?;
        }

        if punch_randomly {
            let start = last_port_index % total_port_count;
            let diff = std::cmp::max(10, max_k2 / ip_count);
            let end = std::cmp::min(start + diff, self.data.shuffled_port_vec.len());
            self.send_symmetric_hole_punch_packet(
                listener.clone(),
                transaction_id,
                &public_ips,
                &self.data.shuffled_port_vec[start..end],
            )
            .await
            .with_context(|| "failed to send symmetric hole punch packet randomly")?;

            return if end >= self.data.shuffled_port_vec.len() {
                Ok(TryPunchSymmetricResponse { last_port_index: 1 })
            } else {
                Ok(TryPunchSymmetricResponse {
                    last_port_index: end as u32,
                })
            };
        }

        return Ok(TryPunchSymmetricResponse { last_port_index: 1 });
    }
}

impl UdpHolePunchRpcService {
    pub fn new(data: Arc<UdpHolePunchConnectorData>) -> Self {
        let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new()));
        join_joinset_background(tasks.clone(), "UdpHolePunchRpcServer".to_owned());
        Self { data, tasks }
    }

    async fn find_listener(&self, addr: &SocketAddr) -> Option<Arc<UdpSocket>> {
        let all_listener_sockets = self.data.listeners.lock().await;

        let listener = all_listener_sockets
            .iter()
            .find(|listener| listener.mapped_addr == *addr && listener.running.load())?;

        Some(listener.get_socket().await)
    }

    async fn select_listener(
        &self,
        use_new_listener: bool,
    ) -> Option<(Arc<UdpSocket>, SocketAddr)> {
        let all_listener_sockets = &self.data.listeners;

        // remove listener that is not active for 40 seconds but keep listeners that are selected less than 30 seconds
        all_listener_sockets.lock().await.retain(|listener| {
            listener.last_active_time.load().elapsed().as_secs() < 40
                || listener.last_select_time.load().elapsed().as_secs() < 30
        });

        let mut use_last = false;
        if all_listener_sockets.lock().await.len() < 4 || use_new_listener {
            tracing::warn!("creating new udp hole punching listener");
            all_listener_sockets.lock().await.push(
                UdpHolePunchListener::new(self.data.peer_mgr.clone())
                    .await
                    .ok()?,
            );
            use_last = true;
        }

        let locked = all_listener_sockets.lock().await;

        let listener = if use_last {
            locked.last()?
        } else {
            // use the listener that is active most recently
            locked
                .iter()
                .max_by_key(|listener| listener.last_active_time.load())?
        };

        Some((listener.get_socket().await, listener.mapped_addr))
    }

    #[tracing::instrument(err, ret(level=Level::DEBUG), skip(self, ports))]
    async fn send_symmetric_hole_punch_packet(
        &self,
        udp: Arc<UdpSocket>,
        transaction_id: u32,
        public_ips: &Vec<Ipv4Addr>,
        ports: &[u16],
    ) -> Result<(), Error> {
        tracing::debug!(
            ?public_ips,
            "sending symmetric hole punching packet, ports len: {}",
            ports.len(),
        );
        for port in ports {
            for pub_ip in public_ips {
                let addr = SocketAddr::V4(SocketAddrV4::new(*pub_ip, *port));
                let packet = new_hole_punch_packet(transaction_id, HOLE_PUNCH_PACKET_BODY_LEN);
                udp.send_to(&packet.into_bytes(), addr).await?;
                tokio::time::sleep(Duration::from_millis(2)).await;
            }
        }
        Ok(())
    }
}

pub struct UdpHolePunchConnector {
    data: Arc<UdpHolePunchConnectorData>,
    tasks: JoinSet<()>,
}

// Currently support:
// Symmetric -> Full Cone
// Any Type of Full Cone -> Any Type of Full Cone

// if same level of full cone, node with smaller peer_id will be the initiator
// if different level of full cone, node with more strict level will be the initiator

impl UdpHolePunchConnector {
    pub fn new(global_ctx: ArcGlobalCtx, peer_mgr: Arc<PeerManager>) -> Self {
        Self {
            data: Arc::new(UdpHolePunchConnectorData {
                global_ctx,
                peer_mgr,
                listeners: Arc::new(Mutex::new(Vec::new())),
                shuffled_port_vec: Arc::new(generate_shuffled_port_vec()),
                udp_array: Arc::new(Mutex::new(None)),
                try_direct_connect: AtomicBool::new(true),
                punch_predicablely: AtomicBool::new(true),
                punch_randomly: AtomicBool::new(true),
                udp_array_size: AtomicUsize::new(80),
            }),
            tasks: JoinSet::new(),
        }
    }

    pub async fn run_as_client(&mut self) -> Result<(), Error> {
        let data = self.data.clone();
        self.tasks.spawn(async move {
            Self::main_loop(data).await;
        });

        Ok(())
    }

    pub async fn run_as_server(&mut self) -> Result<(), Error> {
        self.data
            .peer_mgr
            .get_peer_rpc_mgr()
            .rpc_server()
            .registry()
            .register(
                UdpHolePunchRpcServer::new(UdpHolePunchRpcService::new(self.data.clone())),
                &self.data.global_ctx.get_network_name(),
            );

        Ok(())
    }

    pub async fn run(&mut self) -> Result<(), Error> {
        if self.data.global_ctx.get_flags().disable_p2p {
            return Ok(());
        }
        if self.data.global_ctx.get_flags().disable_udp_hole_punching {
            return Ok(());
        }

        self.run_as_client().await?;
        self.run_as_server().await?;

        Ok(())
    }

    async fn collect_peer_to_connect(
        data: Arc<UdpHolePunchConnectorData>,
    ) -> Vec<(PeerId, NatType)> {
        let mut peers_to_connect = Vec::new();

        // do not do anything if:
        // 1. our stun test has not finished
        // 2. our nat type is OpenInternet or NoPat, which means we can wait other peers to connect us
        let my_nat_type = data.my_nat_type();
        if my_nat_type == NatType::Unknown
            || my_nat_type == NatType::OpenInternet
            || my_nat_type == NatType::NoPat
        {
            return peers_to_connect;
        }

        // collect peer list from peer manager and do some filter:
        // 1. peers without direct conns;
        // 2. peers is full cone (any restricted type);
        for route in data.peer_mgr.list_routes().await.iter() {
            let Some(peer_stun_info) = route.stun_info.as_ref() else {
                continue;
            };
            let Ok(peer_nat_type) = NatType::try_from(peer_stun_info.udp_nat_type) else {
                continue;
            };

            let peer_id: PeerId = route.peer_id;
            let conns = data.peer_mgr.list_peer_conns(peer_id).await;
            if conns.is_some() && conns.unwrap().len() > 0 {
                continue;
            }

            // if peer is symmetric ignore it because we cannot connect to it
            // if peer is open internet or no pat, direct connector will connecto to it
            if peer_nat_type == NatType::Unknown
                || peer_nat_type == NatType::OpenInternet
                || peer_nat_type == NatType::NoPat
                || peer_nat_type == NatType::Symmetric
                || peer_nat_type == NatType::SymUdpFirewall
            {
                continue;
            }

            // if we are symmetric, we can only connect to cone peer
            if (my_nat_type == NatType::Symmetric || my_nat_type == NatType::SymUdpFirewall)
                && (peer_nat_type == NatType::Symmetric || peer_nat_type == NatType::SymUdpFirewall)
            {
                continue;
            }

            // if we have smae level of full cone, node with smaller peer_id will be the initiator
            if my_nat_type == peer_nat_type {
                if data.peer_mgr.my_peer_id() > peer_id {
                    continue;
                }
            } else {
                // if we have different level of full cone
                // we will be the initiator if we have more strict level
                if my_nat_type < peer_nat_type {
                    continue;
                }
            }

            tracing::info!(
                ?peer_id,
                ?peer_nat_type,
                ?my_nat_type,
                ?data.global_ctx.id,
                "found peer to do hole punching"
            );

            peers_to_connect.push((peer_id, peer_nat_type));
        }

        peers_to_connect
    }

    async fn try_connect_with_socket(
        socket: Arc<UdpSocket>,
        remote_mapped_addr: SocketAddr,
    ) -> Result<Box<dyn Tunnel>, Error> {
        let connector = UdpTunnelConnector::new(
            format!(
                "udp://{}:{}",
                remote_mapped_addr.ip(),
                remote_mapped_addr.port()
            )
            .to_string()
            .parse()
            .unwrap(),
        );
        connector
            .try_connect_with_socket(socket, remote_mapped_addr)
            .await
            .map_err(|e| Error::from(e))
    }

    #[tracing::instrument(err)]
    async fn do_hole_punching_cone(
        data: Arc<UdpHolePunchConnectorData>,
        dst_peer_id: PeerId,
    ) -> Result<Box<dyn Tunnel>, anyhow::Error> {
        tracing::info!(?dst_peer_id, "start hole punching");
        // client: choose a local udp port, and get the pubic mapped port from stun server
        let socket = {
            let _g = data.global_ctx.net_ns.guard();
            UdpSocket::bind("0.0.0.0:0").await.with_context(|| "")?
        };
        let local_socket_addr = socket.local_addr()?;
        let local_port = socket.local_addr()?.port();
        drop(socket); // drop the socket to release the port

        let local_mapped_addr = data
            .global_ctx
            .get_stun_info_collector()
            .get_udp_port_mapping(local_port)
            .await
            .with_context(|| "failed to get udp port mapping")?;

        // client -> server: tell server the mapped port, server will return the mapped address of listening port.
        let rpc_stub = data
            .peer_mgr
            .get_peer_rpc_mgr()
            .rpc_client()
            .scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
                data.peer_mgr.my_peer_id(),
                dst_peer_id,
                data.global_ctx.get_network_name(),
            );

        let remote_mapped_addr = rpc_stub
            .try_punch_hole(
                BaseController {},
                TryPunchHoleRequest {
                    local_mapped_addr: Some(local_mapped_addr.into()),
                },
            )
            .await?
            .remote_mapped_addr
            .ok_or(anyhow::anyhow!("failed to get remote mapped addr"))?;

        // server: will send some punching resps, total 10 packets.
        // client: use the socket to create UdpTunnel with UdpTunnelConnector
        // NOTICE: UdpTunnelConnector will ignore the punching resp packet sent by remote.
        let _g = data.global_ctx.net_ns.guard();
        let socket2_socket = socket2::Socket::new(
            socket2::Domain::for_address(local_socket_addr),
            socket2::Type::DGRAM,
            Some(socket2::Protocol::UDP),
        )?;
        setup_sokcet2(&socket2_socket, &local_socket_addr)?;
        let socket = Arc::new(UdpSocket::from_std(socket2_socket.into())?);

        Ok(
            Self::try_connect_with_socket(socket, remote_mapped_addr.into())
                .await
                .with_context(|| "UdpTunnelConnector failed to connect remote")?,
        )
    }

    #[tracing::instrument(err(level = Level::ERROR))]
    async fn do_hole_punching_symmetric(
        data: Arc<UdpHolePunchConnectorData>,
        dst_peer_id: PeerId,
    ) -> Result<Box<dyn Tunnel>, anyhow::Error> {
        let Some(udp_array) = data.udp_array.lock().await.clone() else {
            return Err(anyhow::anyhow!("udp array not started"));
        };

        let rpc_stub = data
            .peer_mgr
            .get_peer_rpc_mgr()
            .rpc_client()
            .scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
                data.peer_mgr.my_peer_id(),
                dst_peer_id,
                data.global_ctx.get_network_name(),
            );

        let local_mapped_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
        let remote_mapped_addr = rpc_stub
            .try_punch_hole(
                BaseController {},
                TryPunchHoleRequest {
                    local_mapped_addr: Some(local_mapped_addr.into()),
                },
            )
            .await?
            .remote_mapped_addr
            .ok_or(anyhow::anyhow!("failed to get remote mapped addr"))?
            .into();

        // try direct connect first
        if data.try_direct_connect.load(Ordering::Relaxed) {
            if let Ok(tunnel) = Self::try_connect_with_socket(
                Arc::new(UdpSocket::bind("0.0.0.0:0").await?),
                remote_mapped_addr,
            )
            .await
            {
                return Ok(tunnel);
            }
        }

        let tid = rand::thread_rng().gen();
        let packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
        udp_array.add_intreast_tid(tid);
        defer! { udp_array.remove_intreast_tid(tid);}
        udp_array.send_with_all(&packet, remote_mapped_addr).await?;

        // get latest port mapping
        let local_mapped_addr = data
            .global_ctx
            .get_stun_info_collector()
            .get_udp_port_mapping(0)
            .await?;
        let port = local_mapped_addr.port();
        let IpAddr::V4(ipv4) = local_mapped_addr.ip() else {
            return Err(anyhow::anyhow!("failed to get local mapped addr"));
        };
        let stun_info = data.global_ctx.get_stun_info_collector().get_stun_info();
        let mut public_ips: Vec<Ipv4Addr> = stun_info
            .public_ip
            .iter()
            .map(|x| x.parse().unwrap())
            .collect();
        if !public_ips.contains(&ipv4) {
            public_ips.push(ipv4);
        }
        if public_ips.is_empty() {
            return Err(anyhow::anyhow!("failed to get public ips"));
        }

        let mut last_port_idx = rand::thread_rng().gen_range(0..data.shuffled_port_vec.len());

        for round in 0..5 {
            let ret = rpc_stub
                .try_punch_symmetric(
                    BaseController {},
                    TryPunchSymmetricRequest {
                        listener_addr: Some(remote_mapped_addr.into()),
                        port: port as u32,
                        public_ips: public_ips.clone().into_iter().map(|x| x.into()).collect(),
                        min_port: stun_info.min_port as u32,
                        max_port: stun_info.max_port as u32,
                        transaction_id: tid,
                        round,
                        last_port_index: last_port_idx as u32,
                    },
                )
                .await;
            tracing::info!(?ret, "punch symmetric return");

            let next_last_port_idx = match ret {
                Ok(s) => s.last_port_index as usize,
                Err(err) => {
                    tracing::error!(?err, "failed to get remote mapped addr");
                    rand::thread_rng().gen_range(0..data.shuffled_port_vec.len())
                }
            };

            // wait for some time to increase the chance of receiving hole punching packet
            tokio::time::sleep(Duration::from_secs(2)).await;

            // no matter what the result is, we should check if we received any hole punching packet
            while let Some(socket) = udp_array.try_fetch_punched_socket(tid) {
                if let Ok(tunnel) = Self::try_connect_with_socket(socket, remote_mapped_addr).await
                {
                    return Ok(tunnel);
                }
            }

            last_port_idx = next_last_port_idx;
        }

        return Err(anyhow::anyhow!("udp array not started"));
    }

    async fn peer_punching_task(
        data: Arc<UdpHolePunchConnectorData>,
        peer_id: PeerId,
    ) -> Result<(), anyhow::Error> {
        const MAX_BACKOFF_TIME: u64 = 300;
        let mut backoff_time = vec![15, 15, 30, 30, 60, 120, 180, MAX_BACKOFF_TIME];
        let my_nat_type = data.my_nat_type();

        loop {
            let ret = if my_nat_type == NatType::FullCone
                || my_nat_type == NatType::Restricted
                || my_nat_type == NatType::PortRestricted
            {
                Self::do_hole_punching_cone(data.clone(), peer_id).await
            } else {
                Self::do_hole_punching_symmetric(data.clone(), peer_id).await
            };

            match ret {
                Err(_) => {
                    tokio::time::sleep(Duration::from_secs(
                        backoff_time.pop().unwrap_or(MAX_BACKOFF_TIME),
                    ))
                    .await;
                    continue;
                }

                Ok(tunnel) => {
                    let _ = data
                        .peer_mgr
                        .add_client_tunnel(tunnel)
                        .await
                        .with_context(|| {
                            "failed to add tunnel as client in hole punch connector"
                        })?;
                    break;
                }
            }
        }

        Ok(())
    }

    async fn main_loop(data: Arc<UdpHolePunchConnectorData>) {
        type JoinTaskRet = Result<(), anyhow::Error>;
        type JoinTask = ScopedTask<JoinTaskRet>;
        let punching_task = Arc::new(DashMap::<(PeerId, NatType), JoinTask>::new());
        let mut last_my_nat_type = NatType::Unknown;

        loop {
            let my_nat_type = data.my_nat_type();
            let peers_to_connect = Self::collect_peer_to_connect(data.clone()).await;

            // remove task not in peers_to_connect
            let mut to_remove = vec![];
            for item in punching_task.iter() {
                if !peers_to_connect.contains(item.key())
                    || item.value().is_finished()
                    || my_nat_type != last_my_nat_type
                {
                    to_remove.push(item.key().clone());
                }
            }
            for key in to_remove {
                if let Some((_, task)) = punching_task.remove(&key) {
                    task.abort();
                    match task.await {
                        Ok(Ok(_)) => {}
                        Ok(Err(task_ret)) => {
                            tracing::error!(?task_ret, "hole punching task failed");
                        }
                        Err(e) => {
                            tracing::error!(?e, "hole punching task aborted");
                        }
                    }
                }
            }

            last_my_nat_type = my_nat_type;

            if !peers_to_connect.is_empty() {
                for item in peers_to_connect {
                    if punching_task.contains_key(&item) {
                        continue;
                    }

                    let my_nat_type = data.my_nat_type();
                    if my_nat_type == NatType::Symmetric || my_nat_type == NatType::SymUdpFirewall {
                        let mut udp_array = data.udp_array.lock().await;
                        if udp_array.is_none() {
                            *udp_array = Some(Arc::new(UdpSocketArray::new(
                                data.udp_array_size.load(Ordering::Relaxed),
                                data.global_ctx.net_ns.clone(),
                            )));
                        }
                        let udp_array = udp_array.as_ref().unwrap();
                        udp_array.start().await.unwrap();
                    }

                    punching_task.insert(
                        item,
                        tokio::spawn(Self::peer_punching_task(data.clone(), item.0)).into(),
                    );
                }
            } else if punching_task.is_empty() {
                data.udp_array.lock().await.take();
            }

            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
        }
    }
}

#[cfg(test)]
pub mod tests {
    use std::sync::atomic::AtomicU32;
    use std::sync::Arc;
    use std::time::Duration;

    use tokio::net::UdpSocket;

    use crate::common::stun::MockStunInfoCollector;
    use crate::proto::common::NatType;
    use crate::tunnel::common::tests::wait_for_condition;

    use crate::{
        connector::udp_hole_punch::UdpHolePunchConnector,
        peers::{
            peer_manager::PeerManager,
            tests::{
                connect_peer_manager, create_mock_peer_manager, wait_route_appear,
                wait_route_appear_with_cost,
            },
        },
    };

    pub fn replace_stun_info_collector(peer_mgr: Arc<PeerManager>, udp_nat_type: NatType) {
        let collector = Box::new(MockStunInfoCollector { udp_nat_type });
        peer_mgr
            .get_global_ctx()
            .replace_stun_info_collector(collector);
    }

    pub async fn create_mock_peer_manager_with_mock_stun(
        udp_nat_type: NatType,
    ) -> Arc<PeerManager> {
        let p_a = create_mock_peer_manager().await;
        replace_stun_info_collector(p_a.clone(), udp_nat_type);
        p_a
    }

    #[tokio::test]
    async fn hole_punching_cone() {
        let p_a = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await;
        let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
        let p_c = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await;
        connect_peer_manager(p_a.clone(), p_b.clone()).await;
        connect_peer_manager(p_b.clone(), p_c.clone()).await;

        wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();

        println!("{:?}", p_a.list_routes().await);

        let mut hole_punching_a = UdpHolePunchConnector::new(p_a.get_global_ctx(), p_a.clone());
        let mut hole_punching_c = UdpHolePunchConnector::new(p_c.get_global_ctx(), p_c.clone());

        hole_punching_a.run().await.unwrap();
        hole_punching_c.run().await.unwrap();

        wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1))
            .await
            .unwrap();
        println!("{:?}", p_a.list_routes().await);
    }

    #[tokio::test]
    async fn hole_punching_symmetric_only_random() {
        let p_a = create_mock_peer_manager_with_mock_stun(NatType::Symmetric).await;
        let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
        let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
        connect_peer_manager(p_a.clone(), p_b.clone()).await;
        connect_peer_manager(p_b.clone(), p_c.clone()).await;
        wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();

        let mut hole_punching_a = UdpHolePunchConnector::new(p_a.get_global_ctx(), p_a.clone());
        let mut hole_punching_c = UdpHolePunchConnector::new(p_c.get_global_ctx(), p_c.clone());

        hole_punching_a
            .data
            .try_direct_connect
            .store(false, std::sync::atomic::Ordering::Relaxed);

        hole_punching_c
            .data
            .punch_predicablely
            .store(false, std::sync::atomic::Ordering::Relaxed);

        hole_punching_a.run().await.unwrap();
        hole_punching_c.run().await.unwrap();

        wait_for_condition(
            || async { hole_punching_a.data.udp_array.lock().await.is_some() },
            Duration::from_secs(5),
        )
        .await;

        wait_for_condition(
            || async {
                wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1))
                    .await
                    .is_ok()
            },
            Duration::from_secs(30),
        )
        .await;
        println!("{:?}", p_a.list_routes().await);

        wait_for_condition(
            || async { hole_punching_a.data.udp_array.lock().await.is_none() },
            Duration::from_secs(20),
        )
        .await;
    }

    #[tokio::test]
    async fn hole_punching_symmetric_only_predict() {
        let p_a = create_mock_peer_manager_with_mock_stun(NatType::Symmetric).await;
        let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
        let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
        connect_peer_manager(p_a.clone(), p_b.clone()).await;
        connect_peer_manager(p_b.clone(), p_c.clone()).await;
        wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();

        let mut hole_punching_a = UdpHolePunchConnector::new(p_a.get_global_ctx(), p_a.clone());
        let mut hole_punching_c = UdpHolePunchConnector::new(p_c.get_global_ctx(), p_c.clone());

        hole_punching_a
            .data
            .try_direct_connect
            .store(false, std::sync::atomic::Ordering::Relaxed);
        hole_punching_a
            .data
            .udp_array_size
            .store(0, std::sync::atomic::Ordering::Relaxed);

        hole_punching_c
            .data
            .punch_randomly
            .store(false, std::sync::atomic::Ordering::Relaxed);

        hole_punching_a.run().await.unwrap();
        hole_punching_c.run().await.unwrap();

        let udp_self = Arc::new(UdpSocket::bind("0.0.0.0:40144").await.unwrap());
        let udp_inc = Arc::new(UdpSocket::bind("0.0.0.0:40147").await.unwrap());
        let udp_inc2 = Arc::new(UdpSocket::bind("0.0.0.0:40200").await.unwrap());
        let udp_dec = Arc::new(UdpSocket::bind("0.0.0.0:40140").await.unwrap());
        let udp_dec2 = Arc::new(UdpSocket::bind("0.0.0.0:40050").await.unwrap());
        let udps = vec![udp_self, udp_inc, udp_inc2, udp_dec, udp_dec2];

        let counter = Arc::new(AtomicU32::new(0));

        // all these sockets should receive hole punching packet
        for udp in udps.iter().map(Arc::clone) {
            let counter = counter.clone();
            tokio::spawn(async move {
                let mut buf = [0u8; 1024];
                let (len, addr) = udp.recv_from(&mut buf).await.unwrap();
                println!("{:?} {:?} {:?}", len, addr, udp.local_addr());
                counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            });
        }

        let udp_len = udps.len();
        wait_for_condition(
            || async { counter.load(std::sync::atomic::Ordering::Relaxed) == udp_len as u32 },
            Duration::from_secs(30),
        )
        .await;
    }
}
