use std::{marker::PhantomData, sync::Arc};
use log::error;
use lru::LruCache;
use sc_client_api::HeaderBackend;
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
use crate::{
    phron_primitives::{BlockHash, BlockNumber},
    data_io::ChainInfoCacheConfig,
    BlockId,
};
pub trait ChainInfoProvider: Send + Sync + 'static {
    fn is_block_imported(&mut self, block: &BlockId) -> bool;
    fn get_finalized_at(&mut self, number: BlockNumber) -> Result<BlockId, ()>;
    fn get_parent_hash(&mut self, block: &BlockId) -> Result<BlockHash, ()>;
    fn get_highest_finalized(&mut self) -> BlockId;
}
pub struct SubstrateChainInfoProvider<B, C>
where
    B: BlockT<Hash = BlockHash>,
    B::Header: HeaderT<Number = BlockNumber>,
    C: HeaderBackend<B> + 'static,
{
    client: Arc<C>,
    _phantom: PhantomData<B>,
}
impl<B, C> SubstrateChainInfoProvider<B, C>
where
    B: BlockT<Hash = BlockHash>,
    B::Header: HeaderT<Number = BlockNumber>,
    C: HeaderBackend<B>,
{
    pub fn new(client: Arc<C>) -> Self {
        SubstrateChainInfoProvider {
            client,
            _phantom: PhantomData,
        }
    }
}
impl<B, C> ChainInfoProvider for SubstrateChainInfoProvider<B, C>
where
    B: BlockT<Hash = BlockHash>,
    B::Header: HeaderT<Number = BlockNumber>,
    C: HeaderBackend<B>,
{
    fn is_block_imported(&mut self, block: &BlockId) -> bool {
        let maybe_header = self
            .client
            .header(block.hash)
            .expect("client must answer a query");
        if let Some(header) = maybe_header {
            return *header.number() == block.number;
        }
        false
    }
    fn get_finalized_at(&mut self, num: BlockNumber) -> Result<BlockId, ()> {
        if self.client.info().finalized_number < num {
            return Err(());
        }
        let block_hash = match self.client.hash(num).ok().flatten() {
            None => {
                error!(target: "chain-info", "Could not get hash for block #{:?}", num);
                return Err(());
            }
            Some(h) => h,
        };
        if let Some(header) = self.client.header(block_hash).expect("client must respond") {
            Ok((header.hash(), num).into())
        } else {
            Err(())
        }
    }
    fn get_parent_hash(&mut self, block: &BlockId) -> Result<BlockHash, ()> {
        if let Some(header) = self.client.header(block.hash).expect("client must respond") {
            Ok(*header.parent_hash())
        } else {
            Err(())
        }
    }
    fn get_highest_finalized(&mut self) -> BlockId {
        let status = self.client.info();
        (status.finalized_hash, status.finalized_number).into()
    }
}
pub struct CachedChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    available_block_with_parent_cache: LruCache<BlockId, BlockHash>,
    available_blocks_cache: LruCache<BlockId, ()>,
    finalized_cache: LruCache<BlockNumber, BlockHash>,
    chain_info_provider: CIP,
}
impl<CIP> CachedChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    pub fn new(chain_info_provider: CIP, config: ChainInfoCacheConfig) -> Self {
        CachedChainInfoProvider {
            available_block_with_parent_cache: LruCache::new(config.block_cache_capacity),
            available_blocks_cache: LruCache::new(config.block_cache_capacity),
            finalized_cache: LruCache::new(config.block_cache_capacity),
            chain_info_provider,
        }
    }
    pub fn inner(&mut self) -> &mut CIP {
        &mut self.chain_info_provider
    }
}
impl<CIP> ChainInfoProvider for CachedChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    fn is_block_imported(&mut self, block: &BlockId) -> bool {
        if self.available_blocks_cache.contains(block) {
            return true;
        }
        if self.chain_info_provider.is_block_imported(block) {
            self.available_blocks_cache.put(block.clone(), ());
            return true;
        }
        false
    }
    fn get_finalized_at(&mut self, num: BlockNumber) -> Result<BlockId, ()> {
        if let Some(hash) = self.finalized_cache.get(&num) {
            return Ok((*hash, num).into());
        }
        if self.get_highest_finalized().number < num {
            return Err(());
        }
        if let Ok(block) = self.chain_info_provider.get_finalized_at(num) {
            self.finalized_cache.put(num, block.hash);
            return Ok(block);
        }
        Err(())
    }
    fn get_parent_hash(&mut self, block: &BlockId) -> Result<BlockHash, ()> {
        if let Some(parent) = self.available_block_with_parent_cache.get(block) {
            return Ok(*parent);
        }
        if let Ok(parent) = self.chain_info_provider.get_parent_hash(block) {
            self.available_block_with_parent_cache
                .put(block.clone(), parent);
            return Ok(parent);
        }
        Err(())
    }
    fn get_highest_finalized(&mut self) -> BlockId {
        self.chain_info_provider.get_highest_finalized()
    }
}
pub struct AuxFinalizationChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    aux_finalized: BlockId,
    chain_info_provider: CIP,
}
impl<CIP> AuxFinalizationChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    pub fn new(chain_info_provider: CIP, aux_finalized: BlockId) -> Self {
        AuxFinalizationChainInfoProvider {
            aux_finalized,
            chain_info_provider,
        }
    }
    pub fn update_aux_finalized(&mut self, aux_finalized: BlockId) {
        self.aux_finalized = aux_finalized;
    }
}
impl<CIP> ChainInfoProvider for AuxFinalizationChainInfoProvider<CIP>
where
    CIP: ChainInfoProvider,
{
    fn is_block_imported(&mut self, block: &BlockId) -> bool {
        self.chain_info_provider.is_block_imported(block)
    }
    fn get_finalized_at(&mut self, num: BlockNumber) -> Result<BlockId, ()> {
        let highest_finalized_inner = self.chain_info_provider.get_highest_finalized();
        if num <= highest_finalized_inner.number {
            return self.chain_info_provider.get_finalized_at(num);
        }
        if num > self.aux_finalized.number {
            return Err(());
        }
        let mut curr_block = self.aux_finalized.clone();
        while curr_block.number > num {
            let parent_hash = self.chain_info_provider.get_parent_hash(&curr_block)?;
            curr_block = (parent_hash, curr_block.number - 1).into();
        }
        Ok(curr_block)
    }
    fn get_parent_hash(&mut self, block: &BlockId) -> Result<BlockHash, ()> {
        self.chain_info_provider.get_parent_hash(block)
    }
    fn get_highest_finalized(&mut self) -> BlockId {
        let highest_finalized_inner = self.chain_info_provider.get_highest_finalized();
        if self.aux_finalized.number > highest_finalized_inner.number {
            self.aux_finalized.clone()
        } else {
            highest_finalized_inner
        }
    }
}