use std::fmt;
use std::ops;

use candid::CandidType;
use ciborium::tag::Captured;
use ex3_crypto::sha256;
use ex3_node_error::OtherError;
use ex3_payload_derive::Ex3Payload;
use ex3_serde::{bincode, cbor};
use ic_stable_structures::storable::Bound;
use ic_stable_structures::Storable;
use num_bigint::BigUint;
use serde::de::{Deserializer, SeqAccess, Visitor};
use serde::ser::{SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

pub use asset::*;
pub use asset_account_binding::*;
pub use deposit::*;
pub use market::*;
pub use order::*;
pub use secret::*;
pub use transfer::*;
pub use types::*;
pub use wallet_registration::*;
pub use withdrawal::*;

use crate::PublicKey;
use crate::{Nonce, TransactionHash, Version, WalletRegisterId};

mod asset;
mod deposit;
mod market;
mod order;
mod secret;
mod transfer;
mod types;
mod withdrawal;

mod asset_account_binding;
mod wallet_registration;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Transaction {
    // Version of the transaction.
    pub version: Version,
    // Type of transaction.
    pub r#type: TransactionType,
    // Wallet register ID of the sender.
    pub from: WalletRegisterId,
    /// Nonce is a number that is used only once for every transaction per wallet.
    /// If the transaction type is Deposit, Wallet Register, or Reset Main Secret, this field is 0.
    /// Otherwise, this field starts from 1.
    pub nonce: Nonce,
    // Payload information of the transaction.
    pub payload: ByteBuf,
    // Signature of the transaction.
    pub signature: ByteBuf,
}

#[derive(Debug, Clone, PartialEq, Eq, Ex3Payload)]
struct TxData {
    #[index(0)]
    version: u8,
    #[index(1)]
    r#type: BigUint,
    #[index(2)]
    from: BigUint,
    #[index(3)]
    nonce: BigUint,
    #[index(4)]
    payload: ByteBuf,
}

impl Transaction {
    pub fn hash(&self) -> TransactionHash {
        let bytes = bincode::serialize(&(
            &self.version,
            &self.r#type,
            &self.from,
            &self.nonce,
            &self.payload,
        ))
        .unwrap();
        sha256(&bytes)
    }

    // Why use this hash function?
    // Because the wallet registration transaction is a special transaction.
    // It is no public key in the transaction payload, but the public key is in the transaction signature.
    // So we need to use the public key in the transaction signature to calculate the hash value.
    pub fn hash_for_wallet_registration(&self, recovery_public_key: PublicKey) -> TransactionHash {
        let bytes = bincode::serialize(&(
            &self.version,
            &self.r#type,
            &self.from,
            &self.nonce,
            &recovery_public_key,
        ))
        .unwrap();
        sha256(&bytes)
    }

    pub fn tx_data_bytes(&self) -> Vec<u8> {
        let tx_data = TxData {
            version: self.version.encode(),
            r#type: self.r#type.clone().into(),
            from: self.from.clone().into(),
            nonce: self.nonce.clone().into(),
            payload: self.payload.clone(),
        };
        let bytes = cbor::serialize(&tx_data).unwrap();
        bytes
    }

    /// Self-encode the transaction into bytes.
    pub fn encode(&self) -> Vec<u8> {
        let data_bytes = bincode::serialize(&(
            &self.r#type,
            &self.from,
            &self.nonce,
            &self.payload,
            &self.signature,
        ))
        .unwrap();

        let mut bytes = vec![self.version.encode()];
        bytes.extend_from_slice(&data_bytes);
        bytes
    }

    /// Decode the transaction from bytes.
    pub fn decode(bytes: &[u8]) -> Result<Self, OtherError> {
        let version = Version::decode(bytes[0..1].try_into().unwrap())
            .map_err(|_| OtherError::new("failed to decode transaction version".to_string()))?;

        if version != Version::V0 {
            return Err(OtherError::new(
                format!(
                    "unsupported transaction version, expected 0, but {}",
                    version.encode()
                )
                .to_string(),
            ));
        }

        let (r#type, from, nonce, payload, signature) = bincode::deserialize(&bytes[1..]).unwrap();

        Ok(Transaction {
            version,
            r#type,
            from,
            nonce,
            payload,
            signature,
        })
    }
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct EncodedTransaction(ByteBuf);

impl From<Transaction> for EncodedTransaction {
    fn from(tx: Transaction) -> Self {
        let bytes = tx.encode();
        EncodedTransaction(ByteBuf::from(bytes))
    }
}

impl From<&Transaction> for EncodedTransaction {
    fn from(tx: &Transaction) -> Self {
        let bytes = tx.encode();
        EncodedTransaction(ByteBuf::from(bytes))
    }
}

impl From<EncodedTransaction> for Transaction {
    fn from(tx: EncodedTransaction) -> Self {
        Transaction::decode(tx.0.as_ref()).expect("failed to decode transaction")
    }
}

impl From<&EncodedTransaction> for Transaction {
    fn from(tx: &EncodedTransaction) -> Self {
        Transaction::decode(tx.0.as_ref()).expect("failed to decode transaction")
    }
}

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

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

impl Storable for EncodedTransaction {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        self.0.as_ref().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        EncodedTransaction(ByteBuf::from(bytes))
    }

    const BOUND: Bound = Bound::Unbounded;
}

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

    #[test]
    fn test_encode_decode_with_empty_signature() {
        let tx = Transaction {
            version: Version::V0,
            r#type: TransactionType::Deposit,
            from: WalletRegisterId::from(1000000u32),
            nonce: Nonce::from(1u32),
            payload: ByteBuf::from(vec![1u8, 2u8, 3u8]),
            signature: ByteBuf::from(vec![]),
        };

        let bytes = tx.encode();
        let tx2 = Transaction::decode(&bytes).unwrap();
        assert_eq!(tx, tx2);
    }

    #[test]
    fn test_encode_decode_with_non_empty_signature() {
        let tx = Transaction {
            version: Version::V0,
            r#type: TransactionType::Deposit,
            from: WalletRegisterId::from(1000000u32),
            nonce: Nonce::from(1u32),
            payload: ByteBuf::from(vec![1u8, 2u8, 3u8]),
            // 65 bytes
            signature: ByteBuf::from(vec![1u8; 65]),
        };

        let bytes = tx.encode();
        let tx2 = Transaction::decode(&bytes).unwrap();
        assert_eq!(tx, tx2);
    }
}
