#[cfg(any(feature = "std", test))]
pub(super) mod arg_handling;

use alloc::vec::Vec;
use core::fmt::{self, Display, Formatter};

use super::super::{RuntimeArgs, TransactionEntryPoint, TransactionScheduling, TransactionTarget};
#[cfg(feature = "datasize")]
use datasize::DataSize;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use rand::{Rng, RngCore};
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
#[cfg(any(feature = "std", test))]
use serde::{Deserialize, Serialize};
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use tracing::debug;

use super::TransactionCategory;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use super::TransactionConfig;
#[cfg(doc)]
use super::TransactionV1;
#[cfg(any(feature = "std", test))]
use crate::InvalidTransactionV1;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use crate::TransactionV1ExcessiveSizeError;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use crate::{
    bytesrepr::Bytes, testing::TestRng, PublicKey, TransactionInvocationTarget, TransactionRuntime,
    TransferTarget,
};
use crate::{
    bytesrepr::{Error, FromBytes, ToBytes},
    transaction::serialization::{
        CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder,
    },
};
/// The body of a [`TransactionV1`].
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(
    any(feature = "std", test),
    derive(Serialize, Deserialize),
    serde(deny_unknown_fields)
)]
#[cfg_attr(feature = "datasize", derive(DataSize))]
#[cfg_attr(
    feature = "json-schema",
    derive(JsonSchema),
    schemars(description = "Body of a `TransactionV1`.")
)]
pub struct TransactionV1Body {
    pub(crate) args: RuntimeArgs,
    pub(crate) target: TransactionTarget,
    pub(crate) entry_point: TransactionEntryPoint,
    pub(crate) transaction_category: u8,
    pub(crate) scheduling: TransactionScheduling,
}

impl TransactionV1Body {
    /// Returns a new `TransactionV1Body`.
    pub fn new(
        args: RuntimeArgs,
        target: TransactionTarget,
        entry_point: TransactionEntryPoint,
        transaction_category: u8,
        scheduling: TransactionScheduling,
    ) -> Self {
        TransactionV1Body {
            args,
            target,
            entry_point,
            transaction_category,
            scheduling,
        }
    }

    /// Returns the runtime args of the transaction.
    pub fn args(&self) -> &RuntimeArgs {
        &self.args
    }

    /// Consumes `self`, returning the runtime args of the transaction.
    pub fn take_args(self) -> RuntimeArgs {
        self.args
    }

    /// Returns the target of the transaction.
    pub fn target(&self) -> &TransactionTarget {
        &self.target
    }

    /// Returns the entry point of the transaction.
    pub fn entry_point(&self) -> &TransactionEntryPoint {
        &self.entry_point
    }

    /// Returns the scheduling kind of the transaction.
    pub fn scheduling(&self) -> &TransactionScheduling {
        &self.scheduling
    }

    /// Returns true if this transaction is a native mint interaction.
    pub fn is_native_mint(&self) -> bool {
        self.transaction_category == TransactionCategory::Mint as u8
    }

    /// Returns true if this transaction is a native auction interaction.
    pub fn is_native_auction(&self) -> bool {
        self.transaction_category == TransactionCategory::Auction as u8
    }

    /// Returns true if this transaction is a smart contract installer or upgrader.
    pub fn is_install_or_upgrade(&self) -> bool {
        self.transaction_category == TransactionCategory::InstallUpgrade as u8
    }

    /// Returns the transaction category.
    pub fn transaction_category(&self) -> u8 {
        self.transaction_category
    }

    /// Consumes `self`, returning its constituent parts.
    pub fn destructure(
        self,
    ) -> (
        RuntimeArgs,
        TransactionTarget,
        TransactionEntryPoint,
        TransactionScheduling,
    ) {
        (self.args, self.target, self.entry_point, self.scheduling)
    }

    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    pub(super) fn is_valid(&self, config: &TransactionConfig) -> Result<(), InvalidTransactionV1> {
        let kind = self.transaction_category;
        if !config.transaction_v1_config.is_supported(kind) {
            return Err(InvalidTransactionV1::InvalidTransactionKind(
                self.transaction_category,
            ));
        }

        let max_serialized_length = config.transaction_v1_config.get_max_serialized_length(kind);
        let actual_length = self.serialized_length();
        if actual_length > max_serialized_length as usize {
            return Err(InvalidTransactionV1::ExcessiveSize(
                TransactionV1ExcessiveSizeError {
                    max_transaction_size: max_serialized_length as u32,
                    actual_transaction_size: actual_length,
                },
            ));
        }

        let max_args_length = config.transaction_v1_config.get_max_args_length(kind);

        let args_length = self.args.serialized_length();
        if args_length > max_args_length as usize {
            debug!(
                args_length,
                max_args_length = max_args_length,
                "transaction runtime args excessive size"
            );
            return Err(InvalidTransactionV1::ExcessiveArgsLength {
                max_length: max_args_length as usize,
                got: args_length,
            });
        }

        match &self.target {
            TransactionTarget::Native => match self.entry_point {
                TransactionEntryPoint::Call => {
                    debug!(
                        entry_point = %self.entry_point,
                        "native transaction cannot have call entry point"
                    );
                    Err(InvalidTransactionV1::EntryPointCannotBeCall)
                }
                TransactionEntryPoint::Custom(_) => {
                    debug!(
                        entry_point = %self.entry_point,
                        "native transaction cannot have custom entry point"
                    );
                    Err(InvalidTransactionV1::EntryPointCannotBeCustom {
                        entry_point: self.entry_point.clone(),
                    })
                }
                TransactionEntryPoint::Transfer => arg_handling::has_valid_transfer_args(
                    &self.args,
                    config.native_transfer_minimum_motes,
                ),
                TransactionEntryPoint::AddBid => arg_handling::has_valid_add_bid_args(&self.args),
                TransactionEntryPoint::WithdrawBid => {
                    arg_handling::has_valid_withdraw_bid_args(&self.args)
                }
                TransactionEntryPoint::Delegate => {
                    arg_handling::has_valid_delegate_args(&self.args)
                }
                TransactionEntryPoint::Undelegate => {
                    arg_handling::has_valid_undelegate_args(&self.args)
                }
                TransactionEntryPoint::Redelegate => {
                    arg_handling::has_valid_redelegate_args(&self.args)
                }
                TransactionEntryPoint::ActivateBid => {
                    arg_handling::has_valid_activate_bid_args(&self.args)
                }
                TransactionEntryPoint::ChangeBidPublicKey => {
                    arg_handling::has_valid_change_bid_public_key_args(&self.args)
                }
                TransactionEntryPoint::AddReservations => {
                    arg_handling::has_valid_add_reservations_args(&self.args)
                }
                TransactionEntryPoint::CancelReservations => {
                    arg_handling::has_valid_cancel_reservations_args(&self.args)
                }
            },
            TransactionTarget::Stored { .. } => match &self.entry_point {
                TransactionEntryPoint::Custom(_) => Ok(()),
                TransactionEntryPoint::Call
                | TransactionEntryPoint::Transfer
                | TransactionEntryPoint::AddBid
                | TransactionEntryPoint::WithdrawBid
                | TransactionEntryPoint::Delegate
                | TransactionEntryPoint::Undelegate
                | TransactionEntryPoint::Redelegate
                | TransactionEntryPoint::ActivateBid
                | TransactionEntryPoint::ChangeBidPublicKey
                | TransactionEntryPoint::AddReservations
                | TransactionEntryPoint::CancelReservations => {
                    debug!(
                        entry_point = %self.entry_point,
                        "transaction targeting stored entity/package must have custom entry point"
                    );
                    Err(InvalidTransactionV1::EntryPointMustBeCustom {
                        entry_point: self.entry_point.clone(),
                    })
                }
            },
            TransactionTarget::Session { module_bytes, .. } => match &self.entry_point {
                TransactionEntryPoint::Call | TransactionEntryPoint::Custom(_) => {
                    if module_bytes.is_empty() {
                        debug!("transaction with session code must not have empty module bytes");
                        return Err(InvalidTransactionV1::EmptyModuleBytes);
                    }
                    Ok(())
                }
                TransactionEntryPoint::Transfer
                | TransactionEntryPoint::AddBid
                | TransactionEntryPoint::WithdrawBid
                | TransactionEntryPoint::Delegate
                | TransactionEntryPoint::Undelegate
                | TransactionEntryPoint::Redelegate
                | TransactionEntryPoint::ActivateBid
                | TransactionEntryPoint::ChangeBidPublicKey
                | TransactionEntryPoint::AddReservations
                | TransactionEntryPoint::CancelReservations => {
                    debug!(
                        entry_point = %self.entry_point,
                        "transaction with session code must use custom or default 'call' entry point"
                    );
                    Err(InvalidTransactionV1::EntryPointMustBeCustom {
                        entry_point: self.entry_point.clone(),
                    })
                }
            },
        }
    }

    fn serialized_field_lengths(&self) -> Vec<usize> {
        vec![
            self.args.serialized_length(),
            self.target.serialized_length(),
            self.entry_point.serialized_length(),
            self.transaction_category.serialized_length(),
            self.scheduling.serialized_length(),
        ]
    }

    /// Returns a random `TransactionV1Body`.
    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    pub fn random_of_category(rng: &mut TestRng, category: u8) -> Self {
        match category {
            0 => Self::random_transfer(rng),
            1 => Self::random_staking(rng),
            2 => Self::random_install_upgrade(rng),
            _ => Self::random_standard(rng),
        }
    }

    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    fn random_transfer(rng: &mut TestRng) -> Self {
        let amount =
            rng.gen_range(TransactionConfig::default().native_transfer_minimum_motes..=u64::MAX);
        let maybe_source = if rng.gen() { Some(rng.gen()) } else { None };
        let target = TransferTarget::random(rng);
        let maybe_id = rng.gen::<bool>().then(|| rng.gen());
        let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id).unwrap();
        TransactionV1Body::new(
            args,
            TransactionTarget::Native,
            TransactionEntryPoint::Transfer,
            TransactionCategory::Mint as u8,
            TransactionScheduling::random(rng),
        )
    }

    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    fn random_standard(rng: &mut TestRng) -> Self {
        let target = TransactionTarget::Stored {
            id: TransactionInvocationTarget::random(rng),
            runtime: TransactionRuntime::VmCasperV1,
        };
        TransactionV1Body::new(
            RuntimeArgs::random(rng),
            target,
            TransactionEntryPoint::Custom(rng.random_string(1..11)),
            TransactionCategory::Large as u8,
            TransactionScheduling::random(rng),
        )
    }

    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    fn random_install_upgrade(rng: &mut TestRng) -> Self {
        let target = TransactionTarget::Session {
            module_bytes: Bytes::from(rng.random_vec(0..100)),
            runtime: TransactionRuntime::VmCasperV1,
        };
        TransactionV1Body::new(
            RuntimeArgs::random(rng),
            target,
            TransactionEntryPoint::Custom(rng.random_string(1..11)),
            TransactionCategory::InstallUpgrade as u8,
            TransactionScheduling::random(rng),
        )
    }

    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    fn random_staking(rng: &mut TestRng) -> Self {
        let public_key = PublicKey::random(rng);
        let delegation_rate = rng.gen();
        let amount = rng.gen::<u64>();
        let minimum_delegation_amount = rng.gen::<u32>() as u64;
        let maximum_delegation_amount = minimum_delegation_amount + rng.gen::<u32>() as u64;
        let args = arg_handling::new_add_bid_args(
            public_key,
            delegation_rate,
            amount,
            minimum_delegation_amount,
            maximum_delegation_amount,
        )
        .unwrap();
        TransactionV1Body::new(
            args,
            TransactionTarget::Native,
            TransactionEntryPoint::AddBid,
            TransactionCategory::Auction as u8,
            TransactionScheduling::random(rng),
        )
    }

    /// Returns a random `TransactionV1Body`.
    #[cfg(any(all(feature = "std", feature = "testing"), test))]
    pub fn random(rng: &mut TestRng) -> Self {
        match rng.gen_range(0..8) {
            0 => {
                let amount = rng.gen_range(
                    TransactionConfig::default().native_transfer_minimum_motes..=u64::MAX,
                );
                let maybe_source = if rng.gen() { Some(rng.gen()) } else { None };
                let target = TransferTarget::random(rng);
                let maybe_id = rng.gen::<bool>().then(|| rng.gen());
                let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id)
                    .unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::Transfer,
                    TransactionCategory::Mint as u8,
                    TransactionScheduling::random(rng),
                )
            }
            1 => {
                let public_key = PublicKey::random(rng);
                let delegation_rate = rng.gen();
                let amount = rng.gen::<u64>();
                let minimum_delegation_amount = rng.gen::<u32>() as u64;
                let maximum_delegation_amount = minimum_delegation_amount + rng.gen::<u32>() as u64;
                let args = arg_handling::new_add_bid_args(
                    public_key,
                    delegation_rate,
                    amount,
                    minimum_delegation_amount,
                    maximum_delegation_amount,
                )
                .unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::AddBid,
                    TransactionCategory::Auction as u8,
                    TransactionScheduling::random(rng),
                )
            }
            2 => {
                let public_key = PublicKey::random(rng);
                let amount = rng.gen::<u64>();
                let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::WithdrawBid,
                    TransactionCategory::Auction as u8,
                    TransactionScheduling::random(rng),
                )
            }
            3 => {
                let delegator = PublicKey::random(rng);
                let validator = PublicKey::random(rng);
                let amount = rng.gen::<u64>();
                let args = arg_handling::new_delegate_args(delegator, validator, amount).unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::Delegate,
                    TransactionCategory::Auction as u8,
                    TransactionScheduling::random(rng),
                )
            }
            4 => {
                let delegator = PublicKey::random(rng);
                let validator = PublicKey::random(rng);
                let amount = rng.gen::<u64>();
                let args = arg_handling::new_undelegate_args(delegator, validator, amount).unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::Undelegate,
                    TransactionCategory::Auction as u8,
                    TransactionScheduling::random(rng),
                )
            }
            5 => {
                let delegator = PublicKey::random(rng);
                let validator = PublicKey::random(rng);
                let amount = rng.gen::<u64>();
                let new_validator = PublicKey::random(rng);
                let args =
                    arg_handling::new_redelegate_args(delegator, validator, amount, new_validator)
                        .unwrap();
                TransactionV1Body::new(
                    args,
                    TransactionTarget::Native,
                    TransactionEntryPoint::Redelegate,
                    TransactionCategory::Auction as u8,
                    TransactionScheduling::random(rng),
                )
            }
            6 => Self::random_standard(rng),
            7 => {
                let mut buffer = vec![0u8; rng.gen_range(1..100)];
                rng.fill_bytes(buffer.as_mut());
                let target = TransactionTarget::Session {
                    module_bytes: Bytes::from(buffer),
                    runtime: TransactionRuntime::VmCasperV1,
                };
                TransactionV1Body::new(
                    RuntimeArgs::random(rng),
                    target,
                    TransactionEntryPoint::Custom(rng.random_string(1..11)),
                    TransactionCategory::Large as u8,
                    TransactionScheduling::random(rng),
                )
            }
            _ => unreachable!(),
        }
    }
}

const ARGS_INDEX: u16 = 0;
const TARGET_INDEX: u16 = 1;
const ENTRY_POINT_INDEX: u16 = 2;
const TRANSACTION_CATEGORY_INDEX: u16 = 3;
const SCHEDULING_INDEX: u16 = 4;

impl FromBytes for TransactionV1Body {
    fn from_bytes(bytes: &[u8]) -> Result<(TransactionV1Body, &[u8]), Error> {
        let (binary_payload, remainder) =
            crate::transaction::serialization::CalltableSerializationEnvelope::from_bytes(
                5, bytes,
            )?;
        let window = binary_payload.start_consuming()?;
        let window = window.ok_or(Error::Formatting)?;
        window.verify_index(ARGS_INDEX)?;
        let (args, window) = window.deserialize_and_maybe_next::<RuntimeArgs>()?;
        let window = window.ok_or(Error::Formatting)?;
        window.verify_index(TARGET_INDEX)?;
        let (target, window) = window.deserialize_and_maybe_next::<TransactionTarget>()?;
        let window = window.ok_or(Error::Formatting)?;
        window.verify_index(ENTRY_POINT_INDEX)?;
        let (entry_point, window) = window.deserialize_and_maybe_next::<TransactionEntryPoint>()?;
        let window = window.ok_or(Error::Formatting)?;
        window.verify_index(TRANSACTION_CATEGORY_INDEX)?;
        let (transaction_category, window) = window.deserialize_and_maybe_next::<u8>()?;
        let window = window.ok_or(Error::Formatting)?;
        window.verify_index(SCHEDULING_INDEX)?;
        let (scheduling, window) = window.deserialize_and_maybe_next::<TransactionScheduling>()?;
        if window.is_some() {
            return Err(Error::Formatting);
        }
        let from_bytes = TransactionV1Body {
            args,
            target,
            entry_point,
            transaction_category,
            scheduling,
        };
        Ok((from_bytes, remainder))
    }
}

impl ToBytes for TransactionV1Body {
    fn to_bytes(&self) -> Result<Vec<u8>, Error> {
        CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
            .add_field(ARGS_INDEX, &self.args)?
            .add_field(TARGET_INDEX, &self.target)?
            .add_field(ENTRY_POINT_INDEX, &self.entry_point)?
            .add_field(TRANSACTION_CATEGORY_INDEX, &self.transaction_category)?
            .add_field(SCHEDULING_INDEX, &self.scheduling)?
            .binary_payload_bytes()
    }
    fn serialized_length(&self) -> usize {
        CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths())
    }
}

impl Display for TransactionV1Body {
    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
        write!(
            formatter,
            "v1-body({} {} {})",
            self.target, self.entry_point, self.scheduling
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{bytesrepr, runtime_args};

    #[test]
    fn bytesrepr_roundtrip() {
        let rng = &mut TestRng::new();
        let body = TransactionV1Body::random(rng);
        bytesrepr::test_serialization_roundtrip(&body);
    }

    #[test]
    fn not_acceptable_due_to_excessive_args_length() {
        let rng = &mut TestRng::new();
        let mut config = TransactionConfig::default();
        let mut body = TransactionV1Body::random_standard(rng);
        config.transaction_v1_config.wasm_lanes =
            vec![vec![body.transaction_category as u64, 1_048_576, 10, 0]];
        body.args = runtime_args! {"a" => 1_u8};

        let expected_error = InvalidTransactionV1::ExcessiveArgsLength {
            max_length: 10,
            got: 15,
        };

        assert_eq!(body.is_valid(&config), Err(expected_error));
    }

    #[test]
    fn not_acceptable_due_to_custom_entry_point_in_native() {
        let rng = &mut TestRng::new();
        let public_key = PublicKey::random(rng);
        let amount = rng.gen::<u64>();
        let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap();
        let entry_point = TransactionEntryPoint::Custom("custom".to_string());
        let body = TransactionV1Body::new(
            args,
            TransactionTarget::Native,
            entry_point.clone(),
            TransactionCategory::Mint as u8,
            TransactionScheduling::random(rng),
        );

        let expected_error = InvalidTransactionV1::EntryPointCannotBeCustom { entry_point };

        let config = TransactionConfig::default();
        assert_eq!(body.is_valid(&config), Err(expected_error));
    }

    #[test]
    fn not_acceptable_due_to_call_entry_point_in_native() {
        let rng = &mut TestRng::new();
        let public_key = PublicKey::random(rng);
        let amount = rng.gen::<u64>();
        let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap();
        let entry_point = TransactionEntryPoint::Call;
        let body = TransactionV1Body::new(
            args,
            TransactionTarget::Native,
            entry_point,
            TransactionCategory::Mint as u8,
            TransactionScheduling::random(rng),
        );

        let expected_error = InvalidTransactionV1::EntryPointCannotBeCall;

        let config = TransactionConfig::default();
        assert_eq!(body.is_valid(&config,), Err(expected_error));
    }

    #[test]
    fn not_acceptable_due_to_non_custom_entry_point_in_stored_or_session() {
        let rng = &mut TestRng::new();
        let config = TransactionConfig::default();

        let mut check = |entry_point: TransactionEntryPoint| {
            let stored_target = TransactionTarget::new_stored(
                TransactionInvocationTarget::ByHash([0; 32]),
                TransactionRuntime::VmCasperV1,
            );
            let session_target = TransactionTarget::new_session(
                Bytes::from(vec![1]),
                TransactionRuntime::VmCasperV1,
            );

            let stored_body = TransactionV1Body::new(
                RuntimeArgs::new(),
                stored_target,
                entry_point.clone(),
                TransactionCategory::Large as u8,
                TransactionScheduling::random(rng),
            );
            let session_body = TransactionV1Body::new(
                RuntimeArgs::new(),
                session_target,
                entry_point.clone(),
                TransactionCategory::Large as u8,
                TransactionScheduling::random(rng),
            );

            let expected_error = InvalidTransactionV1::EntryPointMustBeCustom { entry_point };

            assert_eq!(stored_body.is_valid(&config), Err(expected_error.clone()));
            assert_eq!(session_body.is_valid(&config), Err(expected_error));
        };

        check(TransactionEntryPoint::Transfer);
        check(TransactionEntryPoint::AddBid);
        check(TransactionEntryPoint::WithdrawBid);
        check(TransactionEntryPoint::Delegate);
        check(TransactionEntryPoint::Undelegate);
        check(TransactionEntryPoint::Redelegate);
    }
}
