use std::{
    collections::{hash_map::Entry, HashMap, HashSet},
    time::Duration,
    error::Error,
};

use futures::StreamExt;
use codec::Decode;
use primitive_types::H256;
use libp2p::{
    identify::Info, identity, kad::{
        Event as KademliaEvent, GetClosestPeersError, GetClosestPeersOk, 
        QueryId, QueryResult,
    }, 
    swarm::{Config as SwarmConfig, SwarmEvent}, 
    Multiaddr, PeerId, Swarm 
};

use crate::{
    locator::Locator,
    p2p::{notifications::behaviour::NotificationsToSwarm, BehaviourEvent},
};

use super::p2p::{
    discovery::DiscoveryBuilder,
    peer_behaviour::{PeerInfoEvent, PeerBehaviour},
    transport::{TransportBuilder, MIB},
    notifications::{
        behaviour::{Notifications, ProtocolsData},
        messages::ProtocolRole,
    },
    Behaviour,
};

pub struct Walker {
    genesis: String,
    timeout: Duration,
    maybe_filename: Option<String>,
    swarm: Swarm<Behaviour>,
    queries: HashSet<QueryId>,
    discovered_with_address: HashMap<PeerId, HashSet<Multiaddr>>,
    peer_details: HashMap<PeerId, Info>,
    peer_role: HashMap<PeerId, ProtocolRole>,
    dialed_peers: HashMap<PeerId, usize>,
}

impl Walker {
    pub fn new() -> WalkerBuilder {
        WalkerBuilder::default()
    }

    fn insert_queries(&mut self, num: usize) {
        for _ in 0..num {
            self.queries.insert(
                self.swarm
                    .behaviour_mut()
                    .discovery
                    .get_closest_peers(PeerId::random()));
        }
    }

    fn dialed_peer(&mut self, peer_id: Option<PeerId>) {
        let Some(peer_id) = peer_id else { return };

        self.dialed_peers
            .entry(peer_id)
            .and_modify(|num| *num += 1)
            .or_insert(0);
    }

    async fn inner_walk(&mut self) {
        self.insert_queries(128);
        let mut previous_tracing_time = std::time::Instant::now();

        loop {
            let event = self.swarm.select_next_some().await;

            match event {
                SwarmEvent::Dialing { peer_id, .. } => self.dialed_peer(peer_id),
                SwarmEvent::Behaviour(BehaviourEvent::Discovery(event)) => match event {
                    KademliaEvent::OutboundQueryProgressed { 
                        id, 
                        result: QueryResult::GetClosestPeers(result),
                        ..
                    } => {
                        self.queries.remove(&id);

                        let peers = match result {
                            Ok(GetClosestPeersOk { peers, ..}) => peers,
                            Err(GetClosestPeersError::Timeout { peers, .. }) => peers
                        };
                        let num_discovered = peers.len();

                        let now = std::time::Instant::now();
                        if now.duration_since(previous_tracing_time) > Duration::from_secs(10) {
                            previous_tracing_time = now;
                            tracing::info!("...Discovery in progress last query #{num_discovered}");
                        }

                        if self.queries.is_empty() {
                            self.insert_queries(128);
                        }
                    }
                    KademliaEvent::RoutingUpdated { peer, addresses, .. } => {
                        match self.discovered_with_address.entry(peer) {
                            Entry::Occupied(mut occupied) => {
                                occupied.get_mut().extend(addresses.into_vec());
                            },
                            Entry::Vacant(vacant) => {
                                vacant.insert(addresses.iter().cloned().collect());
                            },
                        };
                    }
                    KademliaEvent::RoutablePeer { peer, address } | KademliaEvent::PendingRoutablePeer { peer, address } => {
                        match self.discovered_with_address.entry(peer) {
                            Entry::Occupied(mut occupied) => {
                                occupied.get_mut().insert(address);
                            },
                            Entry::Vacant(vacant) => {
                                let mut set = HashSet::new();
                                set.insert(address);
                                vacant.insert(set);
                            },
                        };
                    }
                    _ => (),
                },
                SwarmEvent::Behaviour(BehaviourEvent::PeerInfo(info_event)) => match info_event {
                    PeerInfoEvent::Identified { peer_id, info } => {
                        tracing::debug!("Identified peer_id={:?} info={:?}", peer_id, info);
                        self.peer_details.insert(peer_id, info);
                    }
                },
                SwarmEvent::Behaviour(BehaviourEvent::Notifications(
                        NotificationsToSwarm::CustomProtocolOpen { 
                            peer_id, 
                            index, 
                            received_handshake, 
                            inbound, 
                            .. 
                        }
                )) => {
                    if let Ok(role) = ProtocolRole::decode(&mut &received_handshake[..]) {
                        tracing::debug!("Identified peer_id={:?} role={:?}", peer_id, role);
                        self.peer_role.insert(peer_id, role);
                    }

                    tracing::debug!(
                        "Protocol open peer={:?} index={:?} handshake={:?} inbound={:?}",
                        peer_id,
                        index,
                        received_handshake,
                        inbound,
                    );
                }
                _ => (),
            }
        }
    }

    pub async fn walk_around(&mut self) -> Result<(), Box<dyn Error>> {
        let _ = tokio::time::timeout(self.timeout, self.inner_walk()).await;

        println!("Number of dialed peers    : {}", self.dialed_peers.len());
        println!("Total peers discovered    : {}", self.discovered_with_address.len());
        println!("Peers with local identity : {}", self.peer_details.len());

        let infos: HashMap<_, _> = self
            .peer_details
            .iter()
            .filter(|(_, info)| {
                info.protocols
                    .iter()
                    .any(|stream_proto| stream_proto.as_ref().contains(&self.genesis))
            })
            .collect();
        println!("Support correct genesis   : {}", infos.len());
        println!("Peers with associated role: {}", self.peer_role.len());

        Locator::new(&infos, &self.maybe_filename).locate_peers().await;

        Ok(())
    }
}

pub struct WalkerBuilder {
    genesis: String,
    bootnodes: Vec<String>,
    timeout: Duration,
    maybe_filename: Option<String>,
}

impl Default for WalkerBuilder {
    fn default() -> Self {
        Self {
            genesis: Default::default(), 
            bootnodes: Default::default(),
            timeout: Default::default(),
            maybe_filename: None,
        }
    }
}

impl WalkerBuilder {
    fn create_swarm(&self) -> Result<Swarm<Behaviour>, Box<dyn Error>> {
        let local_key = identity::Keypair::generate_ed25519();
        let local_peer_id = PeerId::from(local_key.public());
        tracing::info!("Local peer Id {:?}", local_peer_id);

        let bootnodes = self.bootnodes
            .iter()
            .map(|bootnode| {
                let parts: Vec<_> = bootnode.split('/').collect();
                let peer = parts.last().expect("Bootnode peer should be valid; qed");
                let multiaddress: Multiaddr = bootnode
                    .parse()
                    .expect("Bootnode multiaddress should be valid; qed");
                let peer_id: PeerId = peer
                    .parse()
                    .expect("Bootnode should have valid peer ID; qed");

                tracing::info!("Bootnode peer={:?}", peer_id);
                (peer_id, multiaddress)
            })
            .collect::<Vec<_>>();

        let protocol_data = ProtocolsData {
            genesis_hash: H256::from_slice(hex::decode(self.genesis.clone())?.as_slice()),
            node_role: ProtocolRole::FullNode,
        };

        let mut swarm: Swarm<Behaviour> = {
            let transport = TransportBuilder::new()
                .yamux_maximum_buffer_size(256 * MIB)
                .build(local_key.clone());

            let discovery = DiscoveryBuilder::new()
                .record_ttl(Some(Duration::from_secs(0)))
                .provider_ttl(Some(Duration::from_secs(0)))
                //.query_timeout(Duration::from_secs(5 * 60))
                .query_timeout(self.timeout)
                .build(local_peer_id, &self.genesis);

            let peer_info = PeerBehaviour::new(local_key.public());
            let notifications = Notifications::new(protocol_data);

            let behavior = Behaviour {
                notifications,
                peer_info,
                discovery,
            };

            let config = SwarmConfig::with_tokio_executor();
            Swarm::new(transport, behavior, local_peer_id, config)
        };

        for (peer, multiaddress) in &bootnodes {
            swarm
                .behaviour_mut()
                .discovery
                .add_address(peer, multiaddress.clone());
        }

        Ok(swarm)
    }

    pub fn with_genesis(mut self, genesis: String) -> Self {
        self.genesis = genesis.trim_start_matches("0x").to_string();
        self
    }

    pub fn with_bootnodes(mut self, bootnodes: Vec<String>) -> Self {
        self.bootnodes = bootnodes;
        self
    }

    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn with_filename(mut self, maybe_filename: Option<String>) -> Self {
        self.maybe_filename = maybe_filename;
        self
    }

    pub fn build(&self) -> Result<Walker, Box<dyn Error>> {
        let swarm = self.create_swarm()?;
        Ok(Walker {
            swarm,
            maybe_filename: self.maybe_filename.clone(),
            genesis: self.genesis.clone(),
            timeout: self.timeout,
            queries: HashSet::with_capacity(1024),
            discovered_with_address: HashMap::with_capacity(1024),
            peer_details: HashMap::with_capacity(1024),
            peer_role: HashMap::with_capacity(1024),
            dialed_peers: HashMap::with_capacity(1024),
        })
    }
}
