#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]

use std::sync::Arc;

use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
use alloy_consensus::{Signed, TxLegacy};

use alloy_sol_types::SolCall;

use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};

#[rustfmt::skip]
#[expect(warnings)]
#[expect(needless_pass_by_value)]
#[expect(clippy::all)]
#[expect(clippy::ignored_unit_patterns)]
#[expect(clippy::redundant_closure_for_method_calls)]
mod abi {
  alloy_sol_macro::sol!("contracts/Deployer.sol");
}

/// The Deployer contract for the Serai Router contract.
///
/// This Deployer has a deterministic address, letting it be immediately identified on any instance
/// of the EVM. It then supports retrieving the deployed contracts addresses (which aren't
/// deterministic) using a single call.
#[derive(Clone, Debug)]
pub struct Deployer(Arc<RootProvider<SimpleRequest>>);
impl Deployer {
  /// Obtain the transaction to deploy this contract, already signed.
  ///
  /// The account this transaction is sent from (which is populated in `from`) must be sufficiently
  /// funded for this transaction to be submitted. This account has no known private key to anyone
  /// so ETH sent can be neither misappropriated nor returned.
  pub fn deployment_tx() -> Signed<TxLegacy> {
    pub const BYTECODE: &[u8] =
      include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-deployer/Deployer.bin"));
    let bytecode =
      Bytes::from_hex(BYTECODE).expect("compiled-in Deployer bytecode wasn't valid hex");

    // Legacy transactions are used to ensure the widest possible degree of support across EVMs
    let tx = TxLegacy {
      chain_id: None,
      nonce: 0,
      // This uses a fixed gas price as necessary to achieve a deterministic address
      // The gas price is fixed to 100 gwei, which should be incredibly generous, in order to make
      // this getting stuck unlikely. While expensive, this only has to occur once
      gas_price: 100_000_000_000u128,
      // TODO: Use a more accurate gas limit
      gas_limit: 1_000_000u64,
      to: TxKind::Create,
      value: U256::ZERO,
      input: bytecode,
    };

    ethereum_primitives::deterministically_sign(tx)
  }

  /// Obtain the deterministic address for this contract.
  pub fn address() -> Address {
    let deployer_deployer =
      Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
    Address::create(&deployer_deployer, 0)
  }

  /// Obtain the unsigned transaction to deploy a contract.
  ///
  /// This will not have its `nonce`, `gas_price`, nor `gas_limit` filled out.
  pub fn deploy_tx(init_code: Vec<u8>) -> TxLegacy {
    TxLegacy {
      chain_id: None,
      nonce: 0,
      gas_price: 0,
      gas_limit: 0,
      to: TxKind::Call(Self::address()),
      value: U256::ZERO,
      input: abi::Deployer::deployCall::new((init_code.into(),)).abi_encode().into(),
    }
  }

  /// Construct a new view of the Deployer.
  ///
  /// This will return `None` if the Deployer has yet to be deployed on-chain.
  pub async fn new(
    provider: Arc<RootProvider<SimpleRequest>>,
  ) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
    let address = Self::address();
    let code = provider.get_code_at(address).await?;
    // Contract has yet to be deployed
    if code.is_empty() {
      return Ok(None);
    }
    Ok(Some(Self(provider)))
  }

  /// Find the deployment of a contract.
  pub async fn find_deployment(
    &self,
    init_code_hash: [u8; 32],
  ) -> Result<Option<Address>, RpcError<TransportErrorKind>> {
    let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new(
      abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(),
    ));
    let bytes = self.0.call(&call).await?;
    let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true)
      .map_err(|e| {
        TransportErrorKind::Custom(
          format!("node returned a non-address for function returning address: {e:?}").into(),
        )
      })?
      ._0;

    if **deployment == [0; 20] {
      return Ok(None);
    }
    Ok(Some(deployment))
  }
}
