use core::{ops::Deref, fmt};
use std_shims::{
  vec::Vec,
  io,
  string::{String, ToString},
};

use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use rand::seq::SliceRandom;

use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};

use group::Group;
use curve25519_dalek::{
  constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE},
  scalar::Scalar,
  edwards::EdwardsPoint,
};
use dalek_ff_group as dfg;

#[cfg(feature = "multisig")]
use frost::FrostError;

use crate::{
  Protocol, Commitment, hash, random_scalar,
  serialize::{
    read_byte, read_bytes, read_u64, read_scalar, read_point, read_vec, write_byte, write_scalar,
    write_point, write_raw_vec, write_vec,
  },
  ringct::{
    generate_key_image,
    clsag::{ClsagError, ClsagInput, Clsag},
    bulletproofs::{MAX_OUTPUTS, Bulletproofs},
    RctBase, RctPrunable, RctSignatures,
  },
  transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
  rpc::RpcError,
  wallet::{
    address::{Network, AddressSpec, MoneroAddress},
    ViewPair, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness,
    shared_key, commitment_mask, amount_encryption,
    extra::{ARBITRARY_DATA_MARKER, MAX_ARBITRARY_DATA_SIZE},
  },
};

#[cfg(feature = "std")]
mod builder;
#[cfg(feature = "std")]
pub use builder::SignableTransactionBuilder;

#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::TransactionMachine;
use crate::ringct::EncryptedAmount;

#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
struct SendOutput {
  R: EdwardsPoint,
  view_tag: u8,
  dest: EdwardsPoint,
  commitment: Commitment,
  amount: [u8; 8],
}

impl SendOutput {
  #[allow(non_snake_case)]
  fn internal(
    unique: [u8; 32],
    output: (usize, (MoneroAddress, u64)),
    ecdh: EdwardsPoint,
    R: EdwardsPoint,
  ) -> (SendOutput, Option<[u8; 8]>) {
    let o = output.0;
    let output = output.1;

    let (view_tag, shared_key, payment_id_xor) =
      shared_key(Some(unique).filter(|_| output.0.is_guaranteed()), ecdh, o);

    (
      SendOutput {
        R,
        view_tag,
        dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend),
        commitment: Commitment::new(commitment_mask(shared_key), output.1),
        amount: amount_encryption(output.1, shared_key),
      },
      output
        .0
        .payment_id()
        .map(|id| (u64::from_le_bytes(id) ^ u64::from_le_bytes(payment_id_xor)).to_le_bytes()),
    )
  }

  fn new(
    r: &Zeroizing<Scalar>,
    unique: [u8; 32],
    output: (usize, (MoneroAddress, u64)),
  ) -> (SendOutput, Option<[u8; 8]>) {
    let address = output.1 .0;
    SendOutput::internal(
      unique,
      output,
      r.deref() * address.view,
      if !address.is_subaddress() {
        r.deref() * &ED25519_BASEPOINT_TABLE
      } else {
        r.deref() * address.spend
      },
    )
  }

  fn change(
    ecdh: EdwardsPoint,
    unique: [u8; 32],
    output: (usize, (MoneroAddress, u64)),
  ) -> (SendOutput, Option<[u8; 8]>) {
    SendOutput::internal(unique, output, ecdh, ED25519_BASEPOINT_POINT)
  }
}

#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum TransactionError {
  #[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))]
  MultiplePaymentIds,
  #[cfg_attr(feature = "std", error("no inputs"))]
  NoInputs,
  #[cfg_attr(feature = "std", error("no outputs"))]
  NoOutputs,
  #[cfg_attr(feature = "std", error("invalid number of decoys"))]
  InvalidDecoyQuantity,
  #[cfg_attr(feature = "std", error("only one output and no change address"))]
  NoChange,
  #[cfg_attr(feature = "std", error("too many outputs"))]
  TooManyOutputs,
  #[cfg_attr(feature = "std", error("too much data"))]
  TooMuchData,
  #[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))]
  TooLargeTransaction,
  #[cfg_attr(
    feature = "std",
    error("not enough funds (inputs {inputs}, outputs {outputs}, fee {fee})")
  )]
  NotEnoughFunds { inputs: u64, outputs: u64, fee: u64 },
  #[cfg_attr(feature = "std", error("wrong spend private key"))]
  WrongPrivateKey,
  #[cfg_attr(feature = "std", error("rpc error ({0})"))]
  RpcError(RpcError),
  #[cfg_attr(feature = "std", error("clsag error ({0})"))]
  ClsagError(ClsagError),
  #[cfg_attr(feature = "std", error("invalid transaction ({0})"))]
  InvalidTransaction(RpcError),
  #[cfg(feature = "multisig")]
  #[cfg_attr(feature = "std", error("frost error {0}"))]
  FrostError(FrostError),
}

fn prepare_inputs(
  inputs: &[(SpendableOutput, Decoys)],
  spend: &Zeroizing<Scalar>,
  tx: &mut Transaction,
) -> Result<Vec<(Zeroizing<Scalar>, EdwardsPoint, ClsagInput)>, TransactionError> {
  let mut signable = Vec::with_capacity(inputs.len());

  for (i, (input, decoys)) in inputs.iter().enumerate() {
    let input_spend = Zeroizing::new(input.key_offset() + spend.deref());
    let image = generate_key_image(&input_spend);
    signable.push((
      input_spend,
      image,
      ClsagInput::new(input.commitment().clone(), decoys.clone())
        .map_err(TransactionError::ClsagError)?,
    ));

    tx.prefix.inputs.push(Input::ToKey {
      amount: None,
      key_offsets: decoys.offsets.clone(),
      key_image: signable[i].1,
    });
  }

  signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse());
  tx.prefix.inputs.sort_by(|x, y| {
    if let (Input::ToKey { key_image: x, .. }, Input::ToKey { key_image: y, .. }) = (x, y) {
      x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
    } else {
      panic!("Input wasn't ToKey")
    }
  });

  Ok(signable)
}

// Deterministically calculate what the TX weight and fee will be.
fn calculate_weight_and_fee(
  protocol: Protocol,
  decoy_weights: &[usize],
  n_outputs: usize,
  extra: usize,
  fee_rate: Fee,
) -> (usize, u64) {
  // Starting the fee at 0 here is different than core Monero's wallet2.cpp, which starts its fee
  // calculation with an estimate.
  //
  // This difference is okay in practice because wallet2 still ends up using a fee calculated from
  // a TX's weight, as calculated later in this function.
  //
  // See this PR highlighting wallet2's behavior:
  //   https://github.com/monero-project/monero/pull/8882
  //
  // Even with that PR, if the estimated fee's VarInt byte length is larger than the calculated
  // fee's, the wallet can theoretically use a fee not based on the actual TX weight. This does not
  // occur in practice as it's nearly impossible for wallet2 to estimate a fee that is larger
  // than the calculated fee today, and on top of that, even more unlikely for that estimate's
  // VarInt to be larger in byte length than the calculated fee's.
  let mut weight = 0usize;
  let mut fee = 0u64;

  let mut done = false;
  let mut iters = 0;
  let max_iters = 5;
  while !done {
    weight = Transaction::fee_weight(protocol, decoy_weights, n_outputs, extra, fee);

    let fee_calculated_from_weight = fee_rate.calculate_fee_from_weight(weight);

    // Continue trying to use the fee calculated from the tx's weight
    done = fee_calculated_from_weight == fee;

    fee = fee_calculated_from_weight;

    #[cfg(test)]
    debug_assert!(iters < max_iters, "Reached max fee calculation attempts");
    // Should never happen because the fee VarInt byte length shouldn't change *every* single iter.
    // `iters` reaching `max_iters` is unexpected.
    if iters >= max_iters {
      // Fail-safe break to ensure funds are still spendable
      break;
    }
    iters += 1;
  }

  (weight, fee)
}

/// Fee struct, defined as a per-unit cost and a mask for rounding purposes.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub struct Fee {
  pub per_weight: u64,
  pub mask: u64,
}

impl Fee {
  pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 {
    let fee = (((self.per_weight * u64::try_from(weight).unwrap()) + self.mask - 1) / self.mask) *
      self.mask;
    debug_assert_eq!(weight, self.calculate_weight_from_fee(fee), "Miscalculated weight from fee");
    fee
  }

  pub fn calculate_weight_from_fee(&self, fee: u64) -> usize {
    usize::try_from(fee / self.per_weight).unwrap()
  }
}

/// Fee priority, determining how quickly a transaction is included in a block.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(non_camel_case_types)]
pub enum FeePriority {
  Lowest,
  Low,
  Medium,
  High,
  Custom { priority: u32 },
}

impl FeePriority {
  pub(crate) fn fee_priority(&self) -> u32 {
    match self {
      FeePriority::Lowest => 0,
      FeePriority::Low => 1,
      FeePriority::Medium => 2,
      FeePriority::High => 3,
      FeePriority::Custom { priority, .. } => *priority,
    }
  }
}

#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub(crate) enum InternalPayment {
  Payment((MoneroAddress, u64)),
  Change(Change, u64),
}

/// The eventual output of a SignableTransaction.
///
/// If the SignableTransaction has a Change with a view key, this will also have the view key.
/// Accordingly, it must be treated securely.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Eventuality {
  protocol: Protocol,
  r_seed: Zeroizing<[u8; 32]>,
  inputs: Vec<EdwardsPoint>,
  payments: Vec<InternalPayment>,
  extra: Vec<u8>,
}

/// A signable transaction, either in a single-signer or multisig context.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct SignableTransaction {
  protocol: Protocol,
  r_seed: Option<Zeroizing<[u8; 32]>>,
  inputs: Vec<(SpendableOutput, Decoys)>,
  payments: Vec<InternalPayment>,
  data: Vec<Vec<u8>>,
  fee: u64,
  fee_rate: Fee,
}

/// Specification for a change output.
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct Change {
  address: MoneroAddress,
  view: Option<Zeroizing<Scalar>>,
}

impl fmt::Debug for Change {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    f.debug_struct("Change").field("address", &self.address).finish_non_exhaustive()
  }
}

impl Change {
  /// Create a change output specification from a ViewPair, as needed to maintain privacy.
  pub fn new(view: &ViewPair, guaranteed: bool) -> Change {
    Change {
      address: view.address(
        Network::Mainnet,
        if !guaranteed {
          AddressSpec::Standard
        } else {
          AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true }
        },
      ),
      view: Some(view.view.clone()),
    }
  }

  /// Create a fingerprintable change output specification which will harm privacy. Only use this
  /// if you know what you're doing.
  pub fn fingerprintable(address: MoneroAddress) -> Change {
    Change { address, view: None }
  }
}

fn need_additional(payments: &[InternalPayment]) -> (bool, bool) {
  let mut has_change_view = false;
  let subaddresses = payments
    .iter()
    .filter(|payment| match *payment {
      InternalPayment::Payment(payment) => payment.0.is_subaddress(),
      InternalPayment::Change(change, _) => {
        if change.view.is_some() {
          has_change_view = true;
          // It should not be possible to construct a change specification to a subaddress with a
          // view key
          debug_assert!(!change.address.is_subaddress());
        }
        change.address.is_subaddress()
      }
    })
    .count() !=
    0;

  // We need additional keys if we have any subaddresses
  let mut additional = subaddresses;
  // Unless the above change view key path is taken
  if (payments.len() == 2) && has_change_view {
    additional = false;
  }

  (subaddresses, additional)
}

fn sanity_check_change_payment(payments: &[InternalPayment], has_change_address: bool) {
  debug_assert_eq!(
    payments
      .iter()
      .filter(|payment| match *payment {
        InternalPayment::Payment(_) => false,
        InternalPayment::Change(_, _) => true,
      })
      .count(),
    if has_change_address { 1 } else { 0 },
    "Unexpected number of change outputs"
  );
}

impl SignableTransaction {
  /// Create a signable transaction.
  ///
  /// `r_seed` refers to a seed used to derive the transaction's ephemeral keys (colloquially
  /// called Rs). If None is provided, one will be automatically generated.
  ///
  /// Up to 16 outputs may be present, including the change output. If the change address is
  /// specified, leftover funds will be sent to it.
  ///
  /// Each chunk of data must not exceed MAX_ARBITRARY_DATA_SIZE and will be embedded in TX extra.
  pub fn new(
    protocol: Protocol,
    r_seed: Option<Zeroizing<[u8; 32]>>,
    inputs: Vec<(SpendableOutput, Decoys)>,
    payments: Vec<(MoneroAddress, u64)>,
    change_address: Option<Change>,
    data: Vec<Vec<u8>>,
    fee_rate: Fee,
  ) -> Result<SignableTransaction, TransactionError> {
    // Make sure there's only one payment ID
    let mut has_payment_id = {
      let mut payment_ids = 0;
      let mut count = |addr: MoneroAddress| {
        if addr.payment_id().is_some() {
          payment_ids += 1
        }
      };
      for payment in &payments {
        count(payment.0);
      }
      if let Some(change) = change_address.as_ref() {
        count(change.address);
      }
      if payment_ids > 1 {
        Err(TransactionError::MultiplePaymentIds)?;
      }
      payment_ids == 1
    };

    if inputs.is_empty() {
      Err(TransactionError::NoInputs)?;
    }
    if payments.is_empty() {
      Err(TransactionError::NoOutputs)?;
    }

    for (_, decoys) in &inputs {
      if decoys.len() != protocol.ring_len() {
        Err(TransactionError::InvalidDecoyQuantity)?;
      }
    }

    for part in &data {
      if part.len() > MAX_ARBITRARY_DATA_SIZE {
        Err(TransactionError::TooMuchData)?;
      }
    }

    // If we don't have two outputs, as required by Monero, error
    if (payments.len() == 1) && change_address.is_none() {
      Err(TransactionError::NoChange)?;
    }

    // Get the outgoing amount ignoring fees
    let out_amount = payments.iter().map(|payment| payment.1).sum::<u64>();

    let outputs = payments.len() + usize::from(change_address.is_some());
    if outputs > MAX_OUTPUTS {
      Err(TransactionError::TooManyOutputs)?;
    }

    // Collect payments in a container that includes a change output if a change address is provided
    let mut payments = payments.into_iter().map(InternalPayment::Payment).collect::<Vec<_>>();
    if change_address.is_some() {
      // Push a 0 amount change output that we'll use to do fee calculations.
      // We'll modify the change amount after calculating the fee
      payments.push(InternalPayment::Change(change_address.clone().unwrap(), 0));
    }

    // Determine if we'll need additional pub keys in tx extra
    let (_, additional) = need_additional(&payments);

    // Add a dummy payment ID if there's only 2 payments
    has_payment_id |= outputs == 2;

    // Calculate the extra length
    let extra = Extra::fee_weight(outputs, additional, has_payment_id, data.as_ref());

    // https://github.com/monero-project/monero/pull/8733
    const MAX_EXTRA_SIZE: usize = 1060;
    if extra > MAX_EXTRA_SIZE {
      Err(TransactionError::TooMuchData)?;
    }

    // Caclculate weight of decoys
    let decoy_weights =
      inputs.iter().map(|(_, decoy)| Decoys::fee_weight(&decoy.offsets)).collect::<Vec<_>>();

    // Deterministically calculate tx weight and fee
    let (weight, fee) =
      calculate_weight_and_fee(protocol, &decoy_weights, outputs, extra, fee_rate);

    // The actual limit is half the block size, and for the minimum block size of 300k, that'd be
    // 150k
    // wallet2 will only create transactions up to 100k bytes however
    const MAX_TX_SIZE: usize = 100_000;
    if weight >= MAX_TX_SIZE {
      Err(TransactionError::TooLargeTransaction)?;
    }

    // Make sure we have enough funds
    let in_amount = inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
    if in_amount < (out_amount + fee) {
      Err(TransactionError::NotEnoughFunds { inputs: in_amount, outputs: out_amount, fee })?;
    }

    // Sanity check we have the expected number of change outputs
    sanity_check_change_payment(&payments, change_address.is_some());

    // Modify the amount of the change output
    if change_address.is_some() {
      let change_payment = payments.last_mut().unwrap();
      debug_assert!(matches!(change_payment, InternalPayment::Change(_, _)));
      *change_payment =
        InternalPayment::Change(change_address.clone().unwrap(), in_amount - out_amount - fee);
    }

    // Sanity check the change again after modifying
    sanity_check_change_payment(&payments, change_address.is_some());

    // Sanity check outgoing amount + fee == incoming amount
    debug_assert_eq!(
      payments
        .iter()
        .map(|payment| match *payment {
          InternalPayment::Payment(payment) => payment.1,
          InternalPayment::Change(_, amount) => amount,
        })
        .sum::<u64>() +
        fee,
      in_amount,
      "Outgoing amount + fee != incoming amount"
    );

    Ok(SignableTransaction { protocol, r_seed, inputs, payments, data, fee, fee_rate })
  }

  pub fn fee(&self) -> u64 {
    self.fee
  }

  pub fn fee_rate(&self) -> Fee {
    self.fee_rate
  }

  #[allow(clippy::type_complexity)]
  fn prepare_payments(
    seed: &Zeroizing<[u8; 32]>,
    inputs: &[EdwardsPoint],
    payments: &mut Vec<InternalPayment>,
    uniqueness: [u8; 32],
  ) -> (EdwardsPoint, Vec<Zeroizing<Scalar>>, Vec<SendOutput>, Option<[u8; 8]>) {
    let mut rng = {
      // Hash the inputs into the seed so we don't re-use Rs
      // Doesn't re-use uniqueness as that's based on key images, which requires interactivity
      // to generate. The output keys do not
      // This remains private so long as the seed is private
      let mut r_uniqueness = vec![];
      for input in inputs {
        r_uniqueness.extend(input.compress().to_bytes());
      }
      ChaCha20Rng::from_seed(hash(
        &[b"monero-serai_outputs".as_ref(), seed.as_ref(), &r_uniqueness].concat(),
      ))
    };

    // Shuffle the payments
    payments.shuffle(&mut rng);

    // Used for all non-subaddress outputs, or if there's only one subaddress output and a change
    let tx_key = Zeroizing::new(random_scalar(&mut rng));
    let mut tx_public_key = tx_key.deref() * &ED25519_BASEPOINT_TABLE;

    // If any of these outputs are to a subaddress, we need keys distinct to them
    // The only time this *does not* force having additional keys is when the only other output
    // is a change output we have the view key for, enabling rewriting rA to aR
    let (subaddresses, additional) = need_additional(payments);
    let modified_change_ecdh = subaddresses && (!additional);

    // If we're using the aR rewrite, update tx_public_key from rG to rB
    if modified_change_ecdh {
      for payment in &*payments {
        match payment {
          InternalPayment::Payment(payment) => {
            // This should be the only payment and it should be a subaddress
            debug_assert!(payment.0.is_subaddress());
            tx_public_key = tx_key.deref() * payment.0.spend;
          }
          InternalPayment::Change(_, _) => {}
        }
      }
      debug_assert!(tx_public_key != (tx_key.deref() * &ED25519_BASEPOINT_TABLE));
    }

    // Actually create the outputs
    let mut additional_keys = vec![];
    let mut outputs = Vec::with_capacity(payments.len());
    let mut id = None;
    for (o, mut payment) in payments.drain(..).enumerate() {
      // Downcast the change output to a payment output if it doesn't require special handling
      // regarding it's view key
      payment = if !modified_change_ecdh {
        if let InternalPayment::Change(change, amount) = &payment {
          InternalPayment::Payment((change.address, *amount))
        } else {
          payment
        }
      } else {
        payment
      };

      let (output, payment_id) = match payment {
        InternalPayment::Payment(payment) => {
          // If this is a subaddress, generate a dedicated r. Else, reuse the TX key
          let dedicated = Zeroizing::new(random_scalar(&mut rng));
          let use_dedicated = additional && payment.0.is_subaddress();
          let r = if use_dedicated { &dedicated } else { &tx_key };

          let (mut output, payment_id) = SendOutput::new(r, uniqueness, (o, payment));
          if modified_change_ecdh {
            debug_assert_eq!(tx_public_key, output.R);
          }

          if use_dedicated {
            additional_keys.push(dedicated);
          } else {
            // If this used tx_key, randomize its R
            // This is so when extra is created, there's a distinct R for it to use
            output.R = dfg::EdwardsPoint::random(&mut rng).0;
          }
          (output, payment_id)
        }
        InternalPayment::Change(change, amount) => {
          // Instead of rA, use Ra, where R is r * subaddress_spend_key
          // change.view must be Some as if it's None, this payment would've been downcast
          let ecdh = tx_public_key * change.view.unwrap().deref();
          SendOutput::change(ecdh, uniqueness, (o, (change.address, amount)))
        }
      };

      outputs.push(output);
      id = id.or(payment_id);
    }

    // Include a random payment ID if we don't actually have one
    // It prevents transactions from leaking if they're sending to integrated addresses or not
    // Only do this if we only have two outputs though, as Monero won't add a dummy if there's
    // more than two outputs
    if outputs.len() <= 2 {
      let mut rand = [0; 8];
      rng.fill_bytes(&mut rand);
      id = id.or(Some(rand));
    }

    (tx_public_key, additional_keys, outputs, id)
  }

  #[allow(non_snake_case)]
  fn extra(
    tx_key: EdwardsPoint,
    additional: bool,
    Rs: Vec<EdwardsPoint>,
    id: Option<[u8; 8]>,
    data: &mut Vec<Vec<u8>>,
  ) -> Vec<u8> {
    #[allow(non_snake_case)]
    let Rs_len = Rs.len();
    let mut extra = Extra::new(tx_key, if additional { Rs } else { vec![] });

    if let Some(id) = id {
      let mut id_vec = Vec::with_capacity(1 + 8);
      PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
      extra.push(ExtraField::Nonce(id_vec));
    }

    // Include data if present
    let extra_len = Extra::fee_weight(Rs_len, additional, id.is_some(), data.as_ref());
    for part in data.drain(..) {
      let mut arb = vec![ARBITRARY_DATA_MARKER];
      arb.extend(part);
      extra.push(ExtraField::Nonce(arb));
    }

    let mut serialized = Vec::with_capacity(extra_len);
    extra.write(&mut serialized).unwrap();
    debug_assert_eq!(extra_len, serialized.len());
    serialized
  }

  /// Returns the eventuality of this transaction.
  ///
  /// The eventuality is defined as the TX extra/outputs this transaction will create, if signed
  /// with the specified seed. This eventuality can be compared to on-chain transactions to see
  /// if the transaction has already been signed and published.
  pub fn eventuality(&self) -> Option<Eventuality> {
    let inputs = self.inputs.iter().map(|(input, _)| input.key()).collect::<Vec<_>>();
    let (tx_key, additional, outputs, id) = Self::prepare_payments(
      self.r_seed.as_ref()?,
      &inputs,
      &mut self.payments.clone(),
      // Lie about the uniqueness, used when determining output keys/commitments yet not the
      // ephemeral keys, which is want we want here
      // While we do still grab the outputs variable, it's so we can get its Rs
      [0; 32],
    );
    #[allow(non_snake_case)]
    let Rs = outputs.iter().map(|output| output.R).collect();
    drop(outputs);

    let additional = !additional.is_empty();
    let extra = Self::extra(tx_key, additional, Rs, id, &mut self.data.clone());

    Some(Eventuality {
      protocol: self.protocol,
      r_seed: self.r_seed.clone()?,
      inputs,
      payments: self.payments.clone(),
      extra,
    })
  }

  fn prepare_transaction<R: RngCore + CryptoRng>(
    &mut self,
    rng: &mut R,
    uniqueness: [u8; 32],
  ) -> (Transaction, Scalar) {
    // If no seed for the ephemeral keys was provided, make one
    let r_seed = self.r_seed.clone().unwrap_or_else(|| {
      let mut res = Zeroizing::new([0; 32]);
      rng.fill_bytes(res.as_mut());
      res
    });

    let (tx_key, additional, outputs, id) = Self::prepare_payments(
      &r_seed,
      &self.inputs.iter().map(|(input, _)| input.key()).collect::<Vec<_>>(),
      &mut self.payments,
      uniqueness,
    );
    // This function only cares if additional keys were necessary, not what they were
    let additional = !additional.is_empty();

    let commitments = outputs.iter().map(|output| output.commitment.clone()).collect::<Vec<_>>();
    let sum = commitments.iter().map(|commitment| commitment.mask).sum();

    // Safe due to the constructor checking MAX_OUTPUTS
    let bp = Bulletproofs::prove(rng, &commitments, self.protocol.bp_plus()).unwrap();

    // Create the TX extra
    let extra = Self::extra(
      tx_key,
      additional,
      outputs.iter().map(|output| output.R).collect(),
      id,
      &mut self.data,
    );

    let mut fee = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
    let mut tx_outputs = Vec::with_capacity(outputs.len());
    let mut encrypted_amounts = Vec::with_capacity(outputs.len());
    for output in &outputs {
      fee -= output.commitment.amount;
      tx_outputs.push(Output {
        amount: None,
        key: output.dest.compress(),
        view_tag: Some(output.view_tag).filter(|_| self.protocol.view_tags()),
      });
      encrypted_amounts.push(EncryptedAmount::Compact { amount: output.amount });
    }
    debug_assert_eq!(self.fee, fee, "transaction will use an unexpected fee");

    (
      Transaction {
        prefix: TransactionPrefix {
          version: 2,
          timelock: Timelock::None,
          inputs: vec![],
          outputs: tx_outputs,
          extra,
        },
        signatures: vec![],
        rct_signatures: RctSignatures {
          base: RctBase {
            fee,
            encrypted_amounts,
            pseudo_outs: vec![],
            commitments: commitments.iter().map(Commitment::calculate).collect(),
          },
          prunable: RctPrunable::Clsag { bulletproofs: bp, clsags: vec![], pseudo_outs: vec![] },
        },
      },
      sum,
    )
  }

  /// Sign this transaction.
  // TODO: Remove async
  pub async fn sign<R: RngCore + CryptoRng>(
    mut self,
    rng: &mut R,
    spend: &Zeroizing<Scalar>,
  ) -> Result<Transaction, TransactionError> {
    let mut images = Vec::with_capacity(self.inputs.len());
    for (input, _) in &self.inputs {
      let mut offset = Zeroizing::new(spend.deref() + input.key_offset());
      if (offset.deref() * &ED25519_BASEPOINT_TABLE) != input.key() {
        Err(TransactionError::WrongPrivateKey)?;
      }

      images.push(generate_key_image(&offset));
      offset.zeroize();
    }
    images.sort_by(key_image_sort);

    let (mut tx, mask_sum) = self.prepare_transaction(
      rng,
      uniqueness(
        &images
          .iter()
          .map(|image| Input::ToKey { amount: None, key_offsets: vec![], key_image: *image })
          .collect::<Vec<_>>(),
      ),
    );

    let signable = prepare_inputs(&self.inputs, spend, &mut tx)?;

    let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash());
    match tx.rct_signatures.prunable {
      RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
      RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
        clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>());
        pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::<Vec<_>>());
      }
      _ => unreachable!("attempted to sign a TX which wasn't CLSAG"),
    }

    debug_assert_eq!(
      self.fee_rate.calculate_fee_from_weight(tx.weight()),
      tx.rct_signatures.base.fee,
      "transaction used unexpected fee",
    );

    Ok(tx)
  }
}

impl Eventuality {
  /// Enables building a HashMap of Extra -> Eventuality for efficiently checking if an on-chain
  /// transaction may match this eventuality.
  ///
  /// This extra is cryptographically bound to:
  /// 1) A specific set of inputs (via their output key)
  /// 2) A specific seed for the ephemeral keys
  ///
  /// This extra may be used in a transaction with a distinct set of inputs, yet no honest
  /// transaction which doesn't satisfy this Eventuality will contain it.
  pub fn extra(&self) -> &[u8] {
    &self.extra
  }

  #[must_use]
  pub fn matches(&self, tx: &Transaction) -> bool {
    if self.payments.len() != tx.prefix.outputs.len() {
      return false;
    }

    // Verify extra.
    // Even if all the outputs were correct, a malicious extra could still cause a recipient to
    // fail to receive their funds.
    // This is the cheapest check available to perform as it does not require TX-specific ECC ops.
    if self.extra != tx.prefix.extra {
      return false;
    }

    // Also ensure no timelock was set.
    if tx.prefix.timelock != Timelock::None {
      return false;
    }

    // Generate the outputs. This is TX-specific due to uniqueness.
    let (_, _, outputs, _) = SignableTransaction::prepare_payments(
      &self.r_seed,
      &self.inputs,
      &mut self.payments.clone(),
      uniqueness(&tx.prefix.inputs),
    );

    let rct_type = tx.rct_signatures.rct_type();
    if rct_type != self.protocol.optimal_rct_type() {
      return false;
    }

    // TODO: Remove this when the following for loop is updated
    assert!(
      rct_type.compact_encrypted_amounts(),
      "created an Eventuality for a very old RctType we don't support proving for"
    );

    for (o, (expected, actual)) in outputs.iter().zip(tx.prefix.outputs.iter()).enumerate() {
      // Verify the output, commitment, and encrypted amount.
      if (&Output {
        amount: None,
        key: expected.dest.compress(),
        view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()),
      } != actual) ||
        (Some(&expected.commitment.calculate()) != tx.rct_signatures.base.commitments.get(o)) ||
        (Some(&EncryptedAmount::Compact { amount: expected.amount }) !=
          tx.rct_signatures.base.encrypted_amounts.get(o))
      {
        return false;
      }
    }

    true
  }

  pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
    self.protocol.write(w)?;
    write_raw_vec(write_byte, self.r_seed.as_ref(), w)?;
    write_vec(write_point, &self.inputs, w)?;

    fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
      match payment {
        InternalPayment::Payment(payment) => {
          w.write_all(&[0])?;
          write_vec(write_byte, payment.0.to_string().as_bytes(), w)?;
          w.write_all(&payment.1.to_le_bytes())
        }
        InternalPayment::Change(change, amount) => {
          w.write_all(&[1])?;
          write_vec(write_byte, change.address.to_string().as_bytes(), w)?;
          if let Some(view) = change.view.as_ref() {
            w.write_all(&[1])?;
            write_scalar(view, w)?;
          } else {
            w.write_all(&[0])?;
          }
          w.write_all(&amount.to_le_bytes())
        }
      }
    }
    write_vec(write_payment, &self.payments, w)?;

    write_vec(write_byte, &self.extra, w)
  }

  pub fn serialize(&self) -> Vec<u8> {
    let mut buf = Vec::with_capacity(128);
    self.write(&mut buf).unwrap();
    buf
  }

  pub fn read<R: io::Read>(r: &mut R) -> io::Result<Eventuality> {
    fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
      String::from_utf8(read_vec(read_byte, r)?)
        .ok()
        .and_then(|str| MoneroAddress::from_str_raw(&str).ok())
        .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid address"))
    }

    fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
      Ok(match read_byte(r)? {
        0 => InternalPayment::Payment((read_address(r)?, read_u64(r)?)),
        1 => InternalPayment::Change(
          Change {
            address: read_address(r)?,
            view: match read_byte(r)? {
              0 => None,
              1 => Some(Zeroizing::new(read_scalar(r)?)),
              _ => Err(io::Error::new(io::ErrorKind::Other, "invalid change payment"))?,
            },
          },
          read_u64(r)?,
        ),
        _ => Err(io::Error::new(io::ErrorKind::Other, "invalid payment"))?,
      })
    }

    Ok(Eventuality {
      protocol: Protocol::read(r)?,
      r_seed: Zeroizing::new(read_bytes::<_, 32>(r)?),
      inputs: read_vec(read_point, r)?,
      payments: read_vec(read_payment, r)?,
      extra: read_vec(read_byte, r)?,
    })
  }
}
