#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit = "128"]
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::traits::{Saturating, Zero};
use sp_runtime::{Perbill, RuntimeDebug};
use sp_std::{marker::PhantomData, prelude::*};
use frame_support::{
	dispatch::DispatchResultWithPostInfo,
	ensure,
	traits::{
		tokens::{GetSalary, Pay, PaymentStatus},
		RankedMembers,
	},
};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
pub type Cycle = u32;
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct StatusType<CycleIndex, BlockNumber, Balance> {
	cycle_index: CycleIndex,
	cycle_start: BlockNumber,
	budget: Balance,
	total_registrations: Balance,
	total_unregistered_paid: Balance,
}
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub enum ClaimState<Balance, Id> {
	Nothing,
	Registered(Balance),
	Attempted { registered: Option<Balance>, id: Id, amount: Balance },
}
use ClaimState::*;
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct ClaimantStatus<CycleIndex, Balance, Id> {
	last_active: CycleIndex,
	status: ClaimState<Balance, Id>,
}
#[frame_support::pallet]
pub mod pallet {
	use super::*;
	use frame_support::{dispatch::Pays, pallet_prelude::*};
	use frame_system::pallet_prelude::*;
	#[pallet::pallet]
	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
	#[pallet::config]
	pub trait Config<I: 'static = ()>: frame_system::Config {
		type WeightInfo: WeightInfo;
		type RuntimeEvent: From<Event<Self, I>>
			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
		type Paymaster: Pay<Beneficiary = <Self as frame_system::Config>::AccountId, AssetKind = ()>;
		type Members: RankedMembers<AccountId = <Self as frame_system::Config>::AccountId>;
		type Salary: GetSalary<
			<Self::Members as RankedMembers>::Rank,
			Self::AccountId,
			<Self::Paymaster as Pay>::Balance,
		>;
		#[pallet::constant]
		type RegistrationPeriod: Get<BlockNumberFor<Self>>;
		#[pallet::constant]
		type PayoutPeriod: Get<BlockNumberFor<Self>>;
		#[pallet::constant]
		type Budget: Get<BalanceOf<Self, I>>;
	}
	pub type CycleIndexOf<T> = BlockNumberFor<T>;
	pub type BalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
	pub type IdOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Id;
	pub type StatusOf<T, I> = StatusType<CycleIndexOf<T>, BlockNumberFor<T>, BalanceOf<T, I>>;
	pub type ClaimantStatusOf<T, I> = ClaimantStatus<CycleIndexOf<T>, BalanceOf<T, I>, IdOf<T, I>>;
	#[pallet::storage]
	pub(super) type Status<T: Config<I>, I: 'static = ()> =
		StorageValue<_, StatusOf<T, I>, OptionQuery>;
	#[pallet::storage]
	pub(super) type Claimant<T: Config<I>, I: 'static = ()> =
		StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf<T, I>, OptionQuery>;
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config<I>, I: 'static = ()> {
		Inducted { who: T::AccountId },
		Registered { who: T::AccountId, amount: BalanceOf<T, I> },
		Paid {
			who: T::AccountId,
			beneficiary: T::AccountId,
			amount: BalanceOf<T, I>,
			id: <T::Paymaster as Pay>::Id,
		},
		CycleStarted { index: CycleIndexOf<T> },
	}
	#[pallet::error]
	pub enum Error<T, I = ()> {
		AlreadyStarted,
		NotMember,
		AlreadyInducted,
		NotInducted,
		NoClaim,
		ClaimZero,
		TooLate,
		TooEarly,
		NotYet,
		NotStarted,
		Bankrupt,
		PayError,
		Inconclusive,
		NotCurrent,
	}
	#[pallet::call]
	impl<T: Config<I>, I: 'static> Pallet<T, I> {
		#[pallet::weight(T::WeightInfo::init())]
		#[pallet::call_index(0)]
		pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let _ = ensure_signed(origin)?;
			let now = frame_system::Pallet::<T>::block_number();
			ensure!(!Status::<T, I>::exists(), Error::<T, I>::AlreadyStarted);
			let status = StatusType {
				cycle_index: Zero::zero(),
				cycle_start: now,
				budget: T::Budget::get(),
				total_registrations: Zero::zero(),
				total_unregistered_paid: Zero::zero(),
			};
			Status::<T, I>::put(&status);
			Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::bump())]
		#[pallet::call_index(1)]
		pub fn bump(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let _ = ensure_signed(origin)?;
			let now = frame_system::Pallet::<T>::block_number();
			let cycle_period = Self::cycle_period();
			let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
			status.cycle_start.saturating_accrue(cycle_period);
			ensure!(now >= status.cycle_start, Error::<T, I>::NotYet);
			status.cycle_index.saturating_inc();
			status.budget = T::Budget::get();
			status.total_registrations = Zero::zero();
			status.total_unregistered_paid = Zero::zero();
			Status::<T, I>::put(&status);
			Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::induct())]
		#[pallet::call_index(2)]
		pub fn induct(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let who = ensure_signed(origin)?;
			let cycle_index = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?.cycle_index;
			let _ = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
			ensure!(!Claimant::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
			Claimant::<T, I>::insert(
				&who,
				ClaimantStatus { last_active: cycle_index, status: Nothing },
			);
			Self::deposit_event(Event::<T, I>::Inducted { who });
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::register())]
		#[pallet::call_index(3)]
		pub fn register(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let who = ensure_signed(origin)?;
			let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
			let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
			let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
			let now = frame_system::Pallet::<T>::block_number();
			ensure!(
				now < status.cycle_start + T::RegistrationPeriod::get(),
				Error::<T, I>::TooLate
			);
			ensure!(claimant.last_active < status.cycle_index, Error::<T, I>::NoClaim);
			let payout = T::Salary::get_salary(rank, &who);
			ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
			claimant.last_active = status.cycle_index;
			claimant.status = Registered(payout);
			status.total_registrations.saturating_accrue(payout);
			Claimant::<T, I>::insert(&who, &claimant);
			Status::<T, I>::put(&status);
			Self::deposit_event(Event::<T, I>::Registered { who, amount: payout });
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::payout())]
		#[pallet::call_index(4)]
		pub fn payout(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let who = ensure_signed(origin)?;
			Self::do_payout(who.clone(), who)?;
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::payout_other())]
		#[pallet::call_index(5)]
		pub fn payout_other(
			origin: OriginFor<T>,
			beneficiary: T::AccountId,
		) -> DispatchResultWithPostInfo {
			let who = ensure_signed(origin)?;
			Self::do_payout(who, beneficiary)?;
			Ok(Pays::No.into())
		}
		#[pallet::weight(T::WeightInfo::check_payment())]
		#[pallet::call_index(6)]
		pub fn check_payment(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
			let who = ensure_signed(origin)?;
			let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
			let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
			ensure!(claimant.last_active == status.cycle_index, Error::<T, I>::NotCurrent);
			let (id, registered, amount) = match claimant.status {
				Attempted { id, registered, amount } => (id, registered, amount),
				_ => return Err(Error::<T, I>::NoClaim.into()),
			};
			match T::Paymaster::check_payment(id) {
				PaymentStatus::Failure => {
					if let Some(amount) = registered {
						claimant.status = ClaimState::Registered(amount);
					} else {
						claimant.last_active.saturating_reduce(1u32.into());
						claimant.status = ClaimState::Nothing;
						status.total_unregistered_paid.saturating_reduce(amount);
					}
				},
				PaymentStatus::Success => claimant.status = ClaimState::Nothing,
				_ => return Err(Error::<T, I>::Inconclusive.into()),
			}
			Claimant::<T, I>::insert(&who, &claimant);
			Status::<T, I>::put(&status);
			Ok(Pays::No.into())
		}
	}
	impl<T: Config<I>, I: 'static> Pallet<T, I> {
		pub fn status() -> Option<StatusOf<T, I>> {
			Status::<T, I>::get()
		}
		pub fn last_active(who: &T::AccountId) -> Result<CycleIndexOf<T>, DispatchError> {
			Ok(Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?.last_active)
		}
		pub fn cycle_period() -> BlockNumberFor<T> {
			T::RegistrationPeriod::get() + T::PayoutPeriod::get()
		}
		fn do_payout(who: T::AccountId, beneficiary: T::AccountId) -> DispatchResult {
			let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
			let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
			let now = frame_system::Pallet::<T>::block_number();
			ensure!(
				now >= status.cycle_start + T::RegistrationPeriod::get(),
				Error::<T, I>::TooEarly,
			);
			let (payout, registered) = match claimant.status {
				Registered(unpaid) if claimant.last_active == status.cycle_index => {
					let payout = if status.total_registrations <= status.budget {
						unpaid
					} else {
						Perbill::from_rational(status.budget, status.total_registrations)
							.mul_floor(unpaid)
					};
					(payout, Some(unpaid))
				},
				Nothing | Attempted { .. } if claimant.last_active < status.cycle_index => {
					let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
					let ideal_payout = T::Salary::get_salary(rank, &who);
					let pot = status
						.budget
						.saturating_sub(status.total_registrations)
						.saturating_sub(status.total_unregistered_paid);
					let payout = ideal_payout.min(pot);
					ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
					status.total_unregistered_paid.saturating_accrue(payout);
					(payout, None)
				},
				_ => return Err(Error::<T, I>::NoClaim.into()),
			};
			claimant.last_active = status.cycle_index;
			let id =
				T::Paymaster::pay(&beneficiary, (), payout).map_err(|_| Error::<T, I>::PayError)?;
			claimant.status = Attempted { registered, id, amount: payout };
			Claimant::<T, I>::insert(&who, &claimant);
			Status::<T, I>::put(&status);
			Self::deposit_event(Event::<T, I>::Paid { who, beneficiary, amount: payout, id });
			Ok(())
		}
	}
}