use crate::asset::{CandidCryptoAsset, CryptoAsset};
use crate::{AssetAmount, AssetId, CandidAssetAmount, CandidAssetId, CandidDepositId, DepositId};
use candid::CandidType;
use ex3_crypto::sha256;
use ex3_serde::bincode;
use ex3_timestamp::TimeInNs;
use ic_stable_structures::{storable::Bound, Storable};
use serde::{Deserialize, Serialize};
use std::fmt::Display;

/// DepositIdentifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
pub struct DepositIdentifier {
    /// The asset id
    pub asset_id: AssetId,

    /// The block ref
    /// Not every blockchain only have block height, such as Ton chain
    /// ton : `workchain:shared:blockheight`
    pub block_ref: Option<String>,

    /// The blockchain transaction id
    /// For UTXO, it is the transaction id + tx index
    pub tx_ref: Option<String>,
}

impl Display for DepositIdentifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{}:{}",
            self.asset_id,
            self.block_ref.clone().unwrap_or("".to_string()),
            self.tx_ref.clone().unwrap_or("".to_string())
        )
    }
}

/// CandidDepositIdentifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, CandidType, Ord, PartialOrd)]
pub struct CandidDepositIdentifier {
    /// The asset id
    pub asset_id: CandidAssetId,

    /// The block ref
    /// Not every blockchain only have block height, such as Ton chain
    /// ton : `workchain:shared:blockheight`
    pub block_ref: Option<String>,

    /// The blockchain transaction id
    /// For UTXO, it is the transaction id + tx index
    pub tx_ref: Option<String>,
}

impl From<DepositIdentifier> for CandidDepositIdentifier {
    fn from(deposit_identifier: DepositIdentifier) -> Self {
        Self {
            asset_id: deposit_identifier.asset_id.into(),
            block_ref: deposit_identifier.block_ref,
            tx_ref: deposit_identifier.tx_ref,
        }
    }
}

impl From<CandidDepositIdentifier> for DepositIdentifier {
    fn from(deposit_identifier: CandidDepositIdentifier) -> Self {
        Self {
            asset_id: deposit_identifier.asset_id.into(),
            block_ref: deposit_identifier.block_ref,
            tx_ref: deposit_identifier.tx_ref,
        }
    }
}

/// Original Deposit
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash, Ord, PartialOrd)]
pub struct OriginalDeposit {
    /// From wallet address
    pub from: String,

    /// Specify asset
    pub asset: CryptoAsset,

    /// The amount
    pub amount: AssetAmount,

    /// The block ref
    pub block_ref: Option<String>,

    /// The blockchain transaction id
    /// For UTXO, it is the transaction id + tx index
    pub tx_ref: Option<String>,
}

impl OriginalDeposit {
    pub fn hash(&self) -> [u8; 32] {
        sha256(&bincode::serialize(&self).unwrap())
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize, CandidType, Hash, Ord, PartialOrd)]
pub struct OriginalCandidDeposit {
    /// From wallet address
    pub from: String,

    /// Specify asset
    pub asset: CandidCryptoAsset,

    /// The amount
    pub amount: CandidAssetAmount,

    /// The block ref
    pub block_ref: Option<String>,

    /// The blockchain transaction id
    /// For UTXO, it is the transaction id + tx index
    pub tx_ref: Option<String>,
}

impl Storable for OriginalDeposit {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        bincode::serialize(&self).unwrap().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        bincode::deserialize(bytes.as_ref()).unwrap()
    }

    const BOUND: Bound = Bound::Bounded {
        max_size: 888,
        is_fixed_size: false,
    };
}

impl From<OriginalDeposit> for OriginalCandidDeposit {
    fn from(deposit: OriginalDeposit) -> Self {
        Self {
            asset: deposit.asset.into(),
            block_ref: deposit.block_ref,
            tx_ref: deposit.tx_ref,
            from: deposit.from,
            amount: deposit.amount.into(),
        }
    }
}

impl From<OriginalCandidDeposit> for OriginalDeposit {
    fn from(deposit: OriginalCandidDeposit) -> Self {
        Self {
            asset: deposit.asset.into(),
            block_ref: deposit.block_ref,
            tx_ref: deposit.tx_ref,
            from: deposit.from,
            amount: deposit.amount.into(),
        }
    }
}

/// VerifiedDeposit
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct VerifiedDeposit {
    pub seq_id: DepositId,
    pub deposit: OriginalDeposit,
    pub verified_at: TimeInNs,
}

/// CandidVerifiedDeposit
#[derive(Deserialize, CandidType, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct CandidVerifiedDeposit {
    pub seq_id: CandidDepositId,
    pub deposit: OriginalCandidDeposit,
    pub confirmed_at: TimeInNs,
}

impl Storable for DepositIdentifier {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        bincode::serialize(&self).unwrap().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        bincode::deserialize(bytes.as_ref()).unwrap()
    }

    const BOUND: Bound = Bound::Bounded {
        max_size: 512,
        is_fixed_size: false,
    };
}

impl Storable for VerifiedDeposit {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        bincode::serialize(&self).unwrap().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        bincode::deserialize(bytes.as_ref()).unwrap()
    }

    const BOUND: Bound = Bound::Bounded {
        max_size: 304,
        is_fixed_size: false,
    };
}

impl From<VerifiedDeposit> for CandidVerifiedDeposit {
    fn from(deposit: VerifiedDeposit) -> Self {
        Self {
            seq_id: deposit.seq_id.into(),
            deposit: deposit.deposit.into(),
            confirmed_at: deposit.verified_at,
        }
    }
}

impl From<CandidVerifiedDeposit> for VerifiedDeposit {
    fn from(deposit: CandidVerifiedDeposit) -> Self {
        Self {
            seq_id: deposit.seq_id.into(),
            deposit: deposit.deposit.into(),
            verified_at: deposit.confirmed_at,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::asset::TokenType;
    use crate::chain::{Chain, ChainType};

    #[test]
    fn test_storable_for_deposit_identifier() {
        let tx_id: String = std::iter::repeat('1').take(128).collect();
        let deposit_identifier = DepositIdentifier {
            asset_id: u128::MAX.into(),
            block_ref: Some(u128::MAX.to_string()),
            tx_ref: Some(tx_id),
        };
        let bytes = deposit_identifier.to_bytes();
        assert!(bytes.len() <= 512, "bytes.len() = {}", bytes.len());
        let deposit_identifier2 = DepositIdentifier::from_bytes(bytes);
        assert_eq!(deposit_identifier, deposit_identifier2);
    }

    #[test]
    fn test_storable_for_original_deposit() {
        let original_deposit = OriginalDeposit {
            asset: CryptoAsset {
                chain: Chain {
                    r#type: ChainType::Bitcoin,
                    network: 1u8.into(),
                },
                r#type: TokenType::Native,
                address: "0x1234567890".to_string(),
            },
            block_ref: Some(u128::MAX.to_string()),
            tx_ref: Some(
                "0xf2e0565df1a88e98a0c506287b4d408bf19b59f7b9f6f2a0a54375e28038489d".to_string(),
            ),
            from: "0x1234".to_string(),
            amount: u128::MAX.into(),
        };
        let bytes = original_deposit.to_bytes();
        assert!(bytes.len() <= 888, "bytes.len() = {}", bytes.len());
        let original_deposit2 = OriginalDeposit::from_bytes(bytes);
        assert_eq!(original_deposit, original_deposit2);
    }
}
