use std::io;

use bitcoincash::{
    consensus::{encode, Decodable},
    network::message::CommandString,
    VarInt,
};

use crate::chaindef::{Block, NetworkMessage};
use anyhow::Result;
use bitcoincash::consensus::ReadExt;
use tokio::io::AsyncReadExt;

pub struct P2PNetworkMessage {
    pub magic: u32,
    pub cmd: CommandString,
    pub raw: Vec<u8>,
}

impl P2PNetworkMessage {
    pub fn parse(&self) -> Result<NetworkMessage> {
        let mut raw: &[u8] = &self.raw;
        let payload = match self.cmd.as_ref() {
            "version" => NetworkMessage::Version(Decodable::consensus_decode(&mut raw)?),
            "verack" => NetworkMessage::Verack,
            "inv" => NetworkMessage::Inv(Decodable::consensus_decode(&mut raw)?),
            "block" => NetworkMessage::Block(Decodable::consensus_decode(&mut raw)?),
            "headers" => {
                let len = VarInt::consensus_decode(&mut raw)?.0;
                let mut headers = Vec::with_capacity(len as usize);
                for _ in 0..len {
                    headers.push(Block::consensus_decode(&mut raw)?.header);
                }
                NetworkMessage::Headers(headers)
            }
            "ping" => NetworkMessage::Ping(Decodable::consensus_decode(&mut raw)?),
            "pong" => NetworkMessage::Pong(Decodable::consensus_decode(&mut raw)?),
            "reject" => NetworkMessage::Reject(Decodable::consensus_decode(&mut raw)?),
            _ => NetworkMessage::Unknown {
                command: self.cmd.clone(),
                payload: raw.to_vec(),
            },
        };
        Ok(payload)
    }

    pub async fn async_consensus_decode<D: tokio::io::AsyncRead + Unpin>(
        mut d: D,
    ) -> Result<Self, encode::Error> {
        //  (4) message start (magic)
        //  (12) command
        //  (4) size
        //  (4) checksum
        //  (x) data

        let magic: u32 = d.read_u32_le().await?;
        let cmd: CommandString = Self::read_and_decode(&mut d, 12).await?;
        let len: u32 = d.read_u32_le().await?;
        let _checksum: u32 = d.read_u32_le().await?;
        let mut raw: Vec<u8> = vec![0u8; len as usize];

        if len > 0 {
            d.read_exact(&mut raw).await.map_err(encode::Error::Io)?;
        }

        Ok(Self { magic, cmd, raw })
    }

    async fn read_and_decode<T: Decodable, D: tokio::io::AsyncRead + Unpin>(
        d: &mut D,
        len: usize,
    ) -> Result<T, encode::Error> {
        let mut buf = vec![0u8; len];
        d.read_exact(&mut buf).await.map_err(encode::Error::Io)?;

        let mut cursor = std::io::Cursor::new(buf);
        T::consensus_decode(&mut cursor)
    }
}

impl Decodable for P2PNetworkMessage {
    fn consensus_decode<D: io::Read + ?Sized>(d: &mut D) -> Result<Self, encode::Error> {
        let magic = Decodable::consensus_decode(d)?;
        let cmd = Decodable::consensus_decode(d)?;

        let len = u32::consensus_decode(d)?;
        let _checksum = <[u8; 4]>::consensus_decode(d)?; // assume data is correct
        let mut raw = vec![0u8; len as usize];
        d.read_slice(&mut raw)?;

        Ok(Self { magic, cmd, raw })
    }
}
