#![cfg_attr(not(feature = "std"), no_std)]

use frame_support::{
    ensure,
    pallet_prelude::*,
    traits::{
        Currency, ExistenceRequirement, Get, RankedMembers, RankedMembersSwapHandler,
        VestingSchedule,
    },
    DefaultNoBound,
};
use frame_system::pallet_prelude::*;
pub use pallet::*;
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};

use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
use sp_runtime::traits::{BlockNumberProvider, CheckedDiv, CheckedSub};
use sp_std::prelude::*;

extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, string::String};

mod weights;
pub use crate::weights::WeightInfo;

mod benchmarking;
mod mock;
mod secp_utils;
mod tests;

/// An ethereum address (i.e. 20 bytes, used to represent an Ethereum account).
///
/// This gets serialized to the 0x-prefixed hex representation.
#[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, Default, RuntimeDebug, TypeInfo)]
pub struct EthereumAddress(pub [u8; 20]);

impl Serialize for EthereumAddress {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let hex: String = rustc_hex::ToHex::to_hex(&self.0[..]);
        serializer.serialize_str(&format!("0x{}", hex))
    }
}

impl<'de> Deserialize<'de> for EthereumAddress {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let base_string = String::deserialize(deserializer)?;
        let offset = if base_string.starts_with("0x") { 2 } else { 0 };
        let s = &base_string[offset..];
        if s.len() != 40 {
            Err(serde::de::Error::custom(
                "Bad length of Ethereum address (should be 42 including `0x`)",
            ))?;
        }
        let raw: Vec<u8> = rustc_hex::FromHex::from_hex(s)
            .map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?;
        let mut r = Self::default();
        r.0.copy_from_slice(&raw);
        Ok(r)
    }
}

#[derive(Encode, Decode, Clone, TypeInfo)]
pub struct EcdsaSignature(pub [u8; 65]);

impl PartialEq for EcdsaSignature {
    fn eq(&self, other: &Self) -> bool {
        &self.0[..] == &other.0[..]
    }
}

impl sp_std::fmt::Debug for EcdsaSignature {
    fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
        write!(f, "EcdsaSignature({:?})", &self.0[..])
    }
}

type CurrencyOf<T, I> = <<T as Config<I>>::VestingSchedule as VestingSchedule<
    <T as frame_system::Config>::AccountId,
>>::Currency;

type BalanceOf<T, I> =
    <CurrencyOf<T, I> as Currency<<T as frame_system::Config>::AccountId>>::Balance;

type RankOf<T, I> = <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::Rank;
type AccountIdOf<T, I> = <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::AccountId;

#[frame_support::pallet]
pub mod pallet {
    use super::*;

    #[pallet::pallet]
    #[pallet::without_storage_info]
    pub struct Pallet<T, I = ()>(_);

    #[pallet::config]
    pub trait Config<I: 'static = ()>:
        frame_system::Config + pallet_ranked_collective::Config<I>
    {
        type RuntimeEvent: From<Event<Self, I>>
            + IsType<<Self as frame_system::Config>::RuntimeEvent>;

        type VestingSchedule: VestingSchedule<Self::AccountId, Moment = BlockNumberFor<Self>>;
        type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
        type MemberSwappedHandler: RankedMembersSwapHandler<AccountIdOf<Self, I>, RankOf<Self, I>>;

        #[pallet::constant]
        type Prefix: Get<&'static [u8]>;

        #[pallet::constant]
        type MaximumWithdrawAmount: Get<BalanceOf<Self, I>>;

        #[pallet::constant]
        type VestingBlocks: Get<u32>;

        type WeightInfo: WeightInfo;
    }

    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config<I>, I: 'static = ()> {
        Claimed {
            receiver: T::AccountId,
            donor: T::AccountId,
            amount: BalanceOf<T, I>,
            rank: Option<RankOf<T, I>>,
        },
    }

    #[pallet::error]
    pub enum Error<T, I = ()> {
        InvalidEthereumSignature,
        InvalidEthereumAddress,
        NoBalanceToClaim,
        AddressDecodingFailed,
        ArithmeticError,
        PotUnderflow,
    }

    #[pallet::storage]
    pub type Total<T: Config<I>, I: 'static = ()> = StorageValue<_, BalanceOf<T, I>, ValueQuery>;

    #[pallet::genesis_config]
    #[derive(DefaultNoBound)]
    pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
        pub total: BalanceOf<T, I>,
        pub members_and_ranks: Vec<(T::AccountId, u16)>,
    }

    #[pallet::genesis_build]
    impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
        fn build(&self) {
            let cult_accounts: Vec<_> = self
                .members_and_ranks
                .iter()
                .map(|(account_id, rank)| {
                    assert!(
                        pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(
                            account_id.clone(),
                            *rank,
                            false
                        )
                        .is_ok(),
                        "error during adding and promotion"
                    );
                    account_id
                })
                .collect();

            assert!(
                self.members_and_ranks.len() == cult_accounts.len(),
                "duplicates in `members_and_ranks`"
            );

            Total::<T, I>::put(self.total);
        }
    }

    #[pallet::call]
    impl<T: Config<I>, I: 'static> Pallet<T, I> {
        #[pallet::call_index(0)]
        #[pallet::weight(<T as Config<I>>::WeightInfo::claim())]
        pub fn claim(
            origin: OriginFor<T>,
            ethereum_address: EthereumAddress,
            ethereum_signature: EcdsaSignature,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;
            let data = who.using_encoded(to_ascii_hex);
            let recovered_address = Self::recover_ethereum_address(&ethereum_signature, &data)
                .ok_or(Error::<T, I>::InvalidEthereumSignature)?;

            ensure!(
                recovered_address == ethereum_address,
                Error::<T, I>::InvalidEthereumAddress
            );

            Self::do_claim(who, ethereum_address)
        }
    }
}

fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
    let mut r = Vec::with_capacity(data.len() * 2);
    let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
    for &b in data.iter() {
        push_nibble(b / 16);
        push_nibble(b % 16);
    }
    r
}

impl<T: Config<I>, I: 'static> Pallet<T, I> {
    fn ethereum_signable_message(what: &[u8]) -> Vec<u8> {
        let prefix = T::Prefix::get();
        let mut l = prefix.len() + what.len();
        let mut rev = Vec::new();
        while l > 0 {
            rev.push(b'0' + (l % 10) as u8);
            l /= 10;
        }
        let mut v = b"\x19Ethereum Signed Message:\n".to_vec();
        v.extend(rev.into_iter().rev());
        v.extend_from_slice(prefix);
        v.extend_from_slice(what);
        v
    }

    fn recover_ethereum_address(s: &EcdsaSignature, what: &[u8]) -> Option<EthereumAddress> {
        let msg = keccak_256(&Self::ethereum_signable_message(what));
        let mut res = EthereumAddress::default();
        res.0
            .copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
        Some(res)
    }

    fn into_account_id(address: EthereumAddress) -> Result<T::AccountId, codec::Error> {
        let mut data = [0u8; 32];
        data[0..4].copy_from_slice(b"evm:");
        data[4..24].copy_from_slice(&address.0[..]);

        let hash = sp_core::keccak_256(&data);
        T::AccountId::decode(&mut &hash[..])
    }

    fn do_claim(receiver: T::AccountId, ethereum_address: EthereumAddress) -> DispatchResult {
        let donor = Self::into_account_id(ethereum_address)
            .ok()
            .ok_or(Error::<T, I>::AddressDecodingFailed)?;

        let balance_due = CurrencyOf::<T, I>::free_balance(&donor);
        ensure!(
            balance_due >= CurrencyOf::<T, I>::minimum_balance(),
            Error::<T, I>::NoBalanceToClaim
        );

        let new_total = Total::<T, I>::get()
            .checked_sub(&balance_due)
            .ok_or(Error::<T, I>::PotUnderflow)?;

        CurrencyOf::<T, I>::transfer(
            &donor,
            &receiver,
            balance_due,
            ExistenceRequirement::AllowDeath,
        )?;

        let max_amount = T::MaximumWithdrawAmount::get();
        if balance_due > max_amount {
            let vesting_balance = balance_due
                .checked_sub(&max_amount)
                .ok_or(Error::<T, I>::ArithmeticError)?;

            let vesting_blocks = T::VestingBlocks::get();
            let per_block_balance = vesting_balance
                .checked_div(&vesting_blocks.into())
                .ok_or(Error::<T, I>::ArithmeticError)?;

            T::VestingSchedule::add_vesting_schedule(
                &receiver,
                vesting_balance,
                per_block_balance,
                T::BlockNumberProvider::current_block_number(),
            )?;
        }

        let rank = if let Some(rank) =
            <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::rank_of(&donor)
        {
            pallet_ranked_collective::Pallet::<T, I>::do_remove_member_from_rank(&donor, rank)?;
            let new_rank =
                match <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::rank_of(&receiver)
                {
                    Some(current_rank) if current_rank >= rank => current_rank,
                    Some(current_rank) if current_rank < rank => {
                        for _ in 0..rank - current_rank {
                            pallet_ranked_collective::Pallet::<T, I>::do_promote_member(
                                receiver.clone(),
                                None,
                                false,
                            )?;
                        }
                        rank
                    }
                    _ => {
                        pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(
                            receiver.clone(),
                            rank,
                            false,
                        )?;
                        rank
                    }
                };
            <T as pallet::Config<I>>::MemberSwappedHandler::swapped(&donor, &receiver, new_rank);
            Some(new_rank)
        } else {
            None
        };

        Total::<T, I>::put(new_total);
        Self::deposit_event(Event::<T, I>::Claimed {
            receiver,
            donor,
            amount: balance_due,
            rank,
        });

        Ok(())
    }
}
