//! Network behaviour combining all libp2p protocols.

use crate::config::NetworkConfig;
use crate::protocol::{SynorCodec, SynorRequest, SynorResponse, SYNOR_PROTOCOL};
use crate::topics;
use libp2p::{
    gossipsub::{self, IdentTopic, MessageAuthenticity, MessageId, ValidationMode},
    identify,
    kad::{self, store::MemoryStore},
    mdns, ping,
    request_response::{self, ProtocolSupport},
    swarm::NetworkBehaviour,
    PeerId, StreamProtocol,
};
use sha2::{Digest, Sha256};
use std::time::Duration;

/// Combined network behaviour for Synor.
#[derive(NetworkBehaviour)]
pub struct SynorBehaviour {
    /// GossipSub for pub/sub messaging.
    pub gossipsub: gossipsub::Behaviour,
    /// Kademlia DHT for peer discovery.
    pub kademlia: kad::Behaviour<MemoryStore>,
    /// Request-response for direct requests.
    pub request_response: request_response::Behaviour<SynorCodec>,
    /// Identify for peer identification.
    pub identify: identify::Behaviour,
    /// Ping for connection health.
    pub ping: ping::Behaviour,
    /// mDNS for local peer discovery.
    pub mdns: mdns::tokio::Behaviour,
}

impl SynorBehaviour {
    /// Creates a new behaviour with the given configuration.
    pub fn new(
        keypair: &libp2p::identity::Keypair,
        config: &NetworkConfig,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let local_peer_id = keypair.public().to_peer_id();

        // GossipSub configuration
        let gossipsub_config = gossipsub::ConfigBuilder::default()
            .heartbeat_interval(config.gossipsub.heartbeat_interval)
            .validation_mode(ValidationMode::Strict)
            .mesh_n(config.gossipsub.mesh_n)
            .mesh_n_low(config.gossipsub.mesh_n_low)
            .mesh_n_high(config.gossipsub.mesh_n_high)
            .gossip_lazy(config.gossipsub.gossip_lazy)
            .fanout_ttl(config.gossipsub.fanout_ttl)
            .history_length(config.gossipsub.history_length)
            .history_gossip(config.gossipsub.history_gossip)
            .duplicate_cache_time(config.gossipsub.duplicate_cache_time)
            .message_id_fn(message_id_fn)
            .build()
            .map_err(|e| format!("GossipSub config error: {}", e))?;

        let gossipsub = gossipsub::Behaviour::new(
            MessageAuthenticity::Signed(keypair.clone()),
            gossipsub_config,
        )
        .map_err(|e| format!("GossipSub error: {}", e))?;

        // Kademlia configuration
        let kad_config = kad::Config::new(StreamProtocol::new("/synor/kad/1.0.0"));

        let store = MemoryStore::new(local_peer_id);
        let kademlia = kad::Behaviour::with_config(local_peer_id, store, kad_config);

        // Request-response configuration
        let request_response = request_response::Behaviour::new(
            [(StreamProtocol::new(SYNOR_PROTOCOL), ProtocolSupport::Full)],
            request_response::Config::default().with_request_timeout(config.sync.request_timeout),
        );

        // Identify configuration
        let identify_config =
            identify::Config::new("/synor/id/1.0.0".to_string(), keypair.public())
                .with_agent_version(crate::user_agent())
                .with_push_listen_addr_updates(true);

        let identify = identify::Behaviour::new(identify_config);

        // Ping configuration
        let ping = ping::Behaviour::new(
            ping::Config::new()
                .with_interval(config.ping_interval)
                .with_timeout(Duration::from_secs(30)),
        );

        // mDNS configuration
        let mdns = mdns::tokio::Behaviour::new(mdns::Config::default(), local_peer_id)?;

        Ok(SynorBehaviour {
            gossipsub,
            kademlia,
            request_response,
            identify,
            ping,
            mdns,
        })
    }

    /// Subscribes to all Synor topics.
    pub fn subscribe_topics(&mut self) -> Result<(), gossipsub::SubscriptionError> {
        let topics = [
            IdentTopic::new(topics::BLOCKS),
            IdentTopic::new(topics::TRANSACTIONS),
            IdentTopic::new(topics::HEADERS),
        ];

        for topic in topics {
            self.gossipsub.subscribe(&topic)?;
        }

        Ok(())
    }

    /// Publishes a message to a topic.
    pub fn publish(
        &mut self,
        topic: &str,
        data: Vec<u8>,
    ) -> Result<gossipsub::MessageId, gossipsub::PublishError> {
        let topic = IdentTopic::new(topic);
        self.gossipsub.publish(topic, data)
    }

    /// Publishes a block announcement.
    pub fn publish_block(
        &mut self,
        data: Vec<u8>,
    ) -> Result<gossipsub::MessageId, gossipsub::PublishError> {
        self.publish(topics::BLOCKS, data)
    }

    /// Publishes a transaction announcement.
    pub fn publish_transaction(
        &mut self,
        data: Vec<u8>,
    ) -> Result<gossipsub::MessageId, gossipsub::PublishError> {
        self.publish(topics::TRANSACTIONS, data)
    }

    /// Publishes a header announcement.
    pub fn publish_header(
        &mut self,
        data: Vec<u8>,
    ) -> Result<gossipsub::MessageId, gossipsub::PublishError> {
        self.publish(topics::HEADERS, data)
    }

    /// Sends a request to a peer.
    pub fn send_request(
        &mut self,
        peer: &PeerId,
        request: SynorRequest,
    ) -> request_response::OutboundRequestId {
        self.request_response.send_request(peer, request)
    }

    /// Sends a response to a request.
    #[allow(clippy::result_large_err)]
    pub fn send_response(
        &mut self,
        channel: request_response::ResponseChannel<SynorResponse>,
        response: SynorResponse,
    ) -> Result<(), SynorResponse> {
        self.request_response.send_response(channel, response)
    }

    /// Adds an address to Kademlia.
    pub fn add_address(&mut self, peer: &PeerId, address: libp2p::Multiaddr) {
        self.kademlia.add_address(peer, address);
    }

    /// Starts a Kademlia bootstrap.
    pub fn bootstrap(&mut self) -> Result<kad::QueryId, kad::NoKnownPeers> {
        self.kademlia.bootstrap()
    }

    /// Finds the closest peers to a key.
    pub fn get_closest_peers(&mut self, key: Vec<u8>) -> kad::QueryId {
        self.kademlia.get_closest_peers(key)
    }

    /// Returns the number of peers in the gossipsub mesh.
    pub fn mesh_peers_count(&self) -> usize {
        self.gossipsub.all_mesh_peers().count()
    }

    /// Returns all peers in the gossipsub mesh.
    pub fn mesh_peers(&self) -> Vec<PeerId> {
        self.gossipsub.all_mesh_peers().cloned().collect()
    }

    /// Returns peers subscribed to a topic.
    pub fn topic_peers(&self, topic: &str) -> Vec<PeerId> {
        let topic = IdentTopic::new(topic);
        self.gossipsub.mesh_peers(&topic.hash()).cloned().collect()
    }
}

/// Generates a message ID for GossipSub messages.
fn message_id_fn(message: &gossipsub::Message) -> MessageId {
    let mut hasher = Sha256::new();
    hasher.update(&message.data);
    hasher.update(message.source.map(|p| p.to_bytes()).unwrap_or_default());
    let hash = hasher.finalize();
    MessageId::from(hash.to_vec())
}

/// Events emitted by the behaviour.
#[derive(Debug)]
pub enum BehaviourEvent {
    /// GossipSub message received.
    GossipMessage {
        peer_id: PeerId,
        message_id: MessageId,
        topic: String,
        data: Vec<u8>,
    },
    /// New peer discovered.
    PeerDiscovered(PeerId),
    /// Peer connected.
    PeerConnected(PeerId),
    /// Peer disconnected.
    PeerDisconnected(PeerId),
    /// Request received.
    RequestReceived {
        peer_id: PeerId,
        request_id: request_response::InboundRequestId,
        request: SynorRequest,
        channel: request_response::ResponseChannel<SynorResponse>,
    },
    /// Response received.
    ResponseReceived {
        peer_id: PeerId,
        request_id: request_response::OutboundRequestId,
        response: SynorResponse,
    },
    /// Request failed.
    RequestFailed {
        peer_id: PeerId,
        request_id: request_response::OutboundRequestId,
        error: String,
    },
    /// Ping result.
    PingResult {
        peer_id: PeerId,
        rtt: Option<Duration>,
    },
    /// Identify result.
    IdentifyReceived {
        peer_id: PeerId,
        info: identify::Info,
    },
    /// Kademlia event.
    KademliaEvent(kad::Event),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_message_id() {
        let msg = gossipsub::Message {
            source: Some(PeerId::random()),
            data: b"test message".to_vec(),
            sequence_number: Some(1),
            topic: IdentTopic::new("test").hash(),
        };

        let id1 = message_id_fn(&msg);
        let id2 = message_id_fn(&msg);

        assert_eq!(id1, id2);
    }
}
