//! Parameter types, macros, and utility helpers for ETH RPC endpoints.

use super::{
    error::EthError,
    types::{RpcReceipt, RpcTransaction},
};
use crate::{config::resolve::ResolveError, interest::InterestKind};
use alloy::{
    consensus::{
        ReceiptEnvelope, ReceiptWithBloom, Transaction, TxReceipt, transaction::Recovered,
    },
    eips::BlockId,
    primitives::{Address, TxKind, U256},
    rpc::types::{
        BlockOverrides, Log, TransactionReceipt, TransactionRequest, pubsub::SubscriptionKind,
        state::StateOverride,
    },
};
use serde::Deserialize;
use signet_cold::ColdReceipt;
use signet_hot::db::HotDbRead;
use signet_storage_types::ConfirmationMeta;
use trevm::MIN_TRANSACTION_GAS;

/// Args for `eth_call` and `eth_estimateGas`.
#[derive(Debug, Deserialize)]
pub(crate) struct TxParams(
    pub TransactionRequest,
    #[serde(default)] pub Option<BlockId>,
    #[serde(default)] pub Option<StateOverride>,
    #[serde(default)] pub Option<Box<BlockOverrides>>,
);

/// Args for `eth_getBlockByHash` and `eth_getBlockByNumber`.
#[derive(Debug, Deserialize)]
pub(crate) struct BlockParams<T>(pub T, #[serde(default)] pub Option<bool>);

/// Args for `eth_getStorageAt`.
#[derive(Debug, Deserialize)]
pub(crate) struct StorageAtArgs(pub Address, pub U256, #[serde(default)] pub Option<BlockId>);

/// Args for `eth_getBalance`, `eth_getTransactionCount`, and `eth_getCode`.
#[derive(Debug, Deserialize)]
pub(crate) struct AddrWithBlock(pub Address, #[serde(default)] pub Option<BlockId>);

/// Args for `eth_feeHistory`.
#[derive(Debug, Deserialize)]
pub(crate) struct FeeHistoryArgs(
    pub alloy::primitives::U64,
    pub alloy::eips::BlockNumberOrTag,
    #[serde(default)] pub Option<Vec<f64>>,
);

/// Args for `eth_subscribe`.
#[derive(Debug, Deserialize)]
pub(crate) struct SubscribeArgs(
    pub alloy::rpc::types::pubsub::SubscriptionKind,
    #[serde(default)] pub Option<Box<alloy::rpc::types::Filter>>,
);

impl TryFrom<SubscribeArgs> for InterestKind {
    type Error = EthError;

    fn try_from(args: SubscribeArgs) -> Result<Self, Self::Error> {
        match args.0 {
            SubscriptionKind::Logs => args.1.map(InterestKind::Log).ok_or_else(|| {
                EthError::InvalidParams("missing filter for Logs subscription".into())
            }),
            SubscriptionKind::NewHeads => {
                if args.1.is_some() {
                    Err(EthError::InvalidParams(
                        "filter not supported for NewHeads subscription".into(),
                    ))
                } else {
                    Ok(InterestKind::Block)
                }
            }
            other => {
                Err(EthError::InvalidParams(format!("unsupported subscription kind: {other:?}")))
            }
        }
    }
}

/// Normalize transaction request gas without making DB reads.
///
/// - If the gas is below `MIN_TRANSACTION_GAS`, set it to `None`
/// - If the gas is above the `rpc_gas_cap`, set it to the `rpc_gas_cap`
pub(crate) const fn normalize_gas_stateless(request: &mut TransactionRequest, max_gas: u64) {
    match request.gas {
        Some(..MIN_TRANSACTION_GAS) => request.gas = None,
        Some(val) if val > max_gas => request.gas = Some(max_gas),
        _ => {}
    }
}

/// Await a spawned handler task. Returns the error expression on
/// panic, cancellation, or `None` result.
macro_rules! await_handler {
    ($h:expr, $err:expr) => {
        match $h.await {
            Ok(Some(res)) => res,
            _ => return Err($err),
        }
    };
}
pub(crate) use await_handler;

/// Resolve a block ID and open a hot storage reader at that height.
///
/// Opens a single MDBX read transaction and resolves the block ID
/// within it, ensuring the resolution and subsequent data query share
/// the same database snapshot.
///
/// For hash-based [`BlockId`]s, the hash→number lookup happens inside
/// the returned transaction. For tag-based IDs, the atomic tag read
/// is outside MDBX (unavoidable), but the snapshot is opened first so
/// data is guaranteed to be at least as fresh as the resolved tag.
///
/// Used by account-state endpoints (`balance`, `storage_at`,
/// `addr_tx_count`, `code_at`).
pub(crate) fn hot_reader_at_block<H>(
    ctx: &crate::config::StorageRpcCtx<H>,
    id: BlockId,
) -> Result<(H::RoTx, u64), EthError>
where
    H: signet_hot::HotKv,
    <H::RoTx as signet_hot::model::HotKvRead>::Error: std::error::Error + Send + Sync + 'static,
{
    let reader = ctx.hot_reader()?;
    let height = match id {
        BlockId::Number(tag) => ctx.resolve_block_tag(tag),
        BlockId::Hash(h) => reader
            .get_header_number(&h.block_hash)
            .map_err(|e| ResolveError::Db(Box::new(e)))?
            .ok_or(ResolveError::HashNotFound(h.block_hash))?,
    };
    Ok((reader, height))
}

/// Small wrapper implementing [`trevm::Cfg`] to set the chain ID.
pub(crate) struct CfgFiller(pub u64);

impl trevm::Cfg for CfgFiller {
    fn fill_cfg_env(&self, cfg: &mut trevm::revm::context::CfgEnv) {
        cfg.chain_id = self.0;
    }
}

/// Build an [`alloy::rpc::types::Transaction`] from cold storage types.
pub(crate) fn build_rpc_transaction(
    tx: &signet_storage_types::RecoveredTx,
    meta: &ConfirmationMeta,
    base_fee: Option<u64>,
) -> RpcTransaction {
    let signer = tx.signer();
    let tx_envelope: alloy::consensus::TxEnvelope = tx.clone().into_inner().into();
    let inner = Recovered::new_unchecked(tx_envelope, signer);

    let egp = base_fee
        .map(|bf| inner.effective_tip_per_gas(bf).unwrap_or_default() as u64 + bf)
        .unwrap_or_else(|| inner.max_fee_per_gas() as u64);

    alloy::rpc::types::Transaction {
        inner,
        block_hash: Some(meta.block_hash()),
        block_number: Some(meta.block_number()),
        transaction_index: Some(meta.transaction_index()),
        effective_gas_price: Some(egp as u128),
    }
}

/// Build a [`TransactionReceipt`] from a [`ColdReceipt`] and its transaction.
///
/// The transaction is needed for `to`, `contract_address`, and
/// `effective_gas_price` which are not stored on the receipt.
pub(crate) fn build_receipt(
    cr: &ColdReceipt,
    tx: &signet_storage_types::RecoveredTx,
    base_fee: Option<u64>,
) -> RpcReceipt {
    let logs_bloom = cr.receipt.bloom();
    let status = cr.receipt.status;
    let cumulative_gas_used = cr.receipt.cumulative_gas_used;

    let rpc_receipt = alloy::rpc::types::eth::Receipt {
        status,
        cumulative_gas_used,
        logs: cr.receipt.logs.clone(),
    };

    let (contract_address, to) = match tx.kind() {
        TxKind::Create => (Some(cr.from.create(tx.nonce())), None),
        TxKind::Call(addr) => (None, Some(Address(*addr))),
    };

    let egp = base_fee
        .map(|bf| tx.effective_tip_per_gas(bf).unwrap_or_default() as u64 + bf)
        .unwrap_or_else(|| tx.max_fee_per_gas() as u64);

    TransactionReceipt {
        inner: build_receipt_envelope(
            ReceiptWithBloom { receipt: rpc_receipt, logs_bloom },
            cr.tx_type,
        ),
        transaction_hash: cr.tx_hash,
        transaction_index: Some(cr.transaction_index),
        block_hash: Some(cr.block_hash),
        block_number: Some(cr.block_number),
        from: cr.from,
        to,
        gas_used: cr.gas_used,
        contract_address,
        effective_gas_price: egp as u128,
        blob_gas_price: None,
        blob_gas_used: None,
    }
}

/// Wrap a receipt in the appropriate [`ReceiptEnvelope`] variant.
const fn build_receipt_envelope(
    receipt: ReceiptWithBloom<alloy::consensus::Receipt<Log>>,
    tx_type: alloy::consensus::TxType,
) -> ReceiptEnvelope<Log> {
    match tx_type {
        alloy::consensus::TxType::Legacy => ReceiptEnvelope::Legacy(receipt),
        alloy::consensus::TxType::Eip2930 => ReceiptEnvelope::Eip2930(receipt),
        alloy::consensus::TxType::Eip1559 => ReceiptEnvelope::Eip1559(receipt),
        alloy::consensus::TxType::Eip4844 => ReceiptEnvelope::Eip4844(receipt),
        alloy::consensus::TxType::Eip7702 => ReceiptEnvelope::Eip7702(receipt),
    }
}
