use std::fmt::Display;

use candid::CandidType;
use ex3_ic_stable_structures::{storable::Bound, Storable};
use ex3_serde::bincode;
use ex3_timestamp::TimeInNs;
use serde::{Deserialize, Serialize};

use crate::{
    AssetAmount, AssetId, BlockHeight, CandidAssetAmount, CandidAssetId, CandidBlockHeight,
    CandidDepositId, CandidWalletRegisterId, DepositId, WalletRegisterId,
};

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

    /// The block height
    /// Any chain does not have a block height? we use the timestamp instead? (TBD)
    pub block_height: BlockHeight,

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

impl Display for DepositIdentifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{}:{}",
            self.asset_id,
            self.block_height,
            self.tx_id.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 height
    /// Any chain does not have a block height? we use the timestamp instead? (TBD)
    pub block_height: CandidBlockHeight,

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

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

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

/// Original Deposit
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct OriginalDeposit {
    /// The deposit identifier
    pub identifier: DepositIdentifier,

    /// The amount
    pub amount: AssetAmount,
}

/// Deposit
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct Deposit {
    pub identifier: DepositIdentifier,
    pub from_wallet: WalletRegisterId,
    pub amount: AssetAmount,
}

/// Deposit
#[derive(Deserialize, CandidType, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct CandidDeposit {
    pub identifier: CandidDepositIdentifier,
    pub from_wallet: CandidWalletRegisterId,
    pub amount: CandidAssetAmount,
}

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

/// CandidConfirmedDeposit
#[derive(Deserialize, CandidType, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct CandidConfirmedDeposit {
    pub seq_id: CandidDepositId,
    pub deposit: CandidDeposit,
    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: 200,
        is_fixed_size: false,
    };
}

impl Storable for Deposit {
    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: 264,
        is_fixed_size: false,
    };
}

impl Storable for ConfirmedDeposit {
    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<Deposit> for CandidDeposit {
    fn from(deposit: Deposit) -> Self {
        Self {
            identifier: deposit.identifier.into(),
            from_wallet: deposit.from_wallet.into(),
            amount: deposit.amount.into(),
        }
    }
}

impl From<CandidDeposit> for Deposit {
    fn from(deposit: CandidDeposit) -> Self {
        Self {
            identifier: deposit.identifier.into(),
            from_wallet: deposit.from_wallet.into(),
            amount: deposit.amount.into(),
        }
    }
}

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

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

#[cfg(test)]
mod tests {
    use ex3_serde::bincode;

    use super::*;

    #[test]
    fn test_serde() {
        let deposit_identifier = DepositIdentifier {
            asset_id: 1u64.into(),
            block_height: 2u64.into(),
            tx_id: Some("tx_id".to_string()),
        };

        let deposit = OriginalDeposit {
            identifier: deposit_identifier.clone(),
            amount: 3u64.into(),
        };
        //bincode
        let encoded = bincode::serialize(&deposit).unwrap();
        let decoded: OriginalDeposit = bincode::deserialize(&encoded).unwrap();
        assert_eq!(deposit, decoded);

        // cbor
        let encoded = ex3_serde::cbor::serialize(&deposit).unwrap();
        let decoded: OriginalDeposit = ex3_serde::cbor::deserialize(&encoded).unwrap();
        assert_eq!(deposit, decoded);
    }

    #[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_height: u128::MAX.into(),
            tx_id: Some(tx_id),
        };
        let bytes = deposit_identifier.to_bytes();
        assert!(bytes.len() <= 200, "bytes.len() = {}", bytes.len());
        let deposit_identifier2 = DepositIdentifier::from_bytes(bytes);
        assert_eq!(deposit_identifier, deposit_identifier2);
    }

    #[test]
    fn test_storable_for_deposit() {
        let tx_id: String = std::iter::repeat('1').take(128).collect();
        let deposit = Deposit {
            identifier: DepositIdentifier {
                asset_id: u128::MAX.into(),
                block_height: u128::MAX.into(),
                tx_id: Some(tx_id),
            },
            from_wallet: u128::MAX.into(),
            amount: u128::MAX.into(),
        };
        let bytes = deposit.to_bytes();
        assert!(bytes.len() <= 264, "bytes.len() = {}", bytes.len());
        let deposit2 = Deposit::from_bytes(bytes);
        assert_eq!(deposit, deposit2);
    }
}
