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

use ciborium::tag::Captured;
use ex3_crypto::sha256;
use ex3_node_error::OtherError;
use ex3_payload_derive::Ex3Payload;
use ex3_serde::{bincode, cbor};
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::RegisterAsset;
pub use deposit::{Deposit, DepositIdentifier};
pub use secret::{CreateApiSecret, DestroyApiSecret, ResetMainSecret};
pub use transfer::{BatchTransfer, Transfer};
pub use types::TransactionType;
pub use withdrawal::Withdrawal;

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

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

#[derive(Serialize, Deserialize, 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: Version,
    #[index(1)]
    r#type: TransactionType,
    #[index(2)]
    #[alias_of_big_uint]
    from: WalletRegisterId,
    #[index(3)]
    #[alias_of_big_uint]
    nonce: Nonce,
    #[index(4)]
    payload: ByteBuf,
}

impl Transaction {
    pub fn hash(&self) -> TransactionHash {
        let bytes = bincode::serialize(&self).unwrap();
        sha256!(&bytes)
    }

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

    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(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct EncodedTransaction(pub ByteBuf);

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

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

impl From<EncodedTransaction> for Transaction {
    fn from(tx: EncodedTransaction) -> Self {
        bincode::deserialize(tx.0.as_ref()).unwrap()
    }
}

impl From<&EncodedTransaction> for Transaction {
    fn from(tx: &EncodedTransaction) -> Self {
        bincode::deserialize(tx.0.as_ref()).unwrap()
    }
}

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

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

#[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);
    }
}
