#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod migrations;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
use cfg_traits::{
	self,
	rewards::{AccountRewards, CurrencyGroupChange, GroupRewards},
	Seconds, TimeAsSecs,
};
use cfg_types::fixed_point::FixedPointNumberExtension;
use frame_support::{
	pallet_prelude::*,
	storage::transactional,
	traits::{
		fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
		fungibles::{Inspect, Mutate},
		tokens::{Balance, Fortitude, Precision},
		OneSessionHandler,
	},
	DefaultNoBound, PalletId,
};
use frame_system::pallet_prelude::*;
use num_traits::sign::Unsigned;
pub use pallet::*;
use sp_runtime::{
	traits::{AccountIdConversion, EnsureAdd, EnsureMul, Zero},
	FixedPointNumber, FixedPointOperand, SaturatedConversion, Saturating,
};
use sp_std::{mem, vec::Vec};
pub use weights::WeightInfo;
#[derive(
	Encode, Decode, DefaultNoBound, Clone, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct CollatorChanges<T: Config> {
	pub inc: BoundedVec<T::AccountId, T::MaxCollators>,
	pub out: BoundedVec<T::AccountId, T::MaxCollators>,
}
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T))]
pub struct SessionData<T: Config> {
	pub(crate) collator_reward: T::Balance,
	pub collator_count: u32,
	pub(crate) treasury_inflation_rate: T::Rate,
	pub(crate) last_update: Seconds,
}
impl<T: Config> Default for SessionData<T> {
	fn default() -> Self {
		Self {
			collator_count: 0,
			collator_reward: T::Balance::zero(),
			treasury_inflation_rate: T::Rate::zero(),
			last_update: Seconds::zero(),
		}
	}
}
#[derive(
	PartialEq, Clone, DefaultNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct SessionChanges<T: Config> {
	pub collators: CollatorChanges<T>,
	pub collator_count: Option<u32>,
	pub collator_reward: Option<T::Balance>,
	treasury_inflation_rate: Option<T::Rate>,
	last_update: Seconds,
}
#[frame_support::pallet]
pub mod pallet {
	use super::*;
	pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
	#[pallet::config]
	pub trait Config: frame_system::Config {
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
		type Balance: Balance + MaxEncodedLen + FixedPointOperand + MaybeSerializeDeserialize;
		type Weight: Parameter + MaxEncodedLen + EnsureAdd + Unsigned + FixedPointOperand + Default;
		type Rewards: GroupRewards<Balance = Self::Balance, GroupId = u32>
			+ AccountRewards<
				Self::AccountId,
				Balance = Self::Balance,
				CurrencyId = <Self as Config>::CurrencyId,
			> + CurrencyGroupChange<GroupId = u32, CurrencyId = <Self as Config>::CurrencyId>;
		type Tokens: Mutate<Self::AccountId>
			+ Inspect<
				Self::AccountId,
				AssetId = <Self as Config>::CurrencyId,
				Balance = Self::Balance,
			> + FungibleMutate<Self::AccountId>
			+ FungibleInspect<Self::AccountId, Balance = Self::Balance>;
		type CurrencyId: Parameter + Member + Copy + Ord + MaxEncodedLen;
		#[pallet::constant]
		type StakeCurrencyId: Get<<Self as Config>::CurrencyId>;
		#[pallet::constant]
		type StakeAmount: Get<<Self as Config>::Balance>;
		#[pallet::constant]
		type StakeGroupId: Get<u32>;
		#[pallet::constant]
		type MaxCollators: Get<u32> + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq;
		type TreasuryPalletId: Get<PalletId>;
		type AuthorityId: Member
			+ Parameter
			+ sp_runtime::RuntimeAppPublic
			+ MaybeSerializeDeserialize
			+ MaxEncodedLen;
		type Rate: Parameter
			+ Member
			+ FixedPointNumberExtension
			+ MaybeSerializeDeserialize
			+ MaxEncodedLen;
		type Time: TimeAsSecs;
		type WeightInfo: WeightInfo;
	}
	#[pallet::pallet]
	#[pallet::storage_version(STORAGE_VERSION)]
	pub struct Pallet<T>(_);
	#[pallet::storage]
	#[pallet::getter(fn active_session_data)]
	pub(super) type ActiveSessionData<T: Config> = StorageValue<_, SessionData<T>, ValueQuery>;
	#[pallet::storage]
	#[pallet::getter(fn next_session_changes)]
	pub(super) type NextSessionChanges<T: Config> = StorageValue<_, SessionChanges<T>, ValueQuery>;
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		NewSession {
			collator_reward: T::Balance,
			treasury_inflation_rate: T::Rate,
			last_changes: SessionChanges<T>,
		},
		SessionAdvancementFailed {
			error: DispatchError,
		},
	}
	#[pallet::error]
	pub enum Error<T> {}
	#[pallet::genesis_config]
	#[derive(DefaultNoBound)]
	pub struct GenesisConfig<T: Config> {
		pub collators: Vec<T::AccountId>,
		pub collator_reward: T::Balance,
		pub treasury_inflation_rate: T::Rate,
		pub last_update: Seconds,
	}
	#[pallet::genesis_build]
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
		fn build(&self) {
			T::Rewards::attach_currency(T::StakeCurrencyId::get(), T::StakeGroupId::get()).expect(
				"Should be able to attach default block rewards staking currency to collator group",
			);
			ActiveSessionData::<T>::mutate(|session_data| {
				session_data.collator_count = self.collators.len().saturated_into();
				session_data.collator_reward = self.collator_reward;
				session_data.treasury_inflation_rate = self.treasury_inflation_rate;
				session_data.last_update = self.last_update;
			});
			for collator in &self.collators {
				Pallet::<T>::do_init_collator(collator)
					.expect("Should not panic when initiating genesis collators for block rewards");
			}
		}
	}
	#[pallet::call]
	impl<T: Config> Pallet<T> {
		#[pallet::weight(T::WeightInfo::claim_reward())]
		#[pallet::call_index(0)]
		pub fn claim_reward(origin: OriginFor<T>, account_id: T::AccountId) -> DispatchResult {
			ensure_signed(origin)?;
			T::Rewards::claim_reward(T::StakeCurrencyId::get(), &account_id).map(|_| ())
		}
		#[pallet::weight(T::WeightInfo::set_collator_reward_per_session())]
		#[pallet::call_index(1)]
		pub fn set_collator_reward_per_session(
			origin: OriginFor<T>,
			collator_reward_per_session: T::Balance,
		) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextSessionChanges::<T>::mutate(|c| {
				c.collator_reward = Some(collator_reward_per_session);
			});
			Ok(())
		}
		#[pallet::weight(T::WeightInfo::set_annual_treasury_inflation_rate())]
		#[pallet::call_index(2)]
		pub fn set_annual_treasury_inflation_rate(
			origin: OriginFor<T>,
			treasury_inflation_rate: T::Rate,
		) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextSessionChanges::<T>::mutate(|c| {
				c.treasury_inflation_rate = Some(treasury_inflation_rate);
			});
			Ok(())
		}
	}
}
impl<T: Config> Pallet<T> {
	pub(crate) fn do_init_collator(who: &T::AccountId) -> DispatchResult {
		let existential_deposit =
			<T::Tokens as Inspect<T::AccountId>>::minimum_balance(T::StakeCurrencyId::get());
		<T::Tokens as Mutate<T::AccountId>>::mint_into(
			T::StakeCurrencyId::get(),
			who,
			T::StakeAmount::get().saturating_add(existential_deposit),
		)?;
		T::Rewards::deposit_stake(T::StakeCurrencyId::get(), who, T::StakeAmount::get())
	}
	pub(crate) fn do_exit_collator(who: &T::AccountId) -> DispatchResult {
		let amount = T::Rewards::account_stake(T::StakeCurrencyId::get(), who);
		T::Rewards::withdraw_stake(T::StakeCurrencyId::get(), who, amount)?;
		<T::Tokens as Mutate<T::AccountId>>::burn_from(
			T::StakeCurrencyId::get(),
			who,
			amount,
			Precision::Exact,
			Fortitude::Polite,
		)
		.map(|_| ())
	}
	pub(crate) fn calculate_epoch_treasury_inflation(
		annual_inflation_rate: T::Rate,
		last_update: Seconds,
	) -> T::Balance {
		let total_issuance = <T::Tokens as FungibleInspect<T::AccountId>>::total_issuance();
		let session_duration = T::Time::now().saturating_sub(last_update);
		let inflation_proration =
			cfg_types::pools::saturated_rate_proration(annual_inflation_rate, session_duration);
		inflation_proration.saturating_mul_int(total_issuance)
	}
	fn do_advance_session() {
		let mut num_joining = 0u32;
		let mut num_leaving = 0u32;
		transactional::with_storage_layer(|| -> DispatchResult {
			NextSessionChanges::<T>::try_mutate(|changes| -> DispatchResult {
				ActiveSessionData::<T>::try_mutate(|session_data| {
					let total_collator_reward = session_data
						.collator_reward
						.ensure_mul(session_data.collator_count.into())?;
					T::Rewards::reward_group(T::StakeGroupId::get(), total_collator_reward)?;
					let treasury_inflation = Self::calculate_epoch_treasury_inflation(
						session_data.treasury_inflation_rate,
						session_data.last_update,
					);
					if !treasury_inflation.is_zero() {
						let _ = <T::Tokens as FungibleMutate<T::AccountId>>::mint_into(
							&T::TreasuryPalletId::get().into_account_truncating(),
							treasury_inflation,
						)
						.map_err(|e| {
							log::error!(
								"Failed to mint treasury inflation for session due to error {:?}",
								e
							)
						});
					}
					num_joining = changes.collators.inc.len().saturated_into();
					num_leaving = changes.collators.out.len().saturated_into();
					for leaving in changes.collators.out.iter() {
						Self::do_exit_collator(leaving)?;
					}
					for joining in changes.collators.inc.iter() {
						Self::do_init_collator(joining)?;
					}
					session_data.collator_reward = changes
						.collator_reward
						.unwrap_or(session_data.collator_reward);
					session_data.treasury_inflation_rate = changes
						.treasury_inflation_rate
						.unwrap_or(session_data.treasury_inflation_rate);
					session_data.collator_count = changes
						.collator_count
						.unwrap_or(session_data.collator_count);
					session_data.last_update = T::Time::now();
					Self::deposit_event(Event::NewSession {
						collator_reward: session_data.collator_reward,
						treasury_inflation_rate: session_data.treasury_inflation_rate,
						last_changes: mem::take(changes),
					});
					Ok(())
				})
			})
		})
		.map_err(|error| {
			Self::deposit_event(Event::SessionAdvancementFailed { error });
		})
		.ok();
	}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
	type Public = T::AuthorityId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
	type Key = T::AuthorityId;
	fn on_genesis_session<'a, I: 'a>(_validators: I)
	where
		I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
	{
		}
	fn on_new_session<'a, I: 'a>(_: bool, validators: I, queued_validators: I)
	where
		I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
	{
		Self::do_advance_session();
		let current = validators
			.map(|(acc_id, _)| acc_id.clone())
			.collect::<Vec<_>>();
		let next = queued_validators
			.map(|(acc_id, _)| acc_id.clone())
			.collect::<Vec<_>>();
		if current != next {
			NextSessionChanges::<T>::mutate(
				|SessionChanges {
				     collators,
				     collator_count,
				     ..
				 }| {
					let inc = next
						.clone()
						.into_iter()
						.filter(|n| !current.iter().any(|curr| curr == n))
						.collect::<Vec<_>>();
					let out = current
						.clone()
						.into_iter()
						.filter(|curr| !next.iter().any(|n| n == curr))
						.collect::<Vec<_>>();
					collators.inc = BoundedVec::<_, T::MaxCollators>::truncate_from(inc);
					collators.out = BoundedVec::<_, T::MaxCollators>::truncate_from(out);
					*collator_count = Some(next.len().saturated_into::<u32>());
				},
			);
		}
	}
	fn on_before_session_ending() {
		}
	fn on_disabled(_validator_index: u32) {
		}
}