use crate::{
    VersionedBlockPayload,
    VersionedCompressedBlock,
    config::Config,
    ports::{
        HistoryLookup,
        TemporalRegistry,
    },
    registry::TemporalRegistryAll,
};
use fuel_core_types::{
    blockchain::block::PartialFuelBlock,
    fuel_compression::{
        Compressible,
        ContextError,
        Decompress,
        DecompressibleBy,
        RegistryKey,
    },
    fuel_tx::{
        AssetId,
        CompressedUtxoId,
        Mint,
        ScriptCode,
        Transaction,
        TxPointer as FuelTxPointer,
        UtxoId,
        field::TxPointer,
        input::{
            AsField,
            PredicateCode,
            coin::{
                Coin,
                CoinSpecification,
            },
            message::{
                Message,
                MessageSpecification,
            },
        },
    },
    fuel_types::{
        Address,
        ContractId,
    },
    tai64::Tai64,
};

#[cfg(not(feature = "fault-proving"))]
pub mod not_fault_proving {
    use super::*;
    pub trait DecompressDb: TemporalRegistryAll + HistoryLookup {}
    impl<T> DecompressDb for T where T: TemporalRegistryAll + HistoryLookup {}
}

#[cfg(feature = "fault-proving")]
pub mod fault_proving {
    use super::*;
    use crate::ports::GetRegistryRoot;
    pub trait DecompressDb:
        TemporalRegistryAll + HistoryLookup + GetRegistryRoot
    {
    }
    impl<T> DecompressDb for T where T: TemporalRegistryAll + HistoryLookup + GetRegistryRoot {}
}

#[cfg(feature = "fault-proving")]
use fault_proving::DecompressDb;
use fuel_core_types::fuel_types::bytes::Bytes;
#[cfg(not(feature = "fault-proving"))]
use not_fault_proving::DecompressDb;

/// This must be called for all decompressed blocks in sequence, otherwise the result will be garbage.
pub async fn decompress<D>(
    config: Config,
    mut db: D,
    block: VersionedCompressedBlock,
) -> anyhow::Result<PartialFuelBlock>
where
    D: DecompressDb,
{
    block
        .registrations()
        .write_to_registry(&mut db, block.consensus_header().time)?;

    let ctx = DecompressCtx {
        config,
        timestamp: block.consensus_header().time,
        db,
    };

    let mut transactions = <Vec<Transaction> as DecompressibleBy<_>>::decompress_with(
        block.transactions(),
        &ctx,
    )
    .await?;

    let transaction_count = transactions.len();

    // patch mint transaction
    let mint_tx = transactions
        .last_mut()
        .ok_or_else(|| anyhow::anyhow!("No transactions"))?;
    if let Transaction::Mint(mint) = mint_tx {
        let tx_pointer = mint.tx_pointer_mut();
        *tx_pointer = FuelTxPointer::new(
            block.consensus_header().height,
            #[allow(clippy::arithmetic_side_effects)]
            u16::try_from(transaction_count - 1)?,
        );
    } else {
        anyhow::bail!("Last transaction is not a mint");
    }

    #[cfg(feature = "fault-proving")]
    {
        match block {
            VersionedCompressedBlock::V0(_) => {}
            VersionedCompressedBlock::V1(ref block) => {
                let registry_root_after_decompression = ctx
                    .db
                    .registry_root()
                    .map_err(|e| anyhow::anyhow!("Failed to get registry root: {}", e))?;
                let registry_root_after_compression = block.header.registry_root;
                if registry_root_after_decompression != registry_root_after_compression {
                    anyhow::bail!(
                        "Registry root mismatch. registry root after decompression: {:?}, registry root after compression: {:?}",
                        registry_root_after_decompression,
                        registry_root_after_compression
                    );
                }
            }
        }
    }

    Ok(PartialFuelBlock {
        header: block.partial_block_header(),
        transactions,
    })
}

pub struct DecompressCtx<D> {
    pub config: Config,
    /// Timestamp of the block being decompressed
    pub timestamp: Tai64,
    pub db: D,
}

impl<D> ContextError for DecompressCtx<D> {
    type Error = anyhow::Error;
}

impl<D> DecompressibleBy<DecompressCtx<D>> for UtxoId
where
    D: HistoryLookup,
{
    async fn decompress_with(
        c: CompressedUtxoId,
        ctx: &DecompressCtx<D>,
    ) -> anyhow::Result<Self> {
        ctx.db.utxo_id(c)
    }
}

macro_rules! decompress_impl {
    ($($type:ty),*) => { paste::paste! {
        $(
            impl<D> DecompressibleBy<DecompressCtx<D>> for $type
            where
                D: TemporalRegistry<$type>
            {
                async fn decompress_with(
                    key: RegistryKey,
                    ctx: &DecompressCtx<D>,
                ) -> anyhow::Result<Self> {
                    if key == RegistryKey::DEFAULT_VALUE {
                        return Ok(<$type>::default());
                    }
                    let key_timestamp = ctx.db.read_timestamp(&key)?;
                    if !ctx.config.is_timestamp_accessible(ctx.timestamp, key_timestamp)? {
                        anyhow::bail!("Timestamp not accessible");
                    }
                    ctx.db.read_registry(&key)
                }
            }
        )*
    }};
}

decompress_impl!(AssetId, ContractId, Address, PredicateCode, ScriptCode);

impl<D, Specification> DecompressibleBy<DecompressCtx<D>> for Coin<Specification>
where
    D: DecompressDb,
    Specification: CoinSpecification,
    Specification::Predicate: DecompressibleBy<DecompressCtx<D>>,
    Specification::PredicateData: DecompressibleBy<DecompressCtx<D>>,
    Specification::PredicateGasUsed: DecompressibleBy<DecompressCtx<D>>,
    Specification::Witness: DecompressibleBy<DecompressCtx<D>>,
{
    async fn decompress_with(
        c: <Coin<Specification> as Compressible>::Compressed,
        ctx: &DecompressCtx<D>,
    ) -> anyhow::Result<Coin<Specification>> {
        let utxo_id = UtxoId::decompress_with(c.utxo_id, ctx).await?;
        let coin_info = ctx.db.coin(utxo_id)?;
        let witness_index = c.witness_index.decompress(ctx).await?;
        let predicate_gas_used = c.predicate_gas_used.decompress(ctx).await?;
        let predicate = c.predicate.decompress(ctx).await?;
        let predicate_data = c.predicate_data.decompress(ctx).await?;
        Ok(Self {
            utxo_id,
            owner: coin_info.owner,
            amount: coin_info.amount,
            asset_id: coin_info.asset_id,
            tx_pointer: Default::default(),
            witness_index,
            predicate_gas_used,
            predicate,
            predicate_data,
        })
    }
}

impl<D, Specification> DecompressibleBy<DecompressCtx<D>> for Message<Specification>
where
    D: DecompressDb,
    Specification: MessageSpecification,
    Specification::Data: DecompressibleBy<DecompressCtx<D>> + Default,
    Specification::Predicate: DecompressibleBy<DecompressCtx<D>>,
    Specification::PredicateData: DecompressibleBy<DecompressCtx<D>>,
    Specification::PredicateGasUsed: DecompressibleBy<DecompressCtx<D>>,
    Specification::Witness: DecompressibleBy<DecompressCtx<D>>,
{
    async fn decompress_with(
        c: <Message<Specification> as Compressible>::Compressed,
        ctx: &DecompressCtx<D>,
    ) -> anyhow::Result<Message<Specification>> {
        let msg = ctx.db.message(c.nonce)?;
        let witness_index = c.witness_index.decompress(ctx).await?;
        let predicate_gas_used = c.predicate_gas_used.decompress(ctx).await?;
        let predicate = c.predicate.decompress(ctx).await?;
        let predicate_data = c.predicate_data.decompress(ctx).await?;
        let mut message: Message<Specification> = Message {
            sender: msg.sender,
            recipient: msg.recipient,
            amount: msg.amount,
            nonce: c.nonce,
            witness_index,
            predicate_gas_used,
            data: Default::default(),
            predicate,
            predicate_data,
        };

        if let Some(data) = message.data.as_mut_field() {
            *data = Bytes::new(msg.data.clone());
        }

        Ok(message)
    }
}

impl<D> DecompressibleBy<DecompressCtx<D>> for Mint
where
    D: DecompressDb,
{
    async fn decompress_with(
        c: Self::Compressed,
        ctx: &DecompressCtx<D>,
    ) -> anyhow::Result<Self> {
        Ok(Transaction::mint(
            Default::default(), // TODO: what should we do with this?
            c.input_contract.decompress(ctx).await?,
            c.output_contract.decompress(ctx).await?,
            c.mint_amount.decompress(ctx).await?,
            c.mint_asset_id.decompress(ctx).await?,
            c.gas_price.decompress(ctx).await?,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{
        Deserialize,
        Serialize,
    };

    #[tokio::test]
    async fn decompress_block_with_unknown_version() {
        #[derive(Clone, Serialize, Deserialize)]
        #[allow(clippy::large_enum_variant)]
        enum CompressedBlockWithNewVersions {
            V0(crate::CompressedBlockPayloadV0),
            NewVersion(u32),
            #[serde(untagged)]
            Unknown,
        }

        // Given
        let bad_block =
            postcard::to_stdvec(&CompressedBlockWithNewVersions::NewVersion(1234))
                .unwrap();

        // When
        let result: Result<VersionedCompressedBlock, _> =
            postcard::from_bytes(&bad_block);

        // Then
        let _ =
            result.expect_err("should fail to deserialize because of unknown version");
    }
}
