use sp_runtime::{traits::UniqueSaturatedInto, SaturatedConversion, Saturating};
use sp_staking::SessionIndex;

use crate::{
    deserialisations::{
        de_string_to_block_number, de_string_to_block_number_pure, de_string_to_bytes,
        de_string_to_h256, de_string_to_vec_of_bytes,
    },
    AuthIndex, BalanceOf, BlockCommitment, BlockCommitments, BlockNumberFor, Call, Clap, Config,
    Decode, Deserialize, Encode, ExternalBlockNumber, NetworkIdOf, RuntimeAppPublic, RuntimeDebug,
    SubmitTransaction, Vec, BLOCK_COMMITMENT_DELAY, H256, LOG_TARGET,
};

const NUMBER_OF_TOPICS: usize = 3;

#[derive(RuntimeDebug, Clone, PartialEq, Deserialize, Encode, Decode)]
pub struct EvmResponse {
    #[serde(default)]
    id: Option<u32>,
    #[serde(default, deserialize_with = "de_string_to_bytes")]
    jsonrpc: Option<Vec<u8>>,
    #[serde(default, deserialize_with = "de_string_to_bytes")]
    pub error: Option<Vec<u8>>,
    #[serde(default)]
    pub result: Option<EvmResponseType>,
}

#[derive(RuntimeDebug, Clone, PartialEq, Deserialize, Encode, Decode)]
#[serde(untagged)]
pub enum EvmResponseType {
    #[serde(deserialize_with = "de_string_to_block_number_pure")]
    BlockNumber(ExternalBlockNumber),
    TransactionLogs(Vec<Log>),
}

#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Log {
    #[serde(default, deserialize_with = "de_string_to_h256")]
    pub transaction_hash: Option<H256>,
    #[serde(default, deserialize_with = "de_string_to_block_number")]
    pub block_number: Option<ExternalBlockNumber>,
    #[serde(default, deserialize_with = "de_string_to_vec_of_bytes")]
    pub topics: Vec<Vec<u8>>,
    pub removed: bool,
}

impl EvmResponseType {
    fn prepare_clap<T: Config>(
        &self,
        authority_index: AuthIndex,
        session_index: SessionIndex,
        network_id: NetworkIdOf<T>,
        log: &Log,
    ) -> Clap<T::AccountId, NetworkIdOf<T>, BalanceOf<T>> {
        Clap {
            authority_index,
            session_index,
            network_id,
            removed: log.removed,
            receiver: T::AccountId::decode(&mut &log.topics[1][0..32])
                .expect("32 bytes always construct an AccountId32"),
            amount: u128::from_be_bytes(
                log.topics[2][16..32]
                    .try_into()
                    .expect("amount is valid hex; qed"),
            )
            .saturated_into::<BalanceOf<T>>(),
            transaction_hash: log.transaction_hash.clone().expect("tx hash exists; qed"),
            block_number: log.block_number.expect("block number exists; qed"),
        }
    }

    fn iter_claps_from_logs<T: Config>(
        &self,
        authority_index: AuthIndex,
        session_index: SessionIndex,
        network_id: NetworkIdOf<T>,
    ) -> Vec<Clap<T::AccountId, NetworkIdOf<T>, BalanceOf<T>>> {
        match self {
            EvmResponseType::TransactionLogs(evm_logs) => evm_logs
                .iter()
                .filter_map(move |log| {
                    log.is_sufficient().then(|| {
                        self.prepare_clap::<T>(authority_index, session_index, network_id, log)
                    })
                })
                .collect(),
            EvmResponseType::BlockNumber(_) => Vec::new(),
        }
    }

    fn sign_and_submit_claps<T: Config>(
        &self,
        authority_index: AuthIndex,
        authority_key: T::AuthorityId,
        session_index: SessionIndex,
        network_id: NetworkIdOf<T>,
    ) {
        let claps = self.iter_claps_from_logs::<T>(authority_index, session_index, network_id);
        let claps_len = claps.len();

        log::info!(
            target: LOG_TARGET,
            "👻 Found {:?} claps for network {:?}",
            claps_len,
            network_id,
        );

        for (clap_index, clap) in claps.iter().enumerate() {
            let signature = match authority_key.sign(&clap.encode()) {
                Some(signature) => signature,
                None => {
                    log::info!(
                        target: LOG_TARGET,
                        "👻 Clap #{} signing failed from authority #{:?} for network {:?}",
                        clap_index,
                        authority_index,
                        network_id,
                    );
                    return;
                }
            };

            let call = Call::slow_clap {
                clap: clap.clone(),
                signature,
            };

            if let Err(e) =
                SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
            {
                log::info!(
                    target: LOG_TARGET,
                    "👻 Failed to submit clap #{} from authority #{:?} for network {:?}: {:?}",
                    clap_index,
                    authority_index,
                    network_id,
                    e,
                );
            }
        }
    }

    fn sign_and_submit_block_commitment<T: Config>(
        &self,
        block_now: BlockNumberFor<T>,
        from_block: ExternalBlockNumber,
        authority_index: AuthIndex,
        authority_key: T::AuthorityId,
        session_index: SessionIndex,
        network_id: NetworkIdOf<T>,
    ) {
        let block_commitment = BlockCommitment {
            session_index,
            authority_index,
            network_id,
            last_stored_block: from_block,
        };

        let stored_last_updated = BlockCommitments::<T>::get(&network_id)
            .get(&authority_index)
            .map(|details| {
                details
                    .last_updated
                    .saturating_add(BLOCK_COMMITMENT_DELAY.unique_saturated_into())
            })
            .unwrap_or_default();

        if block_now < stored_last_updated {
            return;
        }

        log::info!(
            target: LOG_TARGET,
            "👻 New block commitment from authority #{:?} for network {:?}",
            authority_index,
            network_id,
        );

        let signature = match authority_key.sign(&block_commitment.encode()) {
            Some(signature) => signature,
            None => {
                log::info!(
                    target: LOG_TARGET,
                    "👻 Block commitment signing failed from authority #{:?} for network {:?}",
                    authority_index,
                    network_id,
                );
                return;
            }
        };

        let call = Call::commit_block {
            block_commitment,
            signature,
        };

        if let Err(e) = SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
            log::info!(
                target: LOG_TARGET,
                "👻 Failed to submit block commitment from authority #{:?} for network {:?}: {:?}",
                authority_index,
                network_id,
                e,
            );
        }
    }

    pub fn sign_and_submit<T: Config>(
        &self,
        block_now: BlockNumberFor<T>,
        from_block: ExternalBlockNumber,
        authority_index: AuthIndex,
        authority_key: T::AuthorityId,
        session_index: SessionIndex,
        network_id: NetworkIdOf<T>,
    ) {
        match self {
            EvmResponseType::TransactionLogs(_) => self.sign_and_submit_claps::<T>(
                authority_index,
                authority_key,
                session_index,
                network_id,
            ),
            EvmResponseType::BlockNumber(_) => self.sign_and_submit_block_commitment::<T>(
                block_now,
                from_block,
                authority_index,
                authority_key,
                session_index,
                network_id,
            ),
        }
    }
}

impl Log {
    pub fn is_sufficient(&self) -> bool {
        self.transaction_hash.is_some()
            && self.block_number.is_some()
            && self.topics.len() == NUMBER_OF_TOPICS
    }
}
