//! Hiring substrate module for the Joystream platform
//!
//! Public APIs:
//! - add_opening
//! - ensure_can_add_application
//! - add_application
//! - deactivate_application
//! - cancel_opening
//! - fill_opening
//! - begin_review
//! - begin_acception_application
//! - unstaked
//!
//! Dependency: Joystream stake module

// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
//#![warn(missing_docs)]

// Test feature dependencies
#[cfg(all(any(test, feature = "test"), not(target_arch = "wasm32")))]
use mockall::predicate::*;
#[cfg(all(any(test, feature = "test"), not(target_arch = "wasm32")))]
use mockall::*;

use stake::{InitiateUnstakingError, Stake, StakeActionError, StakingError, Trait as StakeTrait};

use codec::Codec;

use runtime_primitives::traits::Zero;
use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};

use srml_support::traits::Currency;
use srml_support::{decl_module, decl_storage, ensure, Parameter};

use rstd::collections::btree_map::BTreeMap;
use rstd::collections::btree_set::BTreeSet;
use rstd::iter::Iterator;
use rstd::prelude::*;

use rstd::cell::RefCell;
use rstd::rc::Rc;

use crate::sr_api_hidden_includes_decl_storage::hidden_include::traits::Imbalance;

mod hiring;
#[macro_use]
mod macroes;
mod mock;
mod test;

pub use hiring::*;

/// Main trait of hiring substrate module
pub trait Trait: system::Trait + stake::Trait + Sized {
    /// OpeningId type
    type OpeningId: Parameter
        + Member
        + SimpleArithmetic
        + Codec
        + Default
        + Copy
        + MaybeSerialize
        + PartialEq;

    /// ApplicationId type
    type ApplicationId: Parameter
        + Member
        + SimpleArithmetic
        + Codec
        + Default
        + Copy
        + MaybeSerialize
        + PartialEq;

    /// Type that will handle various staking events
    type ApplicationDeactivatedHandler: ApplicationDeactivatedHandler<Self>;

    /// Marker type for Stake module handler. Indicates that hiring module uses stake module mock.
    type StakeHandlerProvider: StakeHandlerProvider<Self>;
}

decl_storage! {
    trait Store for Module<T: Trait> as Hiring {

        /// Openings.
        pub OpeningById get(opening_by_id): linked_map T::OpeningId => Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>;

        /// Identifier for next opening to be added.
        pub NextOpeningId get(next_opening_id): T::OpeningId;

        /// Applications
        pub ApplicationById get(application_by_id): linked_map T::ApplicationId => Application<T::OpeningId, T::BlockNumber, T::StakeId>;

        /// Identifier for next application to be added.
        pub NextApplicationId get(next_application_id): T::ApplicationId;

        /// Internal purpose of given stake, i.e. fro what application, and whether for the role or for the application.
        pub ApplicationIdByStakingId get(stake_purpose_by_staking_id): linked_map T::StakeId => T::ApplicationId;
    }
}

decl_module! {
    /// Main hiring module definition
    pub struct Module<T: Trait> for enum Call where origin: T::Origin {

        fn on_finalize(now: T::BlockNumber) {

            //
            // == MUTATION SAFE ==
            //

            // Change opening from WaitingToBegin stage to Active::AcceptingApplications stage
            for (opening_id, opening) in Self::openings_waiting_to_begin_iterator(now) {
                let opening_accepting_applications = opening.clone_with_new_active_opening_stage(
                    hiring::ActiveOpeningStage::AcceptingApplications {
                        started_accepting_applicants_at_block: now
                });

                <OpeningById<T>>::insert(opening_id, opening_accepting_applications);
            }

            // Deactivate opening
            for (opening_id,
                opening,
                (
                    applications_added,
                    started_accepting_applicants_at_block,
                    started_review_period_at_block
                )) in Self::openings_expired_review_period_iterator(now) {

                //
                // Deactivate all applications that are part of this opening
                //

                // Get unstaking periods
                let application_stake_unstaking_period = StakingPolicy::opt_staking_policy_to_review_period_expired_unstaking_period(&opening.application_staking_policy);
                let role_stake_unstaking_period = StakingPolicy::opt_staking_policy_to_review_period_expired_unstaking_period(&opening.role_staking_policy);

                // Get applications
                let applications_map = Self::application_id_iter_to_map(applications_added.iter());

                // Deactivate applications
                Self::initiate_application_deactivations(
                    &applications_map,
                    application_stake_unstaking_period,
                    role_stake_unstaking_period,
                    hiring::ApplicationDeactivationCause::ReviewPeriodExpired
                );

                let deactivated_opening =
                    opening.clone_with_new_active_opening_stage(
                        hiring::ActiveOpeningStage::Deactivated {
                            cause: hiring::OpeningDeactivationCause::ReviewPeriodExpired,
                            deactivated_at_block: now,
                            started_accepting_applicants_at_block,
                            started_review_period_at_block: Some(started_review_period_at_block),
                    });

                <OpeningById<T>>::insert(opening_id, deactivated_opening);
            }
        }
    }
}

/*
 *  ======== Main API implementation ========
 */

// Public API implementation
impl<T: Trait> Module<T> {
    /// Add new opening based on given inputs policies.
    /// The new Opening instance has stage WaitingToBegin, and is added to openingsById,
    /// and has identifier equal to nextOpeningId.
    /// The latter is incremented. The used identifier is returned.
    pub fn add_opening(
        activate_at: ActivateOpeningAt<T::BlockNumber>,
        max_review_period_length: T::BlockNumber,
        application_rationing_policy: Option<ApplicationRationingPolicy>,
        application_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
        role_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
        human_readable_text: Vec<u8>,
    ) -> Result<T::OpeningId, AddOpeningError> {
        let current_block_height = <system::Module<T>>::block_number();

        Opening::<BalanceOf<T>, T::BlockNumber, T::ApplicationId>::ensure_can_add_opening(
            current_block_height,
            activate_at.clone(),
            T::Currency::minimum_balance(),
            application_rationing_policy.clone(),
            application_staking_policy.clone(),
            role_staking_policy.clone(),
        )?;

        // == MUTATION SAFE ==

        let new_opening = hiring::Opening::new(
            current_block_height,
            activate_at,
            max_review_period_length,
            application_rationing_policy,
            application_staking_policy,
            role_staking_policy,
            human_readable_text,
        );

        // Get Id for new opening
        let new_opening_id = <NextOpeningId<T>>::get();

        // Insert opening in storage
        <OpeningById<T>>::insert(new_opening_id, new_opening);

        // Update NextOpeningId counter
        <NextOpeningId<T>>::mutate(|id| *id += T::OpeningId::one());

        // Return
        Ok(new_opening_id)
    }

    /// Cancels opening with given identifier, using provided unstaking periods for
    /// application and role, as necesary.
    pub fn cancel_opening(
        opening_id: T::OpeningId,
        application_stake_unstaking_period: Option<T::BlockNumber>,
        role_stake_unstaking_period: Option<T::BlockNumber>,
    ) -> Result<OpeningCancelled, CancelOpeningError> {
        // Ensure that the opening exists
        let opening =
            ensure_opening_exists!(T, opening_id, CancelOpeningError::OpeningDoesNotExist)?;

        // Opening is in stage Active.{AcceptingApplications or ReviewPeriod}

        let (
            active_stage,
            applications_added,
            active_application_count,
            unstaking_application_count,
            deactivated_application_count,
        ) = ensure_opening_is_active!(
            opening.stage,
            CancelOpeningError::OpeningNotInCancellableStage
        )?;

        //
        let current_block_height = <system::Module<T>>::block_number(); // move later!
        let new_active_stage = active_stage.new_stage_on_cancelling(current_block_height)?;

        // Ensure unstaking periods are OK.
        ensure_opt_unstaking_period_is_ok!(
            application_stake_unstaking_period,
            opening.application_staking_policy,
            CancelOpeningError::UnstakingPeriodTooShort(StakePurpose::Application),
            CancelOpeningError::RedundantUnstakingPeriodProvided(StakePurpose::Application)
        )?;

        ensure_opt_unstaking_period_is_ok!(
            role_stake_unstaking_period,
            opening.role_staking_policy,
            CancelOpeningError::UnstakingPeriodTooShort(StakePurpose::Role),
            CancelOpeningError::RedundantUnstakingPeriodProvided(StakePurpose::Role)
        )?;

        //
        // == MUTATION SAFE ==
        //

        // Create and store new cancelled opening
        let new_opening = Opening {
            stage: hiring::OpeningStage::Active {
                stage: new_active_stage,
                applications_added: applications_added.clone(),
                active_application_count,
                unstaking_application_count,
                deactivated_application_count,
            },
            ..opening
        };

        OpeningById::<T>::insert(opening_id, new_opening);

        // Map with applications
        let applications_map = Self::application_id_iter_to_map(applications_added.iter());

        // Initiate deactivation of all active applications
        let net_result = Self::initiate_application_deactivations(
            &applications_map,
            application_stake_unstaking_period,
            role_stake_unstaking_period,
            hiring::ApplicationDeactivationCause::OpeningCancelled,
        );

        // Return
        Ok(OpeningCancelled {
            number_of_unstaking_applications: net_result.number_of_unstaking_applications,
            number_of_deactivated_applications: net_result.number_of_deactivated_applications,
        })
    }

    /// Transit opening to the accepting application stage.
    /// Applies when given opening is in WaitingToBegin stage.
    /// The stage is updated to Active stage with AcceptingApplications substage
    pub fn begin_accepting_applications(
        opening_id: T::OpeningId,
    ) -> Result<(), BeginAcceptingApplicationsError> {
        // Ensure that the opening exists
        let opening = ensure_opening_exists!(
            T,
            opening_id,
            BeginAcceptingApplicationsError::OpeningDoesNotExist
        )?;

        // Ensure that it is the waiting to begin stage
        opening.stage.ensure_opening_stage_is_waiting_to_begin(
            BeginAcceptingApplicationsError::OpeningIsNotInWaitingToBeginStage,
        )?;

        //
        // == MUTATION SAFE ==
        //

        let current_block_height = <system::Module<T>>::block_number();

        // Update state of opening
        let new_opening = opening.clone_with_new_active_opening_stage(
            hiring::ActiveOpeningStage::AcceptingApplications {
                started_accepting_applicants_at_block: current_block_height,
            },
        );

        // Write back opening
        <OpeningById<T>>::insert(opening_id, new_opening);

        // DONE
        Ok(())
    }

    /// Transit opening to the begin review period stage.
    /// Applies when given opening is in Active stage and AcceptingApplications substage.
    /// The stage is updated to Active stage and ReviewPeriod substage
    pub fn begin_review(opening_id: T::OpeningId) -> Result<(), BeginReviewError> {
        // Ensure that the opening exists
        let opening = ensure_opening_exists!(T, opening_id, BeginReviewError::OpeningDoesNotExist)?;

        // Opening is accepting applications
        let (active_stage, _, _, _, _) = ensure_opening_is_active!(
            opening.stage,
            BeginReviewError::OpeningNotInAcceptingApplicationsStage
        )?;

        let started_accepting_applicants_at_block = ensure_active_opening_is_accepting_applications!(
            active_stage,
            BeginReviewError::OpeningNotInAcceptingApplicationsStage
        )?;

        //
        // == MUTATION SAFE ==
        //

        let current_block_height = <system::Module<T>>::block_number();

        let new_opening =
            opening.clone_with_new_active_opening_stage(hiring::ActiveOpeningStage::ReviewPeriod {
                started_accepting_applicants_at_block,
                started_review_period_at_block: current_block_height,
            });

        // Update to new opening
        <OpeningById<T>>::insert(opening_id, new_opening);

        Ok(())
    }

    /// Fill an opening, identified with `opening_id`, currently in the review period.
    /// Applies when given opening is in ReviewPeriod stage.
    /// Given list of applications are deactivated to under the Hired,
    /// all other active applicants are NotHired.
    /// Separately for each group,
    /// unstaking periods for any applicable application and/or role stake must be provided.
    pub fn fill_opening(
        opening_id: T::OpeningId,
        successful_applications: BTreeSet<T::ApplicationId>,
        opt_successful_applicant_application_stake_unstaking_period: Option<T::BlockNumber>,
        opt_failed_applicant_application_stake_unstaking_period: Option<T::BlockNumber>,
        /* this parameter does not make sense? opt_successful_applicant_role_stake_unstaking_period: Option<T::BlockNumber>, */
        opt_failed_applicant_role_stake_unstaking_period: Option<T::BlockNumber>,
    ) -> Result<(), FillOpeningError<T>> {
        // Ensure that the opening exists
        let opening = ensure_opening_exists!(T, opening_id, FillOpeningError::OpeningDoesNotExist)?;

        let (active_stage, applications_added, _, _, _) = ensure_opening_is_active!(
            opening.stage,
            FillOpeningError::OpeningNotInReviewPeriodStage
        )?;

        // Ensure opening is in review period
        let (started_accepting_applicants_at_block, started_review_period_at_block) = active_stage
            .ensure_active_opening_is_in_review_period(
                FillOpeningError::OpeningNotInReviewPeriodStage,
            )?;

        //
        // Ensure that all unstaking periods are neither too short (0) nor redundant.
        //

        ensure_opt_unstaking_period_is_ok!(
            opt_successful_applicant_application_stake_unstaking_period,
            opening.application_staking_policy,
            FillOpeningError::UnstakingPeriodTooShort(
                StakePurpose::Application,
                ApplicationOutcomeInFilledOpening::Success
            ),
            FillOpeningError::RedundantUnstakingPeriodProvided(
                StakePurpose::Application,
                ApplicationOutcomeInFilledOpening::Success
            )
        )?;

        ensure_opt_unstaking_period_is_ok!(
            opt_failed_applicant_application_stake_unstaking_period,
            opening.application_staking_policy,
            FillOpeningError::UnstakingPeriodTooShort(
                StakePurpose::Application,
                ApplicationOutcomeInFilledOpening::Failure
            ),
            FillOpeningError::RedundantUnstakingPeriodProvided(
                StakePurpose::Application,
                ApplicationOutcomeInFilledOpening::Failure
            )
        )?;

        ensure_opt_unstaking_period_is_ok!(
            opt_failed_applicant_role_stake_unstaking_period,
            opening.role_staking_policy,
            FillOpeningError::UnstakingPeriodTooShort(
                StakePurpose::Role,
                ApplicationOutcomeInFilledOpening::Failure
            ),
            FillOpeningError::RedundantUnstakingPeriodProvided(
                StakePurpose::Role,
                ApplicationOutcomeInFilledOpening::Failure
            )
        )?;

        // Ensure that all successful applications actually exist
        for application_id in &successful_applications {
            ensure_application_exists!(
                T,
                *application_id,
                FillOpeningError::ApplicationDoesNotExist(*application_id)
            )?;
        }

        let successful_applications_map =
            Self::application_id_iter_to_map(successful_applications.iter());

        // Ensure that all successful applications are actually active and associated with the opening
        for (application_id, application) in &successful_applications_map {
            ensure_eq!(
                application.stage,
                hiring::ApplicationStage::Active,
                FillOpeningError::ApplicationNotInActiveStage(*application_id,)
            );

            ensure_eq!(
                application.opening_id,
                opening_id,
                FillOpeningError::ApplicationForWrongOpening(*application_id)
            );
        }

        //
        // == MUTATION SAFE ==
        //

        // Deactivate all successful applications, with cause being hired
        Self::initiate_application_deactivations(
            &successful_applications_map,
            opt_successful_applicant_application_stake_unstaking_period,
            None,
            hiring::ApplicationDeactivationCause::Hired,
        );

        // Deactivate all unsuccessful applications, with cause being not being hired.

        // First get all failed applications by their id.
        let failed_applications_map = Self::application_id_iter_to_map(
            applications_added.difference(&successful_applications),
        );

        // Deactivate all successful applications, with cause being not hired
        Self::initiate_application_deactivations(
            &failed_applications_map,
            opt_failed_applicant_application_stake_unstaking_period,
            opt_failed_applicant_role_stake_unstaking_period,
            hiring::ApplicationDeactivationCause::NotHired,
        );

        // Grab current block height
        let current_block_height = <system::Module<T>>::block_number();
        // Get opening with updated counters
        let opening_needed_for_data = <OpeningById<T>>::get(opening_id);

        // Deactivate opening
        let new_opening = opening_needed_for_data.clone_with_new_active_opening_stage(
            hiring::ActiveOpeningStage::Deactivated {
                cause: OpeningDeactivationCause::Filled,
                deactivated_at_block: current_block_height,
                started_accepting_applicants_at_block,
                started_review_period_at_block: Some(started_review_period_at_block),
            },
        );

        // Write back new opening
        <OpeningById<T>>::insert(opening_id, new_opening);

        // DONE
        Ok(())
    }

    /// Adds a new application on the given opening, and begins staking for
    /// the role, the application or both possibly.
    pub fn ensure_can_add_application(
        opening_id: T::OpeningId,
        opt_role_stake_balance: Option<BalanceOf<T>>,
        opt_application_stake_balance: Option<BalanceOf<T>>,
    ) -> Result<DestructuredApplicationCanBeAddedEvaluation<T>, AddApplicationError> {
        // Ensure that the opening exists
        let opening =
            ensure_opening_exists!(T, opening_id, AddApplicationError::OpeningDoesNotExist)?;

        // Ensure that proposed stakes match the policy of the opening.
        let opt_role_stake_balance = ensure_stake_balance_matches_staking_policy!(
            &opt_role_stake_balance,
            &opening.role_staking_policy,
            AddApplicationError::StakeMissingWhenRequired(StakePurpose::Role),
            AddApplicationError::StakeProvidedWhenRedundant(StakePurpose::Role),
            AddApplicationError::StakeAmountTooLow(StakePurpose::Role)
        )?;

        let opt_application_stake_balance = ensure_stake_balance_matches_staking_policy!(
            &opt_application_stake_balance,
            &opening.application_staking_policy,
            AddApplicationError::StakeMissingWhenRequired(StakePurpose::Application),
            AddApplicationError::StakeProvidedWhenRedundant(StakePurpose::Application),
            AddApplicationError::StakeAmountTooLow(StakePurpose::Application)
        )?;

        // Opening is accepting applications

        let (
            active_stage,
            applications_added,
            active_application_count,
            unstaking_application_count,
            deactivated_application_count,
        ) = ensure_opening_is_active!(
            opening.stage,
            AddApplicationError::OpeningNotInAcceptingApplicationsStage
        )?;

        active_stage.ensure_active_opening_is_accepting_applications(
            AddApplicationError::OpeningNotInAcceptingApplicationsStage,
        )?;

        // Ensure that the new application would actually make it
        let would_get_added_success = ensure_application_would_get_added!(
            &opening.application_rationing_policy,
            &applications_added,
            &opt_role_stake_balance,
            &opt_application_stake_balance,
            AddApplicationError::NewApplicationWasCrowdedOut
        )?;

        Ok(DestructuredApplicationCanBeAddedEvaluation {
            opening,
            active_stage,
            applications_added,
            active_application_count,
            unstaking_application_count,
            deactivated_application_count,
            would_get_added_success,
        })
    }

    /// Adds a new application on the given opening, and begins staking for
    /// the role, the application or both possibly.
    pub fn add_application(
        opening_id: T::OpeningId,
        opt_role_stake_imbalance: Option<NegativeImbalance<T>>,
        opt_application_stake_imbalance: Option<NegativeImbalance<T>>,
        human_readable_text: Vec<u8>,
    ) -> Result<ApplicationAdded<T::ApplicationId>, AddApplicationError> {
        let opt_role_stake_balance = Self::create_stake_balance(&opt_role_stake_imbalance);
        let opt_application_stake_balance =
            Self::create_stake_balance(&opt_application_stake_imbalance);

        let can_be_added_destructured = Self::ensure_can_add_application(
            opening_id,
            opt_role_stake_balance,
            opt_application_stake_balance,
        )?;

        //
        // == MUTATION SAFE ==
        //

        // If required, deactive another application that was crowded out.
        if let ApplicationAddedSuccess::CrowdsOutExistingApplication(
            id_of_croweded_out_application,
        ) = can_be_added_destructured.would_get_added_success
        {
            // Get relevant unstaking periods
            let opt_application_stake_unstaking_period =
                hiring::StakingPolicy::opt_staking_policy_to_crowded_out_unstaking_period(
                    &can_be_added_destructured.opening.application_staking_policy,
                );
            let opt_role_stake_unstaking_period =
                hiring::StakingPolicy::opt_staking_policy_to_crowded_out_unstaking_period(
                    &can_be_added_destructured.opening.role_staking_policy,
                );

            // Fetch application
            let crowded_out_application = <ApplicationById<T>>::get(id_of_croweded_out_application);

            // Initiate actual deactivation
            //
            // MUST not have been ignored, is runtime invariant, false means code is broken.
            // But should we do panic in runtime? Is there safer way?
            let deactivation_result = Self::try_to_initiate_application_deactivation(
                &crowded_out_application,
                id_of_croweded_out_application,
                opt_application_stake_unstaking_period,
                opt_role_stake_unstaking_period,
                hiring::ApplicationDeactivationCause::CrowdedOut,
            );

            assert_ne!(
                deactivation_result,
                ApplicationDeactivationInitiationResult::Ignored
            );
        }

        // Get Id for this new application
        let new_application_id = <NextApplicationId<T>>::get();

        // Possibly initiate staking
        let active_role_staking_id =
            Self::infallible_opt_stake_initiation(opt_role_stake_imbalance, &new_application_id);
        let active_application_staking_id = Self::infallible_opt_stake_initiation(
            opt_application_stake_imbalance,
            &new_application_id,
        );

        // Grab current block height
        let current_block_height = <system::Module<T>>::block_number();

        // Compute index for this new application
        let application_index_in_opening =
            can_be_added_destructured.calculate_total_application_count();

        // Create a new application
        let new_application = hiring::Application {
            opening_id,
            application_index_in_opening,
            add_to_opening_in_block: current_block_height,
            active_role_staking_id,
            active_application_staking_id,
            // Stage of new application
            stage: hiring::ApplicationStage::Active,
            human_readable_text,
        };

        // Insert into main application map
        <ApplicationById<T>>::insert(new_application_id, new_application);

        // Update next application id
        <NextApplicationId<T>>::mutate(|id| *id += One::one());

        // Update counter on opening
        // Should reload after possible deactivation in try_to_initiate_application_deactivation
        let opening_needed_for_data = <OpeningById<T>>::get(opening_id);
        let new_active_stage = opening_needed_for_data
            .stage
            .clone_with_added_active_application(new_application_id);

        <OpeningById<T>>::mutate(opening_id, |opening| {
            opening.stage = new_active_stage;
        });

        let application_id_crowded_out = can_be_added_destructured
            .would_get_added_success
            .crowded_out_application_id();

        // DONE
        Ok(ApplicationAdded {
            application_id_added: new_application_id,
            application_id_crowded_out,
        })
    }

    /// Deactive an active application.
    /// Does currently not support slashing
    pub fn deactive_application(
        application_id: T::ApplicationId,
        application_stake_unstaking_period: Option<T::BlockNumber>,
        role_stake_unstaking_period: Option<T::BlockNumber>,
    ) -> Result<(), DeactivateApplicationError> {
        // Check that application id is valid, and if so,
        // grab corresponding application and opening.
        let (application, opening) = ensure_application_exists!(
            T,
            application_id,
            DeactivateApplicationError::ApplicationDoesNotExist,
            auto_fetch_opening
        )?;

        // Application is active
        ensure_eq!(
            application.stage,
            hiring::ApplicationStage::Active,
            DeactivateApplicationError::ApplicationNotActive
        );

        // Opening is accepting applications
        let (active_stage, ..) = ensure_opening_is_active!(
            opening.stage,
            DeactivateApplicationError::OpeningNotAcceptingApplications
        )?;

        active_stage.ensure_active_opening_is_accepting_applications(
            DeactivateApplicationError::OpeningNotAcceptingApplications,
        )?;

        // Ensure unstaking periods are OK.
        ensure_opt_unstaking_period_is_ok!(
            application_stake_unstaking_period,
            opening.application_staking_policy,
            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Application),
            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Application)
        )?;

        ensure_opt_unstaking_period_is_ok!(
            role_stake_unstaking_period,
            opening.role_staking_policy,
            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Role),
            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Role)
        )?;

        //
        // == MUTATION SAFE ==
        //

        // Deactive application
        let result = Self::try_to_initiate_application_deactivation(
            &application,
            application_id,
            application_stake_unstaking_period,
            role_stake_unstaking_period,
            hiring::ApplicationDeactivationCause::External,
        );

        assert_ne!(result, ApplicationDeactivationInitiationResult::Ignored);

        // DONE
        Ok(())
    }

    /// The stake, with the given id, was unstaked.
    pub fn unstaked(stake_id: T::StakeId) -> UnstakedResult {
        // Ignore unstaked
        if !<ApplicationIdByStakingId<T>>::exists(stake_id) {
            return UnstakedResult::StakeIdNonExistent;
        }

        // Get application
        let application_id = <ApplicationIdByStakingId<T>>::get(stake_id);

        assert!(<ApplicationById<T>>::exists(application_id));

        let application = <ApplicationById<T>>::get(application_id);

        // Make sure that we are actually unstaking, ignore otherwise.
        let (deactivation_initiated, cause) = if let ApplicationStage::Unstaking {
            deactivation_initiated,
            cause,
        } = application.stage
        {
            (deactivation_initiated, cause)
        } else {
            return UnstakedResult::ApplicationIsNotUnstaking;
        };

        //
        // == MUTATION SAFE ==
        //

        // Drop stake from stake to application map
        <ApplicationIdByStakingId<T>>::remove(stake_id);
        let current_block_height = <system::Module<T>>::block_number();

        // New application computed
        let mut new_application = application.clone();
        let is_now_done_unstaking = new_application.unstake_application(
            current_block_height,
            deactivation_initiated,
            cause,
            stake_id,
        );

        // Update to new application
        <ApplicationById<T>>::insert(&application_id, new_application);

        // If the application is now finished compeleting any pending unstaking process,
        // then we need to update the opening counters, and make the deactivation callback.
        if is_now_done_unstaking {
            // Update Opening
            // We know the stage MUST be active, hence mutate is certain.
            <OpeningById<T>>::mutate(application.opening_id, |opening| {
                opening.change_opening_stage_after_application_unstaked();
            });

            // Call handler
            T::ApplicationDeactivatedHandler::deactivated(&application_id, cause);
            return UnstakedResult::Unstaked;
        }

        UnstakedResult::UnstakingInProgress
    }
}

/*
 *  === Application Deactivated Handler  ======
 */

/// Handles application deactivation with a cause
pub trait ApplicationDeactivatedHandler<T: Trait> {
    /// An application, with the given id, was fully deactivated, with the
    /// given cause, and was put in the inactive state.
    fn deactivated(application_id: &T::ApplicationId, cause: hiring::ApplicationDeactivationCause);
}

/// Helper implementation so we can provide multiple handlers by grouping handlers in tuple pairs.
/// For example for three handlers, A, B and C we can set the StakingEventHandler type on the trait to:
/// type StakingEventHandler = ((A, B), C)
impl<T: Trait> ApplicationDeactivatedHandler<T> for () {
    fn deactivated(
        _application_id: &T::ApplicationId,
        _cause: hiring::ApplicationDeactivationCause,
    ) {
    }
}

/*
 *  ======== API types bound to the Trait ========
 */

/// Error due to attempting to fill an opening.
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum FillOpeningError<T: Trait> {
    /// Opening does not exist
    OpeningDoesNotExist,

    /// Opening is not in review period
    OpeningNotInReviewPeriodStage,

    /// Provided unstaking period is too short
    UnstakingPeriodTooShort(StakePurpose, ApplicationOutcomeInFilledOpening),

    /// Provided redundant unstaking period
    RedundantUnstakingPeriodProvided(StakePurpose, ApplicationOutcomeInFilledOpening),

    /// Application does not exist
    ApplicationDoesNotExist(T::ApplicationId),

    /// Application is not in active stage
    ApplicationNotInActiveStage(T::ApplicationId),

    /// Application is not for the opening
    ApplicationForWrongOpening(T::ApplicationId),
}

/// Product of ensure_can_add_application()
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct DestructuredApplicationCanBeAddedEvaluation<T: Trait> {
    /// Opening object
    pub opening: Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,

    /// Active opening stage
    pub active_stage: ActiveOpeningStage<T::BlockNumber>,

    /// Collection of added applicaiton ids
    pub applications_added: BTreeSet<T::ApplicationId>,

    /// Active applications counter
    pub active_application_count: u32,

    /// Unstaking applications counter
    pub unstaking_application_count: u32,

    /// Deactivated applications counter
    pub deactivated_application_count: u32,

    /// Prospects of application adding
    pub would_get_added_success: ApplicationAddedSuccess<T>,
}

impl<T: Trait> DestructuredApplicationCanBeAddedEvaluation<T> {
    pub(crate) fn calculate_total_application_count(&self) -> u32 {
        // TODO: fix so that `number_of_appliations_ever_added` can be invoked.
        // cant do this due to bad design of stage => opening.stage.number_of_appliations_ever_added();
        self.active_application_count
            + self.unstaking_application_count
            + self.deactivated_application_count
    }
}

/// Adding application result
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum ApplicationAddedSuccess<T: Trait> {
    /// Application was added without side-effects
    Unconditionally,

    /// Application has crowded out existing application
    CrowdsOutExistingApplication(T::ApplicationId),
}

impl<T: Trait> ApplicationAddedSuccess<T> {
    pub(crate) fn crowded_out_application_id(&self) -> Option<T::ApplicationId> {
        if let ApplicationAddedSuccess::CrowdsOutExistingApplication(id) = self {
            Some(*id)
        } else {
            None
        }
    }
}

/// Prospects of application. Whether it would be added to the opening.
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum ApplicationWouldGetAddedEvaluation<T: Trait> {
    /// Negative prospects
    No,

    /// Positive prospects
    Yes(ApplicationAddedSuccess<T>),
}

/// Balance alias
pub type BalanceOf<T> =
    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;

/// Balance alias for staking
pub type NegativeImbalance<T> =
    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;

/*
 *  ======== ======== ======== ======== =======
 *  ======== PRIVATE TYPES AND METHODS ========
 *  ======== ======== ======== ======== =======
 */

#[derive(PartialEq, Debug, Clone)]
struct ApplicationsDeactivationsInitiationResult {
    number_of_unstaking_applications: u32,
    number_of_deactivated_applications: u32,
}

type ApplicationBTreeMap<T> = BTreeMap<
    <T as Trait>::ApplicationId,
    hiring::Application<
        <T as Trait>::OpeningId,
        <T as system::Trait>::BlockNumber,
        <T as stake::Trait>::StakeId,
    >,
>;

#[derive(PartialEq, Debug, Clone)]
enum ApplicationDeactivationInitiationResult {
    Ignored, // <= is there a case for kicking this out, making sure that initiation cannot happen when it may fail?
    Unstaking,
    Deactivated,
}

// Opening and application iterators
impl<T: Trait> Module<T> {
    // Iterate through ApplicationById map
    fn application_id_iter_to_map<'a>(
        application_id_iter: impl Iterator<Item = &'a T::ApplicationId>,
    ) -> ApplicationBTreeMap<T> {
        application_id_iter
            .map(|application_id| {
                let application = <ApplicationById<T>>::get(application_id);

                (*application_id, application)
            })
            .collect::<BTreeMap<_, _>>()
    }

    // Compute iterator of openings waiting to begin
    fn openings_waiting_to_begin_iterator(
        now: T::BlockNumber,
    ) -> impl Iterator<
        Item = (
            T::OpeningId,
            Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,
        ),
    > {
        <OpeningById<T>>::enumerate().filter_map(move |(opening_id, opening)| {
            if let hiring::OpeningStage::WaitingToBegin { begins_at_block } = opening.stage {
                if begins_at_block == now {
                    Some((opening_id, opening))
                } else {
                    None
                }
            } else {
                None
            }
        })
    }

    // Compute iterator of openings in expired review period
    fn openings_expired_review_period_iterator(
        now: T::BlockNumber,
    ) -> impl Iterator<
        Item = (
            T::OpeningId,
            Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,
            (BTreeSet<T::ApplicationId>, T::BlockNumber, T::BlockNumber),
        ),
    > {
        <OpeningById<T>>::enumerate().filter_map(move |(opening_id, opening)| {
            if let hiring::OpeningStage::Active {
                ref stage,
                ref applications_added,
                ..
            } = opening.stage
            {
                if let hiring::ActiveOpeningStage::ReviewPeriod {
                    ref started_accepting_applicants_at_block,
                    ref started_review_period_at_block,
                } = stage
                {
                    if now == opening.max_review_period_length + *started_review_period_at_block {
                        Some((
                            opening_id,
                            opening.clone(),
                            (
                                applications_added.clone(),
                                *started_accepting_applicants_at_block,
                                *started_review_period_at_block,
                            ),
                        ))
                    } else {
                        None
                    }
                } else {
                    None
                }
            } else {
                None
            }
        })
    }
}

// Application deactivation logic methods.
impl<T: Trait> Module<T> {
    fn initiate_application_deactivations(
        applications: &ApplicationBTreeMap<T>,
        application_stake_unstaking_period: Option<T::BlockNumber>,
        role_stake_unstaking_period: Option<T::BlockNumber>,
        cause: ApplicationDeactivationCause,
    ) -> ApplicationsDeactivationsInitiationResult {
        // Update stage on active applications, and collect result

        applications
            .iter()
            .map(
                |(application_id, application)| -> ApplicationDeactivationInitiationResult {
                    // Initiate deactivations!
                    Self::try_to_initiate_application_deactivation(
                        application,
                        *application_id,
                        application_stake_unstaking_period,
                        role_stake_unstaking_period,
                        cause,
                    )
                },
            )
            .fold(
                // Initiatial reducer value
                ApplicationsDeactivationsInitiationResult {
                    number_of_unstaking_applications: 0,
                    number_of_deactivated_applications: 0,
                },
                |acc, deactivation_result| {
                    // Update accumulator counters based on what actually happened
                    match deactivation_result {
                        ApplicationDeactivationInitiationResult::Ignored => acc,

                        ApplicationDeactivationInitiationResult::Unstaking => {
                            ApplicationsDeactivationsInitiationResult {
                                number_of_unstaking_applications: 1 + acc
                                    .number_of_unstaking_applications,
                                number_of_deactivated_applications: acc
                                    .number_of_deactivated_applications,
                            }
                        }

                        ApplicationDeactivationInitiationResult::Deactivated => {
                            ApplicationsDeactivationsInitiationResult {
                                number_of_unstaking_applications: acc
                                    .number_of_unstaking_applications,
                                number_of_deactivated_applications: 1 + acc
                                    .number_of_deactivated_applications,
                            }
                        }
                    }
                },
            )
    }

    /// Initiates
    fn try_to_initiate_application_deactivation(
        application: &Application<T::OpeningId, T::BlockNumber, T::StakeId>,
        application_id: T::ApplicationId,
        application_stake_unstaking_period: Option<T::BlockNumber>,
        role_stake_unstaking_period: Option<T::BlockNumber>,
        cause: hiring::ApplicationDeactivationCause,
    ) -> ApplicationDeactivationInitiationResult {
        match application.stage {
            ApplicationStage::Active => {
                // Initiate unstaking of any active application stake
                let application_was_unstaked = Self::opt_infallible_unstake(
                    application.active_application_staking_id,
                    application_stake_unstaking_period,
                );

                // Only unstake role stake for a non successful result ie. not Hired
                let role_was_unstaked = cause != hiring::ApplicationDeactivationCause::Hired
                    && Self::opt_infallible_unstake(
                        application.active_role_staking_id,
                        role_stake_unstaking_period,
                    );

                // Capture if any unstaking occured at all
                let was_unstaked = application_was_unstaked || role_was_unstaked;

                // Grab current block height
                let current_block_height = <system::Module<T>>::block_number();

                /*
                 * TODO:
                 * There should be a single transformation based on
                 * was_unstaked which renders a new value for `application.stage`
                 * and `opening.stage`, which guarantees to only produces new values
                 * for given variant values, but the state machine types are currently
                 * not well organised to support this.
                 *
                 * Likewise the construction of hiring::OpeningStage::Active below
                 * is a wreck because of this.
                 *
                 * Issue: https://github.com/Joystream/joystream/issues/36#issuecomment-539567407
                 */

                // Figure out new stage for the application
                let new_application_stage = if was_unstaked {
                    ApplicationStage::Unstaking {
                        deactivation_initiated: current_block_height,
                        cause,
                    }
                } else {
                    ApplicationStage::Inactive {
                        deactivation_initiated: current_block_height,
                        deactivated: current_block_height,
                        cause,
                    }
                };

                // Update the application stage
                <ApplicationById<T>>::mutate(application_id, |application| {
                    application.stage = new_application_stage;
                });

                // Update counters on opening
                <OpeningById<T>>::mutate(application.opening_id, |opening| {
                    // NB: This ugly byref destructuring is same issue as pointed out multiple times now.
                    if let hiring::OpeningStage::Active {
                        ref stage,
                        ref applications_added,
                        ref active_application_count,
                        ref unstaking_application_count,
                        ref deactivated_application_count,
                    } = opening.stage
                    {
                        assert!(*active_application_count > 0);

                        let new_active_application_count = active_application_count - 1;

                        let new_unstaking_application_count =
                            unstaking_application_count + if was_unstaked { 1 } else { 0 };

                        let new_deactivated_application_count =
                            deactivated_application_count + if was_unstaked { 0 } else { 1 };

                        opening.stage = hiring::OpeningStage::Active {
                            stage: stage.clone(),
                            applications_added: applications_added.clone(),
                            active_application_count: new_active_application_count,
                            unstaking_application_count: new_unstaking_application_count,
                            deactivated_application_count: new_deactivated_application_count,
                        };
                    } else {
                        panic!("opening stage must be 'Active'");
                    }
                });

                // Call handler(s)
                if was_unstaked {
                    T::ApplicationDeactivatedHandler::deactivated(&application_id, cause);
                }

                // Return conclusion
                if was_unstaked {
                    ApplicationDeactivationInitiationResult::Unstaking
                } else {
                    ApplicationDeactivationInitiationResult::Deactivated
                }
            }
            _ => ApplicationDeactivationInitiationResult::Ignored,
        }
    }

    /// Tries to unstake, based on a stake id which, if set, MUST
    /// be ready to be unstaked, with an optional unstaking period.
    ///
    /// Returns whether unstaking was actually initiated.
    fn opt_infallible_unstake(
        opt_stake_id: Option<T::StakeId>,
        opt_unstaking_period: Option<T::BlockNumber>,
    ) -> bool {
        if let Some(stake_id) = opt_stake_id {
            // `initiate_unstaking` MUST hold, is runtime invariant, false means code is broken.
            // But should we do panic in runtime? Is there safer way?

            assert!(T::StakeHandlerProvider::staking()
                .initiate_unstaking(&stake_id, opt_unstaking_period)
                .is_ok());
        }

        opt_stake_id.is_some()
    }
}

// Stake initiation
impl<T: Trait> Module<T> {
    fn infallible_opt_stake_initiation(
        opt_imbalance: Option<NegativeImbalance<T>>,
        application_id: &T::ApplicationId,
    ) -> Option<T::StakeId> {
        if let Some(imbalance) = opt_imbalance {
            Some(Self::infallible_stake_initiation_on_application(
                imbalance,
                application_id,
            ))
        } else {
            None
        }
    }

    fn infallible_stake_initiation_on_application(
        imbalance: NegativeImbalance<T>,
        application_id: &T::ApplicationId,
    ) -> T::StakeId {
        // Create stake
        let new_stake_id = T::StakeHandlerProvider::staking().create_stake();

        // Keep track of this stake id to process unstaking callbacks that may
        // be invoked later.
        // NB: We purposefully update state to reflect mapping _before_ initiating staking below
        // in order to be safe from race conditions arising out of third party code executing in callback of staking module.

        // MUST never already be a key for new stake, false means code is broken.
        // But should we do panic in runtime? Is there safer way?
        assert!(!<ApplicationIdByStakingId<T>>::exists(new_stake_id));

        <ApplicationIdByStakingId<T>>::insert(new_stake_id, application_id);

        // Initiate staking
        //
        // MUST work, is runtime invariant, false means code is broken.
        // But should we do panic in runtime? Is there safer way?
        assert_eq!(
            T::StakeHandlerProvider::staking().stake(&new_stake_id, imbalance),
            Ok(())
        );

        new_stake_id
    }
}

// Conditions for adding application
impl<T: Trait> Module<T> {
    /// Evaluates prospects for a new application
    ///
    pub(crate) fn would_application_get_added(
        possible_opening_application_rationing_policy: &Option<ApplicationRationingPolicy>,
        opening_applicants: &BTreeSet<T::ApplicationId>,
        opt_role_stake_balance: &Option<BalanceOf<T>>,
        opt_application_stake_balance: &Option<BalanceOf<T>>,
    ) -> ApplicationWouldGetAddedEvaluation<T> {
        // Check whether any rationing policy is set at all, if not
        // then there is no rationing, and any application can get added.
        let application_rationing_policy = if let Some(application_rationing_policy) =
            possible_opening_application_rationing_policy
        {
            application_rationing_policy
        } else {
            return ApplicationWouldGetAddedEvaluation::Yes(
                ApplicationAddedSuccess::Unconditionally,
            );
        };

        // Map with applications
        let applications_map = Self::application_id_iter_to_map(opening_applicants.iter());

        let active_applications_with_stake_iter =
            applications_map
                .iter()
                .filter_map(|(application_id, application)| {
                    if application.stage == hiring::ApplicationStage::Active {
                        let total_stake =
                            Self::get_opt_stake_amount(application.active_role_staking_id)
                                + Self::get_opt_stake_amount(
                                    application.active_application_staking_id,
                                );

                        Some((application_id, application, total_stake))
                    } else {
                        None
                    }
                });

        // Compute number of active applications
        let number_of_active_applications = active_applications_with_stake_iter.clone().count();

        // Check whether the current number of _active_ applicants is either at or above the maximum
        // limit, if not, then we can add at least one additional application,
        // otherwise we must evaluate whether this new application would specifically get added.
        if (number_of_active_applications as u32)
            < application_rationing_policy.max_active_applicants
        {
            return ApplicationWouldGetAddedEvaluation::Yes(
                ApplicationAddedSuccess::Unconditionally,
            );
        }

        // Here we try to figure out if the new application
        // has sufficient stake to crowd out one of the already
        // active applicants.

        // The total stake of new application
        let total_stake_of_new_application = opt_role_stake_balance.unwrap_or_default()
            + opt_application_stake_balance.unwrap_or_default();

        // The total stake of all current active applications
        let opt_min_item = active_applications_with_stake_iter
            .clone()
            .min_by_key(|(_, _, total_stake)| *total_stake);

        if let Some((application_id, _, lowest_active_total_stake)) = opt_min_item {
            // Finally we compare the two and come up with a final evaluation
            if total_stake_of_new_application <= lowest_active_total_stake {
                ApplicationWouldGetAddedEvaluation::No // stake too low!
            } else {
                ApplicationWouldGetAddedEvaluation::Yes(
                    ApplicationAddedSuccess::CrowdsOutExistingApplication(*application_id),
                )
            }
        } else {
            panic!("`number_of_active_applications` (length of `active_applications_iter`) == 0")
        }
    }

    fn get_opt_stake_amount(stake_id: Option<T::StakeId>) -> BalanceOf<T> {
        stake_id.map_or(<BalanceOf<T> as Zero>::zero(), |stake_id| {
            // INVARIANT: stake MUST exist in the staking module
            assert!(T::StakeHandlerProvider::staking().stake_exists(stake_id));

            let stake = T::StakeHandlerProvider::staking().get_stake(stake_id);

            match stake.staking_status {
                // INVARIANT: stake MUST be in the staked state.
                stake::StakingStatus::Staked(staked_state) => staked_state.staked_amount,
                _ => panic!("stake MUST be in the staked state."),
            }
        })
    }

    pub(crate) fn create_stake_balance(
        opt_stake_imbalance: &Option<NegativeImbalance<T>>,
    ) -> Option<BalanceOf<T>> {
        if let Some(ref imbalance) = opt_stake_imbalance {
            Some(imbalance.peek())
        } else {
            None
        }
    }
}

/*
 *  === Stake module wrappers  ======
 */

/// Defines stake module interface
#[cfg_attr(
    all(any(test, feature = "test"), not(target_arch = "wasm32")),
    automock
)]
pub trait StakeHandler<T: StakeTrait> {
    /// Adds a new Stake which is NotStaked, created at given block, into stakes map.
    fn create_stake(&self) -> T::StakeId;

    /// to the module's account, and the corresponding staked_balance is set to this amount in the new Staked state.
    /// On error, as the negative imbalance is not returned to the caller, it is the caller's responsibility to return the funds
    /// back to the source (by creating a new positive imbalance)
    fn stake(
        &self,
        new_stake_id: &T::StakeId,
        imbalance: NegativeImbalance<T>,
    ) -> Result<(), StakeActionError<stake::StakingError>>;

    /// Checks whether stake exists by its id
    fn stake_exists(&self, stake_id: T::StakeId) -> bool;

    /// Acquires stake by id
    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId>;

    /// Initiate unstaking of a Staked stake.
    fn initiate_unstaking(
        &self,
        stake_id: &T::StakeId,
        unstaking_period: Option<T::BlockNumber>,
    ) -> Result<(), StakeActionError<InitiateUnstakingError>>;
}

/// Allows to provide different StakeHandler implementation. Useful for mocks.
pub trait StakeHandlerProvider<T: Trait> {
    /// Returns StakeHandler. Mock entry point for stake module.
    fn staking() -> Rc<RefCell<dyn StakeHandler<T>>>;
}

impl<T: Trait> StakeHandlerProvider<T> for Module<T> {
    /// Returns StakeHandler. Mock entry point for stake module.
    fn staking() -> Rc<RefCell<dyn StakeHandler<T>>> {
        Rc::new(RefCell::new(HiringStakeHandler {}))
    }
}

/// Default stake module logic implementation
pub struct HiringStakeHandler;
impl<T: Trait> StakeHandler<T> for HiringStakeHandler {
    fn create_stake(&self) -> T::StakeId {
        <stake::Module<T>>::create_stake()
    }

    fn stake(
        &self,
        new_stake_id: &T::StakeId,
        imbalance: NegativeImbalance<T>,
    ) -> Result<(), StakeActionError<StakingError>> {
        <stake::Module<T>>::stake(new_stake_id, imbalance)
    }

    fn stake_exists(&self, stake_id: T::StakeId) -> bool {
        <stake::Stakes<T>>::exists(stake_id)
    }

    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId> {
        <stake::Stakes<T>>::get(stake_id)
    }

    fn initiate_unstaking(
        &self,
        stake_id: &T::StakeId,
        unstaking_period: Option<T::BlockNumber>,
    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
        <stake::Module<T>>::initiate_unstaking(&stake_id, unstaking_period)
    }
}

// Proxy implementation of StakeHandler trait to simplify calls via staking() method
// Allows to get rid of borrow() calls,
// eg.: T::StakeHandlerProvider::staking().get_stake(stake_id);
// instead of T::StakeHandlerProvider::staking().borrow().get_stake(stake_id);
impl<T: Trait> StakeHandler<T> for Rc<RefCell<dyn StakeHandler<T>>> {
    fn create_stake(&self) -> T::StakeId {
        self.borrow().create_stake()
    }

    fn stake(
        &self,
        new_stake_id: &T::StakeId,
        imbalance: NegativeImbalance<T>,
    ) -> Result<(), StakeActionError<StakingError>> {
        self.borrow().stake(new_stake_id, imbalance)
    }

    fn stake_exists(&self, stake_id: T::StakeId) -> bool {
        self.borrow().stake_exists(stake_id)
    }

    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId> {
        self.borrow().get_stake(stake_id)
    }

    fn initiate_unstaking(
        &self,
        stake_id: &T::StakeId,
        unstaking_period: Option<T::BlockNumber>,
    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
        self.borrow().initiate_unstaking(stake_id, unstaking_period)
    }
}
