use std::ops;

use candid::{CandidType, Deserialize, Nat, Principal};
use ex3_crypto::sha256;
use ex3_serde::bincode;
use ex3_timestamp::TimeInNs;
use num_bigint::BigUint;
use serde::Serialize;
use serde_bytes::ByteBuf;

use ex3_canister_error::OtherError;
use ex3_canister_types::{BlockHash, BlockHeight, SerdeBlockHeight};

pub mod error;

pub type TransactionHash = [u8; 32];
pub type PackageId = Nat;
pub type SerdePackageId = BigUint;
type MerkleRoot = [u8; 32];
type BlockchainArchiveCanister = Principal;

pub mod block_version {
    pub const V1: u8 = 1;
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum BlockHeaderResponse {
    Ok(Option<CandidBlockHeader>),
    Forward(BlockchainArchiveCanister),
}

/// Block header
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct BlockHeader {
    pub version: u8,
    pub timestamp: TimeInNs,
    pub height: SerdeBlockHeight,
    pub p_id: SerdePackageId,
    pub pre_block_hash: BlockHash,
    pub tx_root: MerkleRoot,
    pub balance_changed_root: MerkleRoot,
}

#[derive(CandidType, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct CandidBlockHeader {
    pub version: u8,
    pub timestamp: TimeInNs,
    pub height: BlockHeight,
    pub pre_block_hash: BlockHash,
    pub tx_root: MerkleRoot,
    pub balance_changed_root: MerkleRoot,
    pub p_id: PackageId,
}

impl From<CandidBlockHeader> for BlockHeader {
    fn from(candid_block_header: CandidBlockHeader) -> Self {
        BlockHeader {
            version: candid_block_header.version,
            timestamp: candid_block_header.timestamp,
            height: candid_block_header.height.0,
            p_id: candid_block_header.p_id.0,
            pre_block_hash: candid_block_header.pre_block_hash,
            tx_root: candid_block_header.tx_root,
            balance_changed_root: candid_block_header.balance_changed_root,
        }
    }
}

impl From<BlockHeader> for CandidBlockHeader {
    fn from(block_header: BlockHeader) -> Self {
        CandidBlockHeader {
            version: block_header.version,
            timestamp: block_header.timestamp,
            height: block_header.height.into(),
            p_id: block_header.p_id.into(),
            pre_block_hash: block_header.pre_block_hash,
            tx_root: block_header.tx_root,
            balance_changed_root: block_header.balance_changed_root,
        }
    }
}

impl BlockHeader {
    pub fn hash(&self) -> BlockHash {
        let block_header_bytes = bincode::serialize(&self).unwrap();
        sha256!(block_header_bytes.as_slice())
    }
}

impl TryFrom<&[u8]> for BlockHeader {
    type Error = OtherError;

    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
        let (version, timestamp, height, p_id, pre_block_hash, tx_root, balance_changed_root) =
            bincode::deserialize(bytes).map_err(|e| OtherError::new(format!("{:?}", e)))?;
        Ok(Self {
            version,
            timestamp,
            height,
            p_id,
            pre_block_hash,
            tx_root,
            balance_changed_root,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncodedBlockHeader(pub ByteBuf);

impl From<BlockHeader> for EncodedBlockHeader {
    fn from(header: BlockHeader) -> Self {
        let bytes = bincode::serialize(&header).unwrap();
        EncodedBlockHeader(ByteBuf::from(bytes))
    }
}

impl From<EncodedBlockHeader> for BlockHeader {
    fn from(encoded_header: EncodedBlockHeader) -> Self {
        encoded_header.0.as_slice().try_into().unwrap()
    }
}

impl ops::Deref for EncodedBlockHeader {
    type Target = ByteBuf;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(CandidType, Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
pub struct L2Signature {
    #[serde(with = "serde_bytes")]
    pub signature: Vec<u8>,
    #[serde(with = "serde_bytes")]
    pub public_key: Vec<u8>,
}

#[derive(CandidType, Deserialize, Debug)]
pub struct BlockHeaderWithHeight {
    pub height: BlockHeight,
    pub header: CandidBlockHeader,
}
