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

use sp_runtime::{traits::StaticLookup, DispatchResult};
use sp_std::prelude::*;

use frame_support::{dispatch::GetDispatchInfo, traits::UnfilteredDispatchable};

mod extension;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;

pub use extension::CheckOnlySudoAccount;
pub use pallet::*;

type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;

#[frame_support::pallet]
pub mod pallet {
    use super::{DispatchResult, *};
    use frame_support::pallet_prelude::*;
    use frame_system::{pallet_prelude::*, RawOrigin};

    pub mod config_preludes {
        use super::*;
        use frame_support::derive_impl;

        pub struct TestDefaultConfig;

        #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)]
        impl frame_system::DefaultConfig for TestDefaultConfig {}

        #[frame_support::register_default_impl(TestDefaultConfig)]
        impl DefaultConfig for TestDefaultConfig {
            type WeightInfo = ();
            #[inject_runtime_type]
            type RuntimeEvent = ();
            #[inject_runtime_type]
            type RuntimeCall = ();
        }
    }
    #[pallet::config(with_default)]
    pub trait Config: frame_system::Config {
        #[pallet::no_default_bounds]
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

        #[pallet::no_default_bounds]
        type RuntimeCall: Parameter
            + UnfilteredDispatchable<RuntimeOrigin = Self::RuntimeOrigin>
            + GetDispatchInfo;

        type WeightInfo: WeightInfo;
    }

    #[pallet::pallet]
    pub struct Pallet<T>(_);

    #[pallet::call]
    impl<T: Config> Pallet<T> {
        #[pallet::call_index(0)]
        #[pallet::weight({
			let dispatch_info = call.get_dispatch_info();
			(
				T::WeightInfo::sudo().saturating_add(dispatch_info.weight),
				dispatch_info.class
			)
		})]
        pub fn sudo(
            origin: OriginFor<T>,
            call: Box<<T as Config>::RuntimeCall>,
        ) -> DispatchResultWithPostInfo {
            Self::ensure_sudo(origin)?;

            let res = call.dispatch_bypass_filter(RawOrigin::Root.into());
            Self::deposit_event(Event::Sudid {
                sudo_result: res.map(|_| ()).map_err(|e| e.error),
            });

            Ok(Pays::No.into())
        }

        #[pallet::call_index(1)]
        #[pallet::weight((*weight, call.get_dispatch_info().class))]
        pub fn sudo_unchecked_weight(
            origin: OriginFor<T>,
            call: Box<<T as Config>::RuntimeCall>,
            weight: Weight,
        ) -> DispatchResultWithPostInfo {
            Self::ensure_sudo(origin)?;
            let _ = weight;

            let res = call.dispatch_bypass_filter(RawOrigin::Root.into());
            Self::deposit_event(Event::Sudid {
                sudo_result: res.map(|_| ()).map_err(|e| e.error),
            });

            Ok(Pays::No.into())
        }

        #[pallet::call_index(2)]
        #[pallet::weight(T::WeightInfo::set_key())]
        pub fn set_key(
            origin: OriginFor<T>,
            new: AccountIdLookupOf<T>,
        ) -> DispatchResultWithPostInfo {
            Self::ensure_sudo(origin)?;

            let new = T::Lookup::lookup(new)?;
            Self::deposit_event(Event::KeyChanged {
                old: Key::<T>::get(),
                new: new.clone(),
            });
            Key::<T>::put(new);

            Ok(Pays::No.into())
        }

        #[pallet::call_index(3)]
        #[pallet::weight({
			let dispatch_info = call.get_dispatch_info();
			(
				T::WeightInfo::sudo_as().saturating_add(dispatch_info.weight),
				dispatch_info.class,
			)
		})]
        pub fn sudo_as(
            origin: OriginFor<T>,
            who: AccountIdLookupOf<T>,
            call: Box<<T as Config>::RuntimeCall>,
        ) -> DispatchResultWithPostInfo {
            Self::ensure_sudo(origin)?;

            let who = T::Lookup::lookup(who)?;
            let res = call.dispatch_bypass_filter(RawOrigin::Signed(who).into());
            Self::deposit_event(Event::SudoAsDone {
                sudo_result: res.map(|_| ()).map_err(|e| e.error),
            });

            Ok(Pays::No.into())
        }

        #[pallet::call_index(4)]
        #[pallet::weight(T::WeightInfo::remove_key())]
        pub fn remove_key(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
            Self::ensure_sudo(origin)?;

            Self::deposit_event(Event::KeyRemoved {});
            Key::<T>::kill();

            Ok(Pays::No.into())
        }
    }

    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        Sudid {
            sudo_result: DispatchResult,
        },
        KeyChanged {
            old: Option<T::AccountId>,
            new: T::AccountId,
        },
        KeyRemoved,
        SudoAsDone {
            sudo_result: DispatchResult,
        },
    }

    #[pallet::error]
    pub enum Error<T> {
        RequireSudo,
    }

    #[pallet::storage]
    pub(super) type Key<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;

    #[pallet::genesis_config]
    #[derive(frame_support::DefaultNoBound)]
    pub struct GenesisConfig<T: Config> {
        pub key: Option<T::AccountId>,
    }

    #[pallet::genesis_build]
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
        fn build(&self) {
            Key::<T>::set(self.key.clone());
        }
    }

    impl<T: Config> Pallet<T> {
        pub(crate) fn ensure_sudo(origin: OriginFor<T>) -> DispatchResult {
            let sender = ensure_signed_or_root(origin)?;

            if let Some(sender) = sender {
                if Key::<T>::get().map_or(false, |k| k == sender) {
                    Ok(())
                } else {
                    Err(Error::<T>::RequireSudo.into())
                }
            } else {
                Ok(())
            }
        }
    }
}
