use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{pallet_prelude::ConstU32, BoundedVec};
use scale_info::TypeInfo;
use sp_core::{H256, U256};
use sp_std::vec::Vec;

use sp_runtime::{
    traits::{AtLeast32BitUnsigned, Saturating, UniqueSaturatedInto},
    AccountId32, RuntimeDebug,
};

use ghost_helpers::{
    hash_chain::{cumulative_hash, sequential_hash, verify_hash_ancestry},
    merkle_tree::verify_tree_proof,
    SubstrateBlake2Hasher, SubstrateKeccakHasher, UtxoSha2Hasher,
};
use ghost_traits::hashing::GhostHasher;

use crate::{ExternalBlockNumber, ThreadId, WeavingSession};

const EVM_MERKLE_DEPTH: u32 = 8;
const EVM_VALUES_DEPTH: u32 = 8;

const UTXO_MAX_PROOF_BYTES: u32 = 1024;
const UTXO_MAX_HEADER_BYTES: u32 = 80;
const UTXO_MAX_HISTORY_DEPTH: u32 = 144;
const UTXO_MAX_TX_BYTES: u32 = ghost_helpers::utxo::MAX_TX_LEN as u32;

pub enum WeavingOk<NetworkId> {
    Block(ExternalBlockNumber, NetworkId),
    Hash(ExternalBlockNumber, H256, WeavingSession, NetworkId),
}

impl<NetworkId> core::fmt::Display for WeavingOk<NetworkId>
where
    NetworkId: core::fmt::Debug,
{
    fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
        match self {
            Self::Block(block, network_id) => write!(fmt, "Verified block height #{:?} found for network: {:?}.", block, network_id),
            Self::Hash(block, hash, next_session, network_id) => write!(fmt, "Extracted Merkle Root {:?} where next session is {:?}; from block #{:?} for network: {:?}.", hash, next_session, block, network_id),
        }
    }
}

#[derive(
    Default,
    RuntimeDebug,
    Clone,
    Copy,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Encode,
    Decode,
    TypeInfo,
    MaxEncodedLen,
)]
pub struct WeavingState<AuthIndex> {
    pub hash: H256,
    pub count: AuthIndex,
    pub next_session: WeavingSession,
}

impl<AuthIndex> WeavingState<AuthIndex> {
    pub fn new(hash: H256, next_session: WeavingSession, count: AuthIndex) -> Self {
        Self {
            hash,
            next_session,
            count,
        }
    }
}

#[derive(
    Default,
    RuntimeDebug,
    Clone,
    Copy,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Encode,
    Decode,
    TypeInfo,
    MaxEncodedLen,
)]
pub struct AttestationContext<Block, NetworkId, AuthIndex> {
    block: Block,
    network_id: NetworkId,
    authority_index: AuthIndex,
    weaving_session: WeavingSession,
}

pub trait AttestationMetadata<Block, NetworkId, AuthIndex> {
    fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex>;

    fn block(&self) -> Block
    where
        Block: Copy,
    {
        self.context().block
    }

    fn network_id(&self) -> NetworkId
    where
        NetworkId: Copy,
    {
        self.context().network_id
    }

    fn authority_index(&self) -> AuthIndex
    where
        AuthIndex: Copy,
    {
        self.context().authority_index
    }

    fn weaving_session(&self) -> WeavingSession {
        self.context().weaving_session
    }
}

impl<Block, NetworkId, AuthIndex> AttestationContext<Block, NetworkId, AuthIndex> {
    pub fn with_current_block(mut self, block: Block) -> Self {
        self.block = block;
        self
    }

    pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
        self.network_id = network_id;
        self
    }

    pub fn with_authority_index(mut self, authority_index: AuthIndex) -> Self {
        self.authority_index = authority_index;
        self
    }

    pub fn with_weaving_session(mut self, weaving_session: WeavingSession) -> Self {
        self.weaving_session = weaving_session;
        self
    }

    pub fn build_block_attestation(
        self,
        external_block: ExternalBlockNumber,
    ) -> BlockAttestation<Block, NetworkId, AuthIndex> {
        BlockAttestation::<Block, NetworkId, AuthIndex> {
            context: self,
            external_block,
        }
    }

    pub fn build_hash_attestation(
        self,
        root_hash: H256,
        next_session: WeavingSession,
    ) -> HashAttestation<Block, NetworkId, AuthIndex> {
        HashAttestation::<Block, NetworkId, AuthIndex> {
            root_hash,
            next_session,
            context: self,
        }
    }
}

#[derive(
    RuntimeDebug,
    Clone,
    Copy,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Encode,
    Decode,
    TypeInfo,
    MaxEncodedLen,
)]
pub struct BlockAttestation<Block, NetworkId, AuthIndex> {
    context: AttestationContext<Block, NetworkId, AuthIndex>,
    pub external_block: ExternalBlockNumber,
}

#[derive(
    RuntimeDebug,
    Clone,
    Copy,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Encode,
    Decode,
    TypeInfo,
    MaxEncodedLen,
)]
pub struct HashAttestation<Block, NetworkId, AuthIndex> {
    context: AttestationContext<Block, NetworkId, AuthIndex>,
    pub next_session: WeavingSession,
    pub root_hash: H256,
}

impl<Block, NetworkId, AuthIndex> AttestationMetadata<Block, NetworkId, AuthIndex>
    for BlockAttestation<Block, NetworkId, AuthIndex>
{
    fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex> {
        &self.context
    }
}

impl<Block, NetworkId, AuthIndex> AttestationMetadata<Block, NetworkId, AuthIndex>
    for HashAttestation<Block, NetworkId, AuthIndex>
{
    fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex> {
        &self.context
    }
}

#[derive(Default, Copy, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct TapestryDraft<Block: AtLeast32BitUnsigned> {
    pub until: Block,
    pub median: ExternalBlockNumber,
}

#[derive(Default, Clone, Copy)]
pub struct TapestryDraftBuilder<Block, Delay>
where
    Block: Saturating + AtLeast32BitUnsigned + Copy,
    Delay: UniqueSaturatedInto<Block> + AtLeast32BitUnsigned + Copy,
{
    delay: Delay,
    block: Block,
    median: ExternalBlockNumber,
}

impl<Block, Delay> TapestryDraftBuilder<Block, Delay>
where
    Block: Saturating + AtLeast32BitUnsigned + Copy,
    Delay: UniqueSaturatedInto<Block> + AtLeast32BitUnsigned + Copy,
{
    pub fn with_block_delay(mut self, delay: Delay) -> Self {
        self.delay = delay;
        self
    }

    pub fn with_current_block(mut self, block: Block) -> Self {
        self.block = block;
        self
    }

    pub fn with_median_external(mut self, median: ExternalBlockNumber) -> Self {
        self.median = median;
        self
    }

    pub fn build(self) -> TapestryDraft<Block> {
        let delay_converted: Block = self.delay.unique_saturated_into();
        let until = self.block.saturating_add(delay_converted);
        TapestryDraft {
            until,
            median: self.median,
        }
    }
}

#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ThreadPayload<Balance: UniqueSaturatedInto<u128>> {
    proof_index: ThreadId,
    amount: Balance,
    receiver: AccountId32,
}

impl<Balance> ThreadPayload<Balance>
where
    Balance: UniqueSaturatedInto<u128>,
{
    pub fn new(proof_index: ThreadId, amount: Balance, receiver: AccountId32) -> Self {
        Self {
            proof_index,
            amount,
            receiver,
        }
    }
}

#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum ThreadProof<Balance: UniqueSaturatedInto<u128>> {
    EvmThreadProof(EvmThreadProof<Balance>),
    UtxoThreadProof(UtxoThreadProof<Balance>),
}

impl<Balance> ThreadProof<Balance>
where
    Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
    pub fn is_evm_proof_correct(&self) -> bool {
        match self {
            Self::EvmThreadProof(_) => true,
            Self::UtxoThreadProof(_) => false,
        }
    }

    pub fn is_utxo_proof_correct(&self) -> bool {
        match self {
            Self::UtxoThreadProof(_) => true,
            Self::EvmThreadProof(_) => false,
        }
    }

    pub fn amount(&self) -> Balance {
        match self {
            Self::UtxoThreadProof(utxo_thread) => utxo_thread.amount(),
            Self::EvmThreadProof(evm_thread) => evm_thread.amount(),
        }
    }

    pub fn proof_index(&self) -> ThreadId {
        match self {
            Self::UtxoThreadProof(utxo_thread) => utxo_thread.proof_index(),
            Self::EvmThreadProof(evm_thread) => evm_thread.proof_index(),
        }
    }

    pub fn verify_proof(&self, root_hash: H256, gatekeeper: &[u8]) -> Option<AccountId32> {
        match self {
            Self::UtxoThreadProof(utxo_thread) => utxo_thread.verify_proof(root_hash, gatekeeper),
            Self::EvmThreadProof(evm_thread) => evm_thread.verify_proof(root_hash),
        }
    }

    pub fn get_unique_key(&self, session: WeavingSession) -> H256 {
        match self {
            Self::EvmThreadProof(thread_info) => thread_info.get_unique_key(session),
            Self::UtxoThreadProof(utxo_thread) => utxo_thread.get_unique_key(),
        }
    }
}

#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct EvmThreadProof<Balance: UniqueSaturatedInto<u128>> {
    slot_index: ThreadId,
    payload: ThreadPayload<Balance>,
    proof: BoundedVec<H256, ConstU32<EVM_MERKLE_DEPTH>>,
    slots: BoundedVec<H256, ConstU32<EVM_VALUES_DEPTH>>,
}

impl<Balance> EvmThreadProof<Balance>
where
    Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
    pub fn amount(&self) -> Balance {
        self.payload.amount.clone()
    }
    pub fn proof_index(&self) -> ThreadId {
        self.payload.proof_index
    }

    pub fn verify_proof(&self, root_hash: H256) -> Option<AccountId32> {
        if EVM_MERKLE_DEPTH as usize != self.proof.len() {
            return None;
        }

        let proof_valid = verify_tree_proof::<SubstrateKeccakHasher, _>(
            &self.get_preimage_slice().to_fixed_bytes(),
            self.proof.as_ref(),
            root_hash,
            self.payload.proof_index,
        );

        if !proof_valid {
            return None;
        }

        Some(self.payload.receiver.clone())
    }

    fn get_preimage_slice(&self) -> H256 {
        let slot_index_usize: usize = self.slot_index.unique_saturated_into();

        (slot_index_usize < self.slots.len())
            .then(|| {
                self.slots
                    .iter()
                    .enumerate()
                    .map(|(index, &preimage)| {
                        if index == slot_index_usize {
                            self.compute_arguments_hash()
                        } else {
                            preimage
                        }
                    })
                    .collect::<Vec<_>>()
            })
            .map(|slots| cumulative_hash::<SubstrateKeccakHasher, _>(slots))
            .unwrap_or(SubstrateKeccakHasher::empty())
    }

    pub fn get_unique_key(&self, session: WeavingSession) -> H256 {
        let session_bytes = (session as u64).to_be_bytes();
        let slot_bytes = (self.slot_index as u64).to_be_bytes();
        let proof_bytes = (self.payload.proof_index as u64).to_be_bytes();

        let mut h1 = [0u8; 32]; h1[24..32].copy_from_slice(&session_bytes);
        let mut h2 = [0u8; 32]; h2[24..32].copy_from_slice(&slot_bytes);
        let mut h3 = [0u8; 32]; h3[24..32].copy_from_slice(&proof_bytes);

        let metadata: [H256; 3] = [H256::from(h1), H256::from(h2), H256::from(h3)];

        sequential_hash::<SubstrateBlake2Hasher, _>(metadata)
    }

    pub fn new(
        slot_index: ThreadId,
        payload: ThreadPayload<Balance>,
        proof_vec: Vec<H256>,
        slots_vec: Vec<H256>,
    ) -> Self {
        let proof = BoundedVec::<H256, ConstU32<EVM_MERKLE_DEPTH>>::try_from(proof_vec)
            .expect("Should be valid EVM proof");

        let slots = BoundedVec::<H256, ConstU32<EVM_MERKLE_DEPTH>>::try_from(slots_vec)
            .expect("Should be valud EVM proof");

        Self {
            slot_index,
            payload,
            proof,
            slots,
        }
    }

    fn compute_arguments_hash(&self) -> H256 {
        let amount_u128: u128 = self.amount().unique_saturated_into();
        let amount_u256 = U256::from(amount_u128);

        let slot_index_u64: u64 = self.slot_index.unique_saturated_into();
        let slot_index_u256 = U256::from(slot_index_u64);

        let mut padded_amount = [0u8; 32];
        amount_u256.to_big_endian(&mut padded_amount);

        let mut padded_slot_index = [0u8; 32];
        slot_index_u256.to_big_endian(&mut padded_slot_index);

        let inputs: [&[u8]; 3] = [
            padded_amount.as_ref(),
            self.payload.receiver.as_ref(),
            padded_slot_index.as_ref(),
        ];
        sequential_hash::<SubstrateKeccakHasher, _>(inputs)
    }
}

#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct UtxoThreadProof<Balance: UniqueSaturatedInto<u128>> {
    o_index: ThreadId,
    w_index: ThreadId,
    expected_amount: Balance,
    payload: ThreadPayload<Balance>,

    gettxproof_bytes: BoundedVec<u8, ConstU32<UTXO_MAX_PROOF_BYTES>>,
    getrawtransaction_bytes: BoundedVec<u8, ConstU32<UTXO_MAX_TX_BYTES>>,
    ancestry_headers: BoundedVec<
        BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>,
        ConstU32<UTXO_MAX_HISTORY_DEPTH>,
    >,
}

impl<Balance> UtxoThreadProof<Balance>
where
    Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
    pub fn amount(&self) -> Balance {
        self.expected_amount
    }
    pub fn receiver(&self) -> AccountId32 {
        self.payload.receiver.clone()
    }
    pub fn proof_index(&self) -> ThreadId {
        self.payload.proof_index
    }

    pub fn verify_proof(&self, root_hash: H256, gatekeeper_ref: &[u8]) -> Option<AccountId32> {
        use ghost_helpers::utxo::{Header, Transaction};

        if self.gettxproof_bytes.len() < 85 {
            return None;
        }
        let header_bytes = &self.gettxproof_bytes[..80];

        let is_chain_valid = verify_hash_ancestry::<UtxoSha2Hasher, _>(
            header_bytes,
            self.ancestry_headers.iter(),
            root_hash,
        );
        if !is_chain_valid {
            return None;
        }

        let transaction =
            Transaction::<UtxoSha2Hasher>::try_from(self.getrawtransaction_bytes.as_ref()).ok()?;
        let header = Header::<UtxoSha2Hasher>::try_from(self.gettxproof_bytes.as_ref()).ok()?;

        let txid_preimage = transaction.compute_txid()?;

        let root_hash = header.extract_proof(txid_preimage).ok()?;

        if root_hash != header.merkle_root {
            return None;
        }

        let account_id_bytes = transaction.validate(
            self.o_index,
            self.w_index,
            gatekeeper_ref,
            self.expected_amount,
        )?;

        Some(AccountId32::new(account_id_bytes))
    }

    pub fn get_unique_key(&self) -> H256 {
        let o_index_bytes = (self.o_index as u64).to_be_bytes();
        let mut o_index_raw = [0u8; 32];
        o_index_raw[24..32].copy_from_slice(&o_index_bytes);

        let o_index_hash = H256::from(o_index_raw);
        let metadata: &[&[u8]] = &[
            self.gettxproof_bytes.as_ref(),
            self.getrawtransaction_bytes.as_ref(),
            o_index_hash.as_ref(),
        ];

        sequential_hash::<SubstrateBlake2Hasher, _>(metadata)
    }

    pub fn new(
        o_index: ThreadId,
        w_index: ThreadId,
        expected_amount: Balance,
        payload: ThreadPayload<Balance>,
        gettxproof_vec: Vec<u8>,
        getrawtransaction_vec: Vec<u8>,
        ancestry_headers_vec: Vec<Vec<u8>>,
    ) -> Self {
        let gettxproof_bytes =
            BoundedVec::<u8, ConstU32<UTXO_MAX_PROOF_BYTES>>::try_from(gettxproof_vec)
                .expect("Should be valid UTXO gettxproof");

        let getrawtransaction_bytes =
            BoundedVec::<u8, ConstU32<UTXO_MAX_TX_BYTES>>::try_from(getrawtransaction_vec)
                .expect("Should be valid UTXO proof");

        let inner_bounded_headers = ancestry_headers_vec
            .into_iter()
            .map(|raw_header| {
                BoundedVec::<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>::try_from(raw_header)
                    .expect("Should be valid header")
            })
            .collect::<Vec<BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>>>();

        let ancestry_headers = BoundedVec::<
            BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>,
            ConstU32<UTXO_MAX_HISTORY_DEPTH>,
        >::try_from(inner_bounded_headers)
        .expect("Should be valud UTXO headers");

        Self {
            o_index,
            w_index,
            expected_amount,
            gettxproof_bytes,
            getrawtransaction_bytes,
            ancestry_headers,
            payload,
        }
    }
}
