use std::{
    collections::BTreeMap,
    fmt,
    pin::Pin,
    sync::{Arc, LazyLock},
    task::{Context, Poll},
};

use alloy_consensus::{BlockHeader, Transaction, TxReceipt};
use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
use alloy_network::Network;
use alloy_primitives::B256;
use futures::StreamExt;
use parking_lot::{Mutex, RwLock};
use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};

use crate::eth::{
    backend::{info::StorageInfo, notifications::ChainNotifications},
    error::BlockchainError,
};

/// Maximum number of entries in the fee history cache
pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;

/// Initial base fee for EIP-1559 blocks.
pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;

/// Initial default gas price for the first block
pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;

/// Bounds the amount the base fee can change between blocks.
pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;

/// Minimum suggested priority fee
pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;

/// Stores the fee related information
#[derive(Clone, Debug)]
pub struct FeeManager {
    /// Hardfork identifier
    spec_id: SpecId,
    /// The blob params that determine blob fees
    blob_params: Arc<RwLock<BlobParams>>,
    /// Tracks the base fee for the next block post London
    ///
    /// This value will be updated after a new block was mined
    base_fee: Arc<RwLock<u64>>,
    /// Whether the minimum suggested priority fee is enforced
    is_min_priority_fee_enforced: bool,
    /// Tracks the excess blob gas, and the base fee, for the next block post Cancun
    ///
    /// This value will be updated after a new block was mined
    blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
    /// The base price to use Pre London
    ///
    /// This will be constant value unless changed manually
    gas_price: Arc<RwLock<u128>>,
    elasticity: Arc<RwLock<f64>>,
    /// Network-specific base fee params for EIP-1559 calculations
    base_fee_params: BaseFeeParams,
    /// The active Tempo hardfork, set only when running a Tempo chain.
    ///
    /// Tempo replaces EIP-1559: pre-T7 the base fee is fixed, and T7+ uses the TIP-1067 dynamic
    /// controller. It is updated once after a fork's hardfork is auto-detected.
    tempo_hardfork: Arc<RwLock<Option<TempoHardfork>>>,
}

impl FeeManager {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        spec_id: SpecId,
        base_fee: u64,
        is_min_priority_fee_enforced: bool,
        gas_price: u128,
        blob_excess_gas_and_price: BlobExcessGasAndPrice,
        blob_params: BlobParams,
        base_fee_params: BaseFeeParams,
        tempo_hardfork: Option<TempoHardfork>,
    ) -> Self {
        let elasticity = 1f64 / base_fee_params.elasticity_multiplier as f64;
        Self {
            spec_id,
            blob_params: Arc::new(RwLock::new(blob_params)),
            base_fee: Arc::new(RwLock::new(base_fee)),
            is_min_priority_fee_enforced,
            gas_price: Arc::new(RwLock::new(gas_price)),
            blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)),
            elasticity: Arc::new(RwLock::new(elasticity)),
            base_fee_params,
            tempo_hardfork: Arc::new(RwLock::new(tempo_hardfork)),
        }
    }

    /// Returns the active Tempo hardfork, if running a Tempo chain.
    pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
        *self.tempo_hardfork.read()
    }

    /// Sets the active Tempo hardfork, used after a fork's hardfork is auto-detected.
    pub fn set_tempo_hardfork(&self, hardfork: Option<TempoHardfork>) {
        *self.tempo_hardfork.write() = hardfork;
    }

    pub fn elasticity(&self) -> f64 {
        *self.elasticity.read()
    }

    /// Returns true for post London
    pub const fn is_eip1559(&self) -> bool {
        (self.spec_id as u8) >= (SpecId::LONDON as u8)
    }

    pub const fn is_eip4844(&self) -> bool {
        (self.spec_id as u8) >= (SpecId::CANCUN as u8)
    }

    /// Calculates the current blob gas price
    pub fn blob_gas_price(&self) -> u128 {
        if self.is_eip4844() { self.base_fee_per_blob_gas() } else { 0 }
    }

    pub fn base_fee(&self) -> u64 {
        if self.is_eip1559() { *self.base_fee.read() } else { 0 }
    }

    pub const fn is_min_priority_fee_enforced(&self) -> bool {
        self.is_min_priority_fee_enforced
    }

    /// Raw base gas price
    pub fn raw_gas_price(&self) -> u128 {
        *self.gas_price.read()
    }

    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
        self.is_eip4844().then(|| *self.blob_excess_gas_and_price.read())
    }

    pub fn base_fee_per_blob_gas(&self) -> u128 {
        if self.is_eip4844() { self.blob_excess_gas_and_price.read().blob_gasprice } else { 0 }
    }

    /// Returns the current gas price
    pub fn set_gas_price(&self, price: u128) {
        let mut gas = self.gas_price.write();
        *gas = price;
    }

    /// Returns the current base fee
    pub fn set_base_fee(&self, fee: u64) {
        trace!(target: "backend::fees", "updated base fee {:?}", fee);
        let mut base = self.base_fee.write();
        *base = fee;
    }

    /// Sets the current blob excess gas and price
    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
        let mut base = self.blob_excess_gas_and_price.write();
        *base = blob_excess_gas_and_price;
    }

    /// Calculates the base fee for the next block
    pub fn get_next_block_base_fee_per_gas(
        &self,
        gas_used: u64,
        gas_limit: u64,
        last_fee_per_gas: u64,
    ) -> u64 {
        // It's naturally impossible for base fee to be 0;
        // It means it was set by the user deliberately and therefore we treat it as a constant.
        // Therefore, we skip the base fee calculation altogether and we return 0.
        if self.base_fee() == 0 {
            return 0;
        }
        self.calculate_next_block_base_fee_per_gas(gas_used, gas_limit, last_fee_per_gas)
    }

    /// Calculates the next block base fee from the parent block without applying the configured
    /// zero-fee sentinel.
    pub(crate) fn calculate_next_block_base_fee_per_gas(
        &self,
        gas_used: u64,
        gas_limit: u64,
        last_fee_per_gas: u64,
    ) -> u64 {
        if !self.is_eip1559() {
            return 0;
        }
        // Tempo replaces EIP-1559 with its own hardfork-specific base fee rules.
        if let Some(hardfork) = self.tempo_hardfork() {
            return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
        }
        calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
    }

    /// Calculates the next block blob base fee.
    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
        self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
    }

    /// Calculates the next block blob excess gas, using the provided parent blob excess gas and
    /// parent blob gas used
    pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
        self.blob_params().next_block_excess_blob_gas_osaka(
            blob_excess_gas,
            blob_gas_used,
            self.base_fee(),
        )
    }

    /// Configures the blob params
    pub fn set_blob_params(&self, blob_params: BlobParams) {
        *self.blob_params.write() = blob_params;
    }

    /// Returns the active [`BlobParams`]
    pub fn blob_params(&self) -> BlobParams {
        *self.blob_params.read()
    }
}

/// Computes the next block's base fee for a Tempo chain.
///
/// - T7+: the TIP-1067 dynamic controller, an EIP-1559 update against a fixed 10M gas target
///   clamped to `[floor, cap]`.
/// - Pre-T7: the fixed hardfork base fee (10 gwei pre-T1, 20 gwei T1+).
fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
    if hardfork.is_t7() {
        return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
    }
    crate::config::tempo_default_base_fee(hardfork)
}

/// An async service that takes care of the `FeeHistory` cache
pub struct FeeHistoryService<N: Network>
where
    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
{
    /// blob parameters for the current spec
    blob_params: BlobParams,
    /// incoming notifications about new blocks
    new_blocks: ChainNotifications,
    /// contains all fee history related entries
    cache: FeeHistoryCache,
    /// number of items to consider
    fee_history_limit: u64,
    /// a type that can fetch ethereum-storage data
    storage_info: StorageInfo<N>,
}

impl<N: Network> FeeHistoryService<N>
where
    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
{
    pub const fn new(
        blob_params: BlobParams,
        new_blocks: ChainNotifications,
        cache: FeeHistoryCache,
        storage_info: StorageInfo<N>,
    ) -> Self {
        Self {
            blob_params,
            new_blocks,
            cache,
            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
            storage_info,
        }
    }

    /// Returns the configured history limit
    pub const fn fee_history_limit(&self) -> u64 {
        self.fee_history_limit
    }

    /// Inserts a new cache entry for the given block
    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
        let (result, block_number) = self.create_cache_entry(hash, header);
        self.insert_cache_entry(result, block_number);
    }

    /// Create a new history entry for the block
    fn create_cache_entry(
        &self,
        hash: B256,
        header: &impl BlockHeader,
    ) -> (FeeHistoryCacheItem, Option<u64>) {
        create_fee_history_cache_item(hash, header, &self.storage_info, self.blob_params)
    }

    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
        insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
    }
}

/// Inserts an entry into the fee history cache and trims it back to `fee_history_limit`.
///
/// Used by the async [`FeeHistoryService`]. The `eth_feeHistory` fallback applies the same bounded
/// insertion policy to a batch under one lock.
pub(crate) fn insert_fee_history_cache_item(
    cache: &FeeHistoryCache,
    item: FeeHistoryCacheItem,
    block_number: Option<u64>,
    fee_history_limit: u64,
) {
    if let Some(block_number) = block_number {
        trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
        let mut cache = cache.lock();
        cache.insert(block_number, item);

        // Trim to the cache limit by dropping the oldest entries (smallest block numbers).
        // `pop_first` is saturating and correct regardless of insertion order, unlike the
        // previous index math which could underflow when the `eth_feeHistory` fallback inserts
        // entries out of order.
        while cache.len() as u64 > fee_history_limit {
            cache.pop_first();
        }
    }
}

/// Percentile list from 0.0 to 100.0 with a 0.5 resolution (201 points).
///
/// Constant across blocks, so it is computed once instead of being rebuilt on every
/// `create_fee_history_cache_item` call.
static REWARD_PERCENTILES: LazyLock<Vec<f64>> = LazyLock::new(|| {
    let mut percentile: f64 = 0.0;
    (0..=200)
        .map(|_| {
            let val = percentile;
            percentile += 0.5;
            val
        })
        .collect()
});

/// Builds the [`FeeHistoryCacheItem`] for a single block.
///
/// Shared by the async [`FeeHistoryService`] and by `eth_feeHistory` itself: the service can lag
/// the chain head (it only runs when the node task is polled), so the RPC handler computes any
/// missing entry on demand with the same logic instead of returning a short response.
pub(crate) fn create_fee_history_cache_item<N: Network>(
    hash: B256,
    header: &impl BlockHeader,
    storage_info: &StorageInfo<N>,
    blob_params: BlobParams,
) -> (FeeHistoryCacheItem, Option<u64>)
where
    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
{
    let mut block_number: Option<u64> = None;
    let base_fee = header.base_fee_per_gas().unwrap_or_default();
    let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
    let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
    let base_fee_per_blob_gas = header.blob_fee(blob_params);

    let mut item = FeeHistoryCacheItem {
        block_hash: hash,
        base_fee: base_fee as u128,
        gas_used_ratio: 0f64,
        blob_gas_used_ratio: 0f64,
        rewards: Vec::new(),
        excess_blob_gas,
        base_fee_per_blob_gas,
        blob_gas_used,
    };

    let current_block = storage_info.block(hash);
    let current_receipts = storage_info.receipts(hash);

    if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
        block_number = Some(block.header.number());

        let gas_used = block.header.gas_used() as f64;
        let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
        item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
        item.blob_gas_used_ratio = blob_gas_used
            .map(|g| {
                let max = blob_params.max_blob_gas_per_block() as f64;
                if max == 0.0 { 0.0 } else { g / max }
            })
            .unwrap_or(0.0);

        // extract useful tx info (gas_used, effective_reward)
        let mut transactions: Vec<(_, _)> = receipts
            .iter()
            .enumerate()
            .map(|(i, receipt)| {
                let cumulative = receipt.cumulative_gas_used();
                let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
                let gas_used = cumulative - prev_cumulative;
                let effective_reward = block
                    .body
                    .transactions
                    .get(i)
                    .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
                    .unwrap_or(0);

                (gas_used, effective_reward)
            })
            .collect();

        // sort by effective reward asc
        transactions.sort_by_key(|(_, reward)| *reward);

        // calculate percentile rewards
        item.rewards = REWARD_PERCENTILES
            .iter()
            .filter_map(|&p| {
                let target_gas = (p * gas_used / 100f64) as u64;
                let mut sum_gas = 0;
                for (gas_used, effective_reward) in transactions.iter().copied() {
                    sum_gas += gas_used;
                    if target_gas <= sum_gas {
                        return Some(effective_reward);
                    }
                }
                None
            })
            .collect();
    } else {
        item.rewards = vec![0; REWARD_PERCENTILES.len()];
    }
    (item, block_number)
}

// An endless future that listens for new blocks and updates the cache
impl<N: Network> Future for FeeHistoryService<N>
where
    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
{
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let pin = self.get_mut();

        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
            // add the imported block.
            if let Some(block) = notification.as_new_block() {
                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
            }
        }

        Poll::Pending
    }
}

pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;

/// A single item in the whole fee history cache
#[derive(Clone, Debug)]
pub struct FeeHistoryCacheItem {
    pub block_hash: B256,
    pub base_fee: u128,
    pub gas_used_ratio: f64,
    pub base_fee_per_blob_gas: Option<u128>,
    pub blob_gas_used_ratio: f64,
    pub excess_blob_gas: Option<u128>,
    pub blob_gas_used: Option<u128>,
    pub rewards: Vec<u128>,
}

#[derive(Clone, Default)]
pub struct FeeDetails {
    pub gas_price: Option<u128>,
    pub max_fee_per_gas: Option<u128>,
    pub max_priority_fee_per_gas: Option<u128>,
    pub max_fee_per_blob_gas: Option<u128>,
}

impl FeeDetails {
    /// All values zero
    pub const fn zero() -> Self {
        Self {
            gas_price: Some(0),
            max_fee_per_gas: Some(0),
            max_priority_fee_per_gas: Some(0),
            max_fee_per_blob_gas: None,
        }
    }

    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
    pub const fn or_zero_fees(self) -> Self {
        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
            self;

        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
        let gas_price = if no_fees { Some(0) } else { gas_price };
        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
        let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };

        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
    }

    /// Turns this type into a tuple
    pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
            self;
        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
    }

    /// Creates a new instance from the request's gas related values
    pub fn new(
        request_gas_price: Option<u128>,
        request_max_fee: Option<u128>,
        request_priority: Option<u128>,
        max_fee_per_blob_gas: Option<u128>,
    ) -> Result<Self, BlockchainError> {
        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
            (gas_price, None, None, None) => {
                // Legacy request, all default to gas price.
                Ok(Self {
                    gas_price,
                    max_fee_per_gas: gas_price,
                    max_priority_fee_per_gas: gas_price,
                    max_fee_per_blob_gas: None,
                })
            }
            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
                // eip-1559
                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
                if let Some(max_priority) = max_priority {
                    let max_fee = max_fee.unwrap_or_default();
                    if max_priority > max_fee {
                        return Err(BlockchainError::InvalidFeeInput);
                    }
                }
                Ok(Self {
                    gas_price: max_fee,
                    max_fee_per_gas: max_fee,
                    max_priority_fee_per_gas: max_priority,
                    max_fee_per_blob_gas,
                })
            }
        }
    }
}

impl fmt::Debug for FeeDetails {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "Fees {{ ")?;
        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
        write!(fmt, "}}")?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fee_manager(spec_id: SpecId) -> FeeManager {
        FeeManager::new(
            spec_id,
            INITIAL_BASE_FEE,
            true,
            INITIAL_GAS_PRICE,
            BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
            BlobParams::cancun(),
            BaseFeeParams::ethereum(),
            None,
        )
    }

    #[test]
    fn raw_next_base_fee_respects_london_activation() {
        let berlin = fee_manager(SpecId::BERLIN);
        assert_eq!(
            berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
            0
        );

        let london = fee_manager(SpecId::LONDON);
        assert_ne!(
            london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
            0
        );
    }
}
