use super::challenge::Challenge;
use super::constants::*;
use super::network_callback::NetworkCallback;
use super::trade::Trade;
use super::types::SeedInfo;
use super::types::{NetworkData, NetworkGame, NetworkRequestState, NetworkTeam};
use crate::app::AppEvent;
use crate::core::world::World;
use crate::core::Team;
use crate::game_engine::types::TeamInGame;
use crate::game_engine::{Tournament, TournamentId};
use crate::network::network_store_data::NetworkStoreData;
use crate::network::types::TournamentRequestState;
use crate::store::serialize;
use crate::types::{AppResult, GameId, HashMapWithResult, PlayerMap};
use crate::types::{PlayerId, TeamId};
use crate::types::{SystemTimeTick, Tick};
use anyhow::anyhow;
use futures::StreamExt;
use itertools::Itertools;
use libp2p::gossipsub::{self, IdentTopic};
use libp2p::identity::Keypair;
use libp2p::multiaddr::Protocol;
use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::{DialError, NetworkBehaviour, SwarmEvent};
use libp2p::{
    autonat, dcutr, identify, identity, kad, noise, relay, tcp, yamux, PeerId, StreamProtocol,
    TransportError,
};
use libp2p::{Multiaddr, Swarm};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;

/// Strip /p2p/ suffix and filter out non-routable address.
pub(crate) fn sanitize_addr(addr: &Multiaddr) -> Option<Multiaddr> {
    let clean: Multiaddr = addr
        .iter()
        .filter(|p| !matches!(p, Protocol::P2p(_)))
        .collect();
    if is_routable(&clean) {
        Some(clean)
    } else {
        None
    }
}

/// Strip /p2p/ suffixes and filter out non-routable addresses from identify listen_addrs.
pub(crate) fn sanitize_listen_addrs(addrs: &[Multiaddr]) -> Vec<Multiaddr> {
    addrs.iter().filter_map(sanitize_addr).collect()
}

/// Whether a multiaddr routes through a relay circuit (contains /p2p-circuit).
/// Such addresses are relay-dependent, so they are kept out of the Kademlia DHT.
pub(crate) fn is_relay_circuit(addr: &Multiaddr) -> bool {
    addr.iter().any(|p| matches!(p, Protocol::P2pCircuit))
}

/// Extract the IP from an observed address and combine it with our actual listen port.
/// The observed_addr from identify contains the correct public IP but the wrong
/// (ephemeral) port from the outbound connection.
fn build_external_addr_from_observed(
    observed_addr: &Multiaddr,
    listen_port: u16,
) -> Option<Multiaddr> {
    let mut ip_proto = None;
    for proto in observed_addr.iter() {
        match proto {
            Protocol::Ip4(ip) if !ip.is_unspecified() && !ip.is_loopback() => {
                ip_proto = Some(Protocol::Ip4(ip));
                break;
            }
            Protocol::Ip6(ip) if !ip.is_unspecified() && !ip.is_loopback() => {
                ip_proto = Some(Protocol::Ip6(ip));
                break;
            }
            _ => {}
        }
    }
    let ip = ip_proto?;
    let mut addr = Multiaddr::empty();
    addr.push(ip);
    addr.push(Protocol::Tcp(listen_port));
    Some(addr)
}

fn is_routable(addr: &Multiaddr) -> bool {
    for proto in addr.iter() {
        match proto {
            Protocol::Ip4(ip) => {
                if ip.is_unspecified() || ip.is_loopback() || ip.is_private() || ip.is_link_local()
                {
                    return false;
                }
            }
            Protocol::Ip6(ip) => {
                // fe80::/10 are IPv6 link-local addresses, not routable beyond the local link
                if ip.is_unspecified() || ip.is_loopback() || (ip.segments()[0] & 0xffc0) == 0xfe80
                {
                    return false;
                }
            }
            _ => {}
        }
    }
    true
}

#[derive(NetworkBehaviour)]
pub struct Behaviour {
    pub gossipsub: gossipsub::Behaviour,
    pub identify: identify::Behaviour,
    pub kademlia: kad::Behaviour<kad::store::MemoryStore>,
    // NAT traversal. Every node runs the client side; only the relayer build
    // (`--features relayer`) compiles the server side.
    pub relay_client: relay::client::Behaviour,
    pub dcutr: dcutr::Behaviour,
    pub autonat_client: autonat::v2::client::Behaviour,
    // Server roles only exist in the relayer build (`--features relayer`).
    #[cfg(feature = "relayer")]
    pub relay_server: relay::Behaviour,
    #[cfg(feature = "relayer")]
    pub autonat_server: autonat::v2::server::Behaviour,
}

#[derive(Debug, Default)]
enum SwarmStatus {
    #[default]
    Uninitialized,
    Ready {
        sender: mpsc::Sender<SwarmCommand>,
    },
}

#[derive(Debug, Clone)]
enum SwarmCommand {
    Dial {
        address: Multiaddr,
    },
    Send {
        topic: IdentTopic,
        data: Vec<u8>,
    },
    /// Resolve a peer's current address through the Kademlia DHT, then dial it.
    /// Used for self-healing reconnection without the relayer.
    FindPeer {
        peer_id: PeerId,
    },
}

#[derive(Debug)]
pub struct NetworkHandler {
    local_keypair: Keypair,
    pub connected_peers_count: usize,
    own_peer_id: PeerId,
    pub seed_addresses: Vec<Multiaddr>,
    swarm_status: SwarmStatus,
    /// Whether we have already kicked off the initial dial of the seed and known
    /// peers. `bind_address` fires once per listen address (TCP/QUIC x v4/v6 plus
    /// relay reservations), so without this guard we would re-dial everyone many
    /// times over and collide on the reused TCP source port.
    initial_dial_done: bool,
}

impl NetworkHandler {
    fn new_swarm(
        keypair: Keypair,
        tcp_port: u16,
        use_ipv4: bool,
        use_ipv6: bool,
    ) -> AppResult<Swarm<Behaviour>> {
        // To content-address message, we can take the hash of message and use it as an ID.
        let message_id_fn = |message: &gossipsub::Message| {
            let mut s = DefaultHasher::new();
            message.data.hash(&mut s);
            gossipsub::MessageId::from(s.finish().to_string())
        };

        // Set a custom gossipsub configuration
        let gossipsub_config = gossipsub::ConfigBuilder::default()
            .heartbeat_interval(Duration::from_secs(1))
            .validation_mode(gossipsub::ValidationMode::Strict)
            .message_id_fn(message_id_fn)
            .build()
            .expect("Valid config");

        let mut gossipsub = gossipsub::Behaviour::new(
            gossipsub::MessageAuthenticity::Signed(keypair.clone()),
            gossipsub_config,
        )
        .expect("Correct configuration");

        gossipsub.subscribe(&IdentTopic::new(TOPIC))?;

        let peer_id = keypair.public().to_peer_id();

        let identify = identify::Behaviour::new(
            identify::Config::new("/rebels/1.0.0".to_string(), keypair.public())
                .with_push_listen_addr_updates(true),
        );

        let mut kad_config = kad::Config::new(StreamProtocol::new("/rebels/kad/1.0.0"));
        kad_config.set_query_timeout(Duration::from_secs(60));
        let store = kad::store::MemoryStore::new(peer_id);
        let kademlia = kad::Behaviour::with_config(peer_id, store, kad_config);

        let mut swarm = libp2p::SwarmBuilder::with_existing_identity(keypair)
            .with_tokio()
            .with_tcp(
                tcp::Config::default(),
                noise::Config::new,
                yamux::Config::default,
            )?
            .with_quic()
            .with_dns()?
            .with_relay_client(noise::Config::new, yamux::Config::default)?
            .with_behaviour(|_keypair, relay_client| Behaviour {
                gossipsub,
                identify,
                kademlia,
                relay_client,
                dcutr: dcutr::Behaviour::new(peer_id),
                autonat_client: autonat::v2::client::Behaviour::default(),
                #[cfg(feature = "relayer")]
                relay_server: relay::Behaviour::new(peer_id, relay::Config::default()),
                #[cfg(feature = "relayer")]
                autonat_server: autonat::v2::server::Behaviour::default(),
            })?
            .with_swarm_config(|cfg| {
                cfg.with_idle_connection_timeout(Duration::from_secs(u64::MAX))
            })
            .build();

        let mut succesful_listen_on = false;

        // Listen on QUIC (UDP) and TCP. QUIC is preferred for hole punching.
        if use_ipv6 {
            for addr in [
                format!("/ip6/::/udp/{tcp_port}/quic-v1"),
                format!("/ip6/::/tcp/{tcp_port}"),
            ] {
                if let Err(e) = swarm.listen_on(addr.parse()?) {
                    log::error!("Could not listen on ip6 {addr}: {e}");
                } else {
                    succesful_listen_on = true;
                }
            }
        }

        // Fallback to ipv4 if ipv6 gave an error.
        if use_ipv4 || !succesful_listen_on {
            for addr in [
                format!("/ip4/0.0.0.0/udp/{tcp_port}/quic-v1"),
                format!("/ip4/0.0.0.0/tcp/{tcp_port}"),
            ] {
                if let Err(e) = swarm.listen_on(addr.parse()?) {
                    log::error!("Could not listen on ip4 {addr}: {e}");
                } else {
                    succesful_listen_on = true;
                }
            }
        }

        if !succesful_listen_on {
            return Err(anyhow!("Swarm could not start listening."));
        }

        Ok(swarm)
    }

    pub fn test_default() -> Self {
        let local_keypair = identity::Keypair::generate_ed25519();
        let own_peer_id = PeerId::from_public_key(&local_keypair.public());
        Self {
            local_keypair,
            connected_peers_count: 0,
            own_peer_id,
            seed_addresses: vec![],
            swarm_status: SwarmStatus::Uninitialized,
            initial_dial_done: false,
        }
    }

    pub fn new(seed_node_ip: Option<&String>) -> AppResult<Self> {
        let local_keypair = identity::Keypair::generate_ed25519();
        let mut seed_addresses = vec![
            format!("/dns4/{DEFAULT_SEED_URL}/tcp/{DEFAULT_SEED_PORT}")
                .parse()
                .expect("Invalid default seed address."),
            format!("/ip6/{DEFAULT_SEED_IPV6}/tcp/{DEFAULT_SEED_PORT}")
                .parse()
                .expect("Invalid provided seed ip."),
        ];

        if let Some(ip) = seed_node_ip {
            if let Ok(address) = format!("/ip4/{ip}/tcp/{DEFAULT_SEED_PORT}").parse() {
                seed_addresses.push(address);
            } else if let Ok(address) = format!("/ip6/{ip}/tcp/{DEFAULT_SEED_PORT}").parse() {
                seed_addresses.push(address);
            }
        }

        log::info!(
            "Network handler created with {} seed addresses.",
            seed_addresses.len()
        );

        let own_peer_id = local_keypair.public().to_peer_id();

        Ok(Self {
            local_keypair,
            connected_peers_count: 0,
            own_peer_id,
            seed_addresses,
            swarm_status: SwarmStatus::Uninitialized,
            initial_dial_done: false,
        })
    }

    pub fn with_keypair(mut self, keypair: Keypair) -> Self {
        self.local_keypair = keypair;
        self.own_peer_id = self.local_keypair.public().to_peer_id();

        self
    }

    pub fn set_keypair(&mut self, keypair: Keypair) {
        self.local_keypair = keypair;
        self.own_peer_id = self.local_keypair.public().to_peer_id();
    }

    pub fn keypair_bytes(&self) -> AppResult<Vec<u8>> {
        let bytes = self.local_keypair.to_protobuf_encoding()?;
        Ok(bytes)
    }

    pub fn own_peer_id(&self) -> &PeerId {
        &self.own_peer_id
    }

    pub fn start_polling_events(
        &mut self,
        event_sender: mpsc::Sender<AppEvent>,
        cancellation_token: CancellationToken,
        tcp_port: u16,
        use_ipv4: bool,
        use_ipv6: bool,
    ) -> JoinHandle<()> {
        let local_keypair = self.local_keypair.clone();
        let own_peer_id = *self.own_peer_id();
        let seed_addresses = self.seed_addresses.clone();

        // Sized to absorb a reconnect burst: dial_known_peers can enqueue a Dial
        // and a FindPeer per known peer at once.
        let (sender, mut receiver) = mpsc::channel(256);

        self.swarm_status = SwarmStatus::Ready { sender };
        let handle = tokio::spawn(async move {
            let mut swarm =
                if let Ok(swarm) = Self::new_swarm(local_keypair, tcp_port, use_ipv4, use_ipv6) {
                    swarm
                } else {
                    return;
                };

            assert_eq!(own_peer_id, *swarm.local_peer_id());
            let mut kad_bootstrapped = false;
            // Whether we have already requested a relay reservation. Done once,
            // on the first relay we see, so NATed peers get a reachable address.
            let mut relay_reserved = false;
            // The relay circuit base (`/<relay>/p2p/<relay-id>/p2p-circuit`) we
            // reserved on. Appending `/p2p/<target>` gives a dialable address that
            // reaches any peer through the relay, which DCUtR then upgrades.
            let mut relay_circuit: Option<Multiaddr> = None;

            loop {
                tokio::select! {
                    _ = cancellation_token.cancelled() => {
                        log::info!("NetworkHandler background task shutting down.");
                        if !swarm.behaviour_mut().gossipsub.unsubscribe(&IdentTopic::new(TOPIC)) {
                            log::error!("Cannot unsubscribe from events");
                        }

                        let connected_peers = swarm.connected_peers().cloned().collect_vec();
                        for peer_id in connected_peers {
                            let _ = swarm
                                .disconnect_peer_id(peer_id)
                                .map_err(|e| log::error!("Error disconnecting peer id {peer_id}: {e:?}"));
                        }

                        break;
                    }

                    event = swarm.select_next_some() => {
                        // Handle identify/kad events inside the swarm task
                        // where we have direct access to the swarm.
                        match &event {
                            SwarmEvent::Behaviour(BehaviourEvent::Identify(
                                identify::Event::Received { peer_id, info, .. }
                            )) => {
                                // Add only direct (non-circuit) routable addresses to the DHT;
                                // circuit addresses are relay-dependent.
                                let mut added = 0;
                                for addr in sanitize_listen_addrs(&info.listen_addrs) {
                                    if is_relay_circuit(&addr) {
                                        continue;
                                    }
                                    swarm.behaviour_mut().kademlia.add_address(peer_id, addr);
                                    added += 1;
                                }
                                log::debug!(
                                    "Identify from {peer_id}: added {added} routable addrs to Kademlia (from {} total)",
                                    info.listen_addrs.len()
                                );

                                // If this peer is a relay and we have not reserved a slot yet,
                                // listen on a circuit address through it so we become reachable from behind a NAT.
                                // DCUtR then upgrades the relayed connection to a direct one.
                                if !relay_reserved
                                    && info.protocols.contains(&relay::HOP_PROTOCOL_NAME)
                                {
                                    for seed in &seed_addresses {
                                        let circuit = seed
                                            .clone()
                                            .with(Protocol::P2p(*peer_id))
                                            .with(Protocol::P2pCircuit);
                                        match swarm.listen_on(circuit.clone()) {
                                            Ok(_) => {
                                                relay_reserved = true;
                                                if relay_circuit.is_none() {
                                                    relay_circuit = Some(circuit.clone());
                                                }
                                                log::info!("Requesting relay reservation via {circuit}");
                                            }
                                            Err(e) => log::debug!(
                                                "Could not listen on relay circuit {circuit}: {e}"
                                            ),
                                        }
                                    }
                                }

                                // Build a proper external address from the observed IP and our actual listen port.
                                // The observed_addr contains our public IP as seen by the remote peer,
                                // but with an ephemeral source port, not our listen port.
                                // Using it as-is would advertise unreachable addresses.
                                // Skip when listening on an OS-assigned ephemeral port (0):
                                // the configured port is not our real listen port, so we must not advertise a bogus /tcp/0 external address.
                                if tcp_port != 0 {
                                    if let Some(ext_addr) = build_external_addr_from_observed(&info.observed_addr, tcp_port) {
                                        log::debug!("Adding external address from observed: {ext_addr}");
                                        swarm.add_external_address(ext_addr);
                                    }
                                }
                            }
                            SwarmEvent::Behaviour(BehaviourEvent::AutonatClient(
                                autonat::v2::client::Event { result, tested_addr, .. }
                            )) => {
                                let mode = if result.is_ok() {
                                    kad::Mode::Server
                                } else {
                                    kad::Mode::Client
                                };
                                log::info!("AutoNAT result for {tested_addr}: {result:?} -> kad {mode:?}");
                                swarm.behaviour_mut().kademlia.set_mode(Some(mode));
                            }
                            SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => {
                                log::info!("DCUtR event: {event:?}");
                            }
                            SwarmEvent::Behaviour(BehaviourEvent::Kademlia(
                                kad::Event::OutboundQueryProgressed {
                                    result: kad::QueryResult::GetClosestPeers(Ok(ok)),
                                    step,
                                    ..
                                }
                            )) if step.last => {
                                // A FindPeer lookup finished: dial the target now that the DHT
                                // has supplied addresses for it (self-healing reconnection).
                                if let Ok(target) = PeerId::from_bytes(&ok.key) {
                                    if target != own_peer_id {
                                        // New port, not the reused listen port: avoids
                                        // EADDRINUSE on redundant dials (reuse is only for
                                        // DCUtR, which sets its own DialOpts).
                                        let _ = swarm.dial(
                                            DialOpts::peer_id(target).allocate_new_port().build(),
                                        );
                                    }
                                }
                            }
                            SwarmEvent::NewExternalAddrOfPeer { peer_id, address } => {
                                if let Some(clean) = sanitize_addr(address) {
                                    if !is_relay_circuit(&clean) {
                                        swarm.behaviour_mut().kademlia.add_address(peer_id, clean);
                                    }
                                }
                            }
                            SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
                                let kind = if is_relay_circuit(endpoint.get_remote_address()) {
                                    "relayed"
                                } else {
                                    "direct"
                                };
                                log::info!(
                                    "Connection established with {peer_id} ({kind}): {}",
                                    endpoint.get_remote_address()
                                );
                                if !kad_bootstrapped
                                    && swarm.behaviour_mut().kademlia.bootstrap().is_ok()
                                {
                                    kad_bootstrapped = true;
                                    log::info!("Kademlia bootstrap initiated");
                                }
                            }
                            _ => {}
                        }

                        if event_sender.send(AppEvent::NetworkEvent(event)).await.is_err() {
                            log::warn!("App receiver dropped; stopping network loop");
                            break;
                        }
                    }

                    Some(command) = receiver.recv() => {
                        match command {
                            SwarmCommand::Send { topic, data } => {
                                if let Err(e) = swarm
                                    .behaviour_mut()
                                    .gossipsub
                                    .publish(topic, data) {
                                        log::error!("Swarm send error: {e}");
                                    }
                            }
                            SwarmCommand::Dial { address } => {
                                log::debug!("Dialing address: {address}");
                                // New port (see FindPeer dial): avoids EADDRINUSE.
                                let opts = DialOpts::unknown_peer_id()
                                    .address(address.clone())
                                    .allocate_new_port()
                                    .build();
                                if let Err(e) = swarm.dial(opts) {
                                    log::error!("Swarm dial error for {address}: {e}");
                                }
                            }
                            SwarmCommand::FindPeer { peer_id } => {
                                log::debug!("Looking up peer {peer_id} in the DHT");
                                swarm.behaviour_mut().kademlia.get_closest_peers(peer_id);
                                // Also reach the peer through the relay circuit; DCUtR then upgrades the relayed connection to a direct one.
                                if let Some(base) = &relay_circuit {
                                    let via_relay = base.clone().with(Protocol::P2p(peer_id));
                                    let opts = DialOpts::unknown_peer_id()
                                        .address(via_relay.clone())
                                        .allocate_new_port()
                                        .build();
                                    if let Err(e) = swarm.dial(opts) {
                                        log::debug!("Could not dial {peer_id} via relay {via_relay}: {e}");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        });

        handle
    }

    fn _send(&self, data: &NetworkData) -> AppResult<()> {
        match &self.swarm_status {
            SwarmStatus::Uninitialized => {}
            SwarmStatus::Ready { sender } => {
                let data = serialize(data)?;
                sender.try_send(SwarmCommand::Send {
                    topic: IdentTopic::new(TOPIC),
                    data,
                })?;
            }
        }
        Ok(())
    }

    pub fn dial_seed(&mut self) -> AppResult<()> {
        match &self.swarm_status {
            SwarmStatus::Uninitialized => {}
            SwarmStatus::Ready { sender } => {
                for address in self.seed_addresses.iter() {
                    sender.try_send(SwarmCommand::Dial {
                        address: address.clone(),
                    })?;
                }
            }
        }

        Ok(())
    }

    pub fn dial_address(&mut self, address: Multiaddr) -> AppResult<()> {
        match &self.swarm_status {
            SwarmStatus::Uninitialized => {}
            SwarmStatus::Ready { sender } => {
                sender.try_send(SwarmCommand::Dial { address })?;
            }
        }

        Ok(())
    }

    /// Dial every known peer address (skipping our own). Used at startup and when
    /// reconnecting after losing all connections, so that the relayer is not the
    /// only way back into the network.
    pub fn dial_known_peers(
        &mut self,
        peer_addresses: &HashMap<PeerId, Multiaddr>,
    ) -> AppResult<()> {
        let own_peer_id = self.own_peer_id;
        for (peer_id, address) in peer_addresses.iter() {
            if *peer_id == own_peer_id {
                continue;
            }
            // Try the last known direct address. Skip relay-circuit addresses:
            // sanitize_addr strips the relay PeerId they need, so they are not
            // dialable; NATed peers are reached via the derived circuit in find_peer.
            if let Some(clean) = sanitize_addr(address) {
                if !is_relay_circuit(&clean) {
                    if let Err(e) = self.dial_address(clean.clone()) {
                        log::warn!("dial_known_peers: failed to dial {peer_id} at {clean}: {e}");
                    }
                }
            }
            // Also resolve the peer through the DHT: this recovers a current address
            // even if the stored one is stale, and works without the relayer.
            // Best-effort: a full command channel must not abort the whole reconnect.
            if let Err(e) = self.find_peer(*peer_id) {
                log::warn!("dial_known_peers: failed to enqueue DHT lookup for {peer_id}: {e}");
            }
        }
        Ok(())
    }

    /// Dial the seed and known peers exactly once. `bind_address` calls this on every `NewListenAddr`,
    /// but we only want one initial dial round; later reconnection after a drop is handled by the periodic reconnect path.
    pub fn initial_dial(&mut self, peer_addresses: &HashMap<PeerId, Multiaddr>) -> AppResult<()> {
        if self.initial_dial_done {
            return Ok(());
        }
        self.initial_dial_done = true;
        self.dial_seed()?;
        self.dial_known_peers(peer_addresses)?;
        Ok(())
    }

    /// Ask the swarm to resolve a peer through the Kademlia DHT and dial it.
    pub fn find_peer(&mut self, peer_id: PeerId) -> AppResult<()> {
        match &self.swarm_status {
            SwarmStatus::Uninitialized => {}
            SwarmStatus::Ready { sender } => {
                sender.try_send(SwarmCommand::FindPeer { peer_id })?;
            }
        }
        Ok(())
    }

    pub fn send_message(
        &self,
        timestamp: Tick,
        from_peer_id: PeerId,
        author: String,
        message: String,
    ) -> AppResult<()> {
        self._send(&NetworkData::Message {
            timestamp,
            from_peer_id,
            author,
            message,
        })
    }

    pub fn send_sync_request(&mut self) -> AppResult<()> {
        self._send(&NetworkData::SyncRequest)
    }

    #[cfg(feature = "relayer")]
    pub fn send_relayer_message_to_team(
        &self,
        message: String,
        team_id: Option<TeamId>,
    ) -> AppResult<()> {
        self._send(&NetworkData::RelayerMessageToTeam {
            timestamp: Tick::now(),
            message,
            team_id,
        })
    }

    pub fn send_seed_info(&mut self, network_store_data: NetworkStoreData) -> AppResult<()> {
        let seed_info = SeedInfo::new(network_store_data)?;

        self._send(&NetworkData::SeedInfo {
            timestamp: Tick::now(),
            seed_info,
        })
    }

    pub fn send_own_team(&self, world: &World) -> AppResult<()> {
        if world.has_own_team() {
            self.send_team(world, world.own_team_id)?
        } else {
            return Err(anyhow!("No own team"));
        };

        // If own team is playing with network peer, send the game.
        if let Some(game_id) = world.get_own_team()?.current_game {
            let game = world.games.get_or_err(&game_id)?;
            // FIXME: Send game even if we are playing with local team.
            if game.is_network() {
                return self.send_game(world, &game_id);
            }
        }

        Ok(())
    }

    pub fn resend_tournaments(&self, world: &World) -> AppResult<()> {
        for tournament in world.tournaments.values() {
            // Only organizer has most updated state of the tournament.
            if tournament.organizer_id == world.own_team_id || tournament.is_initialized() {
                self.send_tournament(tournament.clone())?;
            }
        }

        Ok(())
    }

    pub fn resend_open_trades(&self, world: &World) -> AppResult<Vec<(PlayerId, PlayerId)>> {
        let own_team = world.get_own_team()?;
        let mut to_remove = vec![];
        for trade in own_team.sent_trades.values() {
            let id = (trade.proposer_player.id, trade.target_player.id);
            if trade.state != NetworkRequestState::Syn {
                to_remove.push(id);
                continue;
            }

            let team_id = if let Some(id) = trade.target_player.team {
                id
            } else {
                to_remove.push(id);
                continue;
            };

            let target_team = if let Some(t) = world.teams.get(&team_id) {
                t
            } else {
                to_remove.push(id);
                continue;
            };

            if own_team
                .can_trade_players_with_team(
                    &trade.proposer_player,
                    &trade.target_player,
                    target_team,
                )
                .is_err()
            {
                to_remove.push(id);
                continue;
            }
            self.send_trade(trade.clone())?;
        }

        Ok(to_remove)
    }

    pub fn resend_open_challenges(&self, world: &World) -> AppResult<Vec<TeamId>> {
        let own_team = world.get_own_team()?;
        let mut to_remove = vec![];
        for challenge in own_team.sent_challenges.values() {
            let team_id = challenge.away_team_in_game.team_id;
            let team = if let Some(t) = world.teams.get(&team_id) {
                t
            } else {
                to_remove.push(team_id);
                continue;
            };

            if own_team.can_challenge_network_team(team).is_err() {
                to_remove.push(team_id);
                continue;
            }

            if challenge.state == NetworkRequestState::Syn {
                self.send_challenge(challenge.clone())?;
            }
        }

        Ok(to_remove)
    }

    fn send_game(&self, world: &World, game_id: &GameId) -> AppResult<()> {
        let game = NetworkGame::from_game_id(world, game_id)?;
        self._send(&NetworkData::Game {
            timestamp: Tick::now(),
            game,
        })
    }

    fn send_team(&self, world: &World, team_id: TeamId) -> AppResult<()> {
        let team = NetworkTeam::from_team_id(world, &team_id, *self.own_peer_id())?;
        self._send(&NetworkData::Team {
            timestamp: Tick::now(),
            team,
        })
    }

    pub fn send_tournament(&self, tournament: Tournament) -> AppResult<()> {
        self._send(&NetworkData::Tournament {
            timestamp: Tick::now(),
            tournament,
        })
    }

    pub fn send_tournament_registration_request(
        &self,
        tournament_id: TournamentId,
        team_id: TeamId,
        team_data: Option<(Team, PlayerMap)>,
        request_state: TournamentRequestState,
    ) -> AppResult<()> {
        self._send(&NetworkData::TournamentRegistrationRequest {
            timestamp: Tick::now(),
            tournament_id,
            team_id,
            team_data,
            request_state,
        })
    }

    pub fn send_challenge(&self, challenge: Challenge) -> AppResult<()> {
        self._send(&NetworkData::Challenge {
            timestamp: Tick::now(),
            challenge,
        })
    }

    pub fn send_trade(&self, trade: Trade) -> AppResult<()> {
        self._send(&NetworkData::Trade {
            timestamp: Tick::now(),
            trade,
        })
    }

    pub fn send_new_challenge(
        &self,
        world: &World,
        peer_id: PeerId,
        team_id: TeamId,
        app_version: [usize; 3],
    ) -> AppResult<Challenge> {
        self.send_own_team(world)?;
        let mut home_team_in_game =
            TeamInGame::from_team_id(&world.own_team_id, &world.teams, &world.players)?;
        home_team_in_game.peer_id = Some(*self.own_peer_id());

        let away_team_in_game = TeamInGame::from_team_id(&team_id, &world.teams, &world.players)?;

        let challenge = Challenge::new(
            *self.own_peer_id(),
            peer_id,
            home_team_in_game,
            away_team_in_game,
            app_version,
        );

        self.send_challenge(challenge.clone())?;
        Ok(challenge)
    }

    pub fn send_new_trade(
        &self,
        world: &World,
        target_peer_id: PeerId,
        proposer_player_id: PlayerId,
        target_player_id: PlayerId,
    ) -> AppResult<Trade> {
        self.send_own_team(world)?;

        let proposer_player = world.players.get_or_err(&proposer_player_id)?.clone();
        let target_player = world.players.get_or_err(&target_player_id)?.clone();

        let trade = Trade::new(
            *self.own_peer_id(),
            target_peer_id,
            proposer_player,
            target_player,
            0,
        );

        self.send_trade(trade.clone())?;
        Ok(trade)
    }

    pub fn accept_challenge(&self, world: &World, challenge: Challenge) -> AppResult<()> {
        self.send_own_team(world)?;
        let handle_syn = || -> AppResult<()> {
            let home_team = world
                .teams
                .get_or_err(&challenge.home_team_in_game.team_id)?;
            let away_team = world
                .teams
                .get_or_err(&challenge.away_team_in_game.team_id)?;

            // Away team is our team.
            if away_team.current_game.is_some() {
                return Err(anyhow!("{} is already playing", away_team.name));
            }

            away_team.can_accept_network_challenge(home_team)?;

            let mut away_team_in_game =
                TeamInGame::from_team_id(&world.own_team_id, &world.teams, &world.players)?;

            away_team_in_game.peer_id = Some(*self.own_peer_id());

            // Note: we do not start immediately the game at this point,
            // because it could take a long time to accept a challenge
            // and the status of the challenger could have changed considerably
            // possibly making the challenge invalid.
            let mut challenge = challenge.clone();
            challenge.away_team_in_game = away_team_in_game;
            challenge.state = NetworkRequestState::SynAck;
            self.send_challenge(challenge)?;
            Ok(())
        };

        if let Err(err) = handle_syn() {
            let mut challenge = challenge.clone();
            challenge.state = NetworkRequestState::Failed {
                error_message: err.to_string(),
            };
            self.send_challenge(challenge)?;
            return Err(anyhow!(err.to_string()));
        }
        Ok(())
    }

    pub fn decline_challenge(&self, mut challenge: Challenge) -> AppResult<()> {
        challenge.state = NetworkRequestState::Failed {
            error_message: format!("{} declined", challenge.away_team_in_game.name),
        };
        self.send_challenge(challenge)?;
        Ok(())
    }

    pub fn accept_trade(&self, world: &World, trade: Trade) -> AppResult<()> {
        let handle_syn = || -> AppResult<()> {
            let own_team = world.get_own_team()?;
            let proposer_team = if let Some(proposer_team_id) = trade.proposer_player.team {
                world.teams.get_or_err(&proposer_team_id)?
            } else {
                return Err(anyhow!("Trade target player has no team"));
            };

            // Note: we do not apply immediately the trade at this point,
            // because it could take a long time to accept a trade
            // and the status of the proposer could have changed considerably
            // possibly making the trade invalid.
            let mut trade = trade.clone();
            let target_player = world.players.get_or_err(&trade.target_player.id)?.clone();
            trade.target_player = target_player;
            proposer_team.can_trade_players_with_team(
                &trade.proposer_player,
                &trade.target_player,
                own_team,
            )?;

            trade.state = NetworkRequestState::SynAck;
            self.send_trade(trade)?;
            Ok(())
        };

        if let Err(err) = handle_syn() {
            let mut trade = trade.clone();
            trade.state = NetworkRequestState::Failed {
                error_message: err.to_string(),
            };
            self.send_trade(trade)?;
            return Err(anyhow!(err.to_string()));
        }
        Ok(())
    }

    pub fn decline_trade(&self, trade: Trade) -> AppResult<()> {
        let mut trade = trade.clone();
        trade.state = NetworkRequestState::Failed {
            error_message: "Trade declined".to_string(),
        };
        self.send_trade(trade)?;
        Ok(())
    }

    pub fn handle_network_events(
        &mut self,
        event: SwarmEvent<BehaviourEvent>,
    ) -> Option<NetworkCallback> {
        match event {
            SwarmEvent::NewListenAddr { address, .. } => {
                Some(NetworkCallback::BindAddress { address })
            }
            SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Message {
                propagation_source: _,
                message_id: _,
                message,
            })) => {
                assert!(message.topic == IdentTopic::new(TOPIC).hash());
                Some(NetworkCallback::HandleMessage { message })
            }
            SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Subscribed {
                peer_id,
                topic,
            })) => {
                assert!(topic == IdentTopic::new(TOPIC).hash());
                Some(NetworkCallback::Subscribe { peer_id, topic })
            }
            SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Unsubscribed {
                peer_id,
                topic,
            })) => {
                assert!(topic == IdentTopic::new(TOPIC).hash());
                Some(NetworkCallback::Unsubscribe { peer_id, topic })
            }
            SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
                peer_id,
                info,
                ..
            })) => {
                let listen_addrs = sanitize_listen_addrs(&info.listen_addrs);
                Some(NetworkCallback::PeerIdentified {
                    peer_id,
                    listen_addrs,
                })
            }
            SwarmEvent::Behaviour(BehaviourEvent::Identify(_)) => None,
            SwarmEvent::Behaviour(BehaviourEvent::Kademlia(_)) => None,
            SwarmEvent::NewExternalAddrOfPeer { peer_id, address } => {
                sanitize_addr(&address).map(|clean| NetworkCallback::PeerIdentified {
                    peer_id,
                    listen_addrs: vec![clean],
                })
            }
            SwarmEvent::ExpiredListenAddr {
                listener_id: _,
                address,
            } => Some(NetworkCallback::PushSwarmPanelLog {
                timestamp: Tick::now(),
                peer_id: None,
                text: format!("Expired listen address: {address}"),
                level: log::Level::Warn,
            }),
            SwarmEvent::ConnectionEstablished {
                peer_id, endpoint, ..
            } => {
                self.connected_peers_count += 1;
                Some(NetworkCallback::HandleConnectionEstablished { peer_id, endpoint })
            }
            SwarmEvent::ConnectionClosed { peer_id, .. } => {
                self.connected_peers_count -= 1;
                Some(NetworkCallback::CloseConnection { peer_id })
            }
            SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
                let peer_info = peer_id.map_or("unknown".to_string(), |p| p.to_string());
                let text = match error {
                    DialError::Transport(errors) => {
                        let error_text = errors
                            .iter()
                            .map(|(addr, error)| match error {
                                TransportError::Other(err) => format!("{addr} -> {err}"),
                                _ => format!("{addr} -> {error}"),
                            })
                            .join(", ");
                        format!("Dial failed for peer {peer_info}: {error_text}")
                    }
                    _ => format!("Dial failed for peer {peer_info}: {error}"),
                };
                log::trace!("{}", text);
                Some(NetworkCallback::PushSwarmPanelLog {
                    timestamp: Tick::now(),
                    peer_id: None,
                    text,
                    level: log::Level::Trace,
                })
            }
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::TOPIC;
    use crate::{
        app::App,
        core::{constants::NETWORK_GAME_START_DELAY, types::TeamLocation, world::World},
        network::{
            network_callback::NetworkCallback,
            types::{NetworkData, NetworkRequestState, NetworkTeam},
        },
        store::{deserialize, serialize},
        types::{AppResult, HashMapWithResult, SystemTimeTick, Tick},
        ui::UiCallback,
    };
    use anyhow::anyhow;
    use libp2p::{
        gossipsub::{IdentTopic, Message},
        PeerId,
    };
    use rand::SeedableRng;
    use rand_chacha::ChaCha8Rng;

    #[test]
    fn test_network_challenge_success() -> AppResult<()> {
        let topic = IdentTopic::new(TOPIC);

        let mut app1 = App::test_with_network_handler()?;
        let mut app2 = App::test_with_network_handler()?;

        let proposer_peer_id = app1.network_handler.own_peer_id().clone();
        let target_peer_id = app2.network_handler.own_peer_id().clone();

        // Add other team by hand
        let mut own_team1 = app1.world.get_own_team()?.clone();
        own_team1.peer_id = Some(proposer_peer_id);
        let planet_id = own_team1.home_planet_id;
        for player_id in own_team1.player_ids.iter() {
            let player = app1.world.players.get(&player_id).unwrap();
            app2.world.players.insert(player_id.clone(), player.clone());
        }
        app2.world.teams.insert(own_team1.id, own_team1);

        let mut own_team2 = app2.world.get_own_team()?.clone();
        own_team2.peer_id = Some(target_peer_id);
        // Override current location to ensure challenge is possible
        own_team2.current_location = TeamLocation::OnPlanet { planet_id };

        for player_id in own_team2.player_ids.iter() {
            let player = app2.world.players.get(&player_id).unwrap();
            app1.world.players.insert(player_id.clone(), player.clone());
        }
        app1.world.teams.insert(own_team2.id, own_team2.clone());
        app2.world.teams.insert(own_team2.id, own_team2);

        let cb = UiCallback::ChallengeTeam {
            team_id: app2.world.own_team_id,
        };

        if let Err(e) = cb.call(&mut app1) {
            return Err(e);
        }

        let own_team1 = app1.world.get_own_team()?;
        assert!(own_team1
            .sent_challenges
            .get(&app2.world.own_team_id)
            .is_some());
        assert!(own_team1.current_game.is_none());

        let syn_challenge = own_team1
            .sent_challenges
            .get(&app2.world.own_team_id)
            .unwrap()
            .clone();

        // Mock up send_challenge
        let network_data = NetworkData::Challenge {
            timestamp: Tick::now(),
            challenge: syn_challenge,
        };
        let data = serialize::<NetworkData>(&network_data)?;

        let message = Message {
            source: None,
            data,
            sequence_number: None,
            topic: topic.clone().into(),
        };
        let cb = NetworkCallback::HandleMessage { message };
        assert!(cb.call(&mut app2).is_ok());

        let own_team2 = app2.world.get_own_team()?.clone();
        assert!(own_team2.current_game.is_none());
        let received_challenge = own_team2.received_challenges.get(&app1.world.own_team_id);
        assert!(received_challenge.is_some());

        let cb = UiCallback::AcceptChallenge {
            challenge: received_challenge.unwrap().clone(),
        };

        // Still no game
        let own_team2 = app2.world.get_own_team()?.clone();
        assert!(own_team2.current_game.is_none());

        if let Err(e) = cb.call(&mut app2) {
            return Err(e);
        }

        // Get response challenges
        let mut syn_ack_challenge = received_challenge.unwrap().clone();
        syn_ack_challenge.state = NetworkRequestState::SynAck;
        let mut ack_challenge = received_challenge.unwrap().clone();
        let starting_at = Tick::now() + NETWORK_GAME_START_DELAY;
        ack_challenge.starting_at = Some(starting_at);
        ack_challenge.state = NetworkRequestState::Ack;

        let network_data = NetworkData::Challenge {
            timestamp: Tick::now(),
            challenge: syn_ack_challenge,
        };
        let data = serialize::<NetworkData>(&network_data)?;

        let message = Message {
            source: None,
            data,
            sequence_number: None,
            topic: topic.clone().into(),
        };

        // Check that challenge has been removed after accepting
        let own_team2 = app2.world.get_own_team()?.clone();
        let received_challenge = own_team2.received_challenges.get(&app1.world.own_team_id);
        assert!(received_challenge.is_none());

        let cb = NetworkCallback::HandleMessage { message };
        let own_team1 = app1.world.get_own_team()?.clone();
        assert!(own_team1.current_game.is_none());
        assert!(cb.call(&mut app1).is_ok());
        let own_team1 = app1.world.get_own_team()?.clone();
        assert!(own_team1.current_game.is_some());

        let game_id = own_team1.current_game.unwrap();
        let game = app1.world.games.get_or_err(&game_id)?;
        println!("{:?}, starting_at {}", game_id, game.starting_at);

        let network_data = NetworkData::Challenge {
            timestamp: Tick::now(),
            challenge: ack_challenge,
        };
        let data = serialize::<NetworkData>(&network_data)?;

        let message = Message {
            source: None,
            data,
            sequence_number: None,
            topic: topic.clone().into(),
        };

        let cb = NetworkCallback::HandleMessage { message };
        let own_team2 = app2.world.get_own_team()?.clone();
        assert!(own_team2.current_game.is_none());

        if let Err(e) = cb.call(&mut app2) {
            return Err(e);
        }

        let own_team2 = app2.world.get_own_team()?.clone();
        let game_id = own_team2.current_game.unwrap();
        let game = app2.world.games.get_or_err(&game_id)?;
        println!("{:?}, starting_at {}", game_id, game.starting_at);
        assert!(own_team2.current_game == Some(game_id));

        Ok(())
    }

    #[test]
    fn test_send_own_team() -> AppResult<()> {
        let mut world = World::new(None);
        let rng = &mut ChaCha8Rng::from_rng(&mut rand::rng());
        let home_planet = world.planets.keys().next().unwrap().clone();
        let team_name = "Testen".to_string();
        let ship_name = "Tosten".to_string();
        let own_team_id = world.generate_random_team(rng, home_planet, team_name, ship_name);
        let network_team =
            NetworkTeam::from_team_id(&world, &own_team_id.unwrap(), PeerId::random()).unwrap();

        let timestamp = Tick::now();
        let serialized_network_data = serialize(&NetworkData::Team {
            timestamp,
            team: network_team.clone(),
        })?;

        let deserialized_network_data = deserialize::<NetworkData>(&serialized_network_data)?;

        match deserialized_network_data {
            NetworkData::Team {
                timestamp: deserialized_timestamp,
                team: deserialized_team,
            } => {
                assert!(deserialized_timestamp == timestamp);

                assert_eq!(deserialized_team.team, network_team.team);
                assert_eq!(deserialized_team.players.len(), network_team.players.len());

                let player_id = &network_team.team.player_ids[0];
                let network_player = network_team.players.get(player_id).unwrap().clone();
                let deserialized_player = deserialized_team.players.get(player_id).unwrap().clone();
                assert_eq!(deserialized_player.id, network_player.id);
                assert_eq!(deserialized_player.mental, network_player.mental);
            }
            _ => return Err(anyhow!("Invalid NetworkData deserialization")),
        }

        Ok(())
    }
}
