package keeper

import (
	"time"

	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	"github.com/kava-labs/kava/x/community/types"
)

const nanosecondsInOneSecond = int64(1000000000)

// PayoutAccumulatedStakingRewards calculates and transfers taking rewards to the fee collector address
func (k Keeper) PayoutAccumulatedStakingRewards(ctx sdk.Context) {
	// get module parameters which define the amount of rewards to payout per second
	params := k.mustGetParams(ctx)
	currentBlockTime := ctx.BlockTime()
	state := k.GetStakingRewardsState(ctx)

	// we have un-initialized state -- set accumulation time and exit since there is nothing to do
	if state.LastAccumulationTime.IsZero() {
		state.LastAccumulationTime = currentBlockTime

		k.SetStakingRewardsState(ctx, state)

		return
	}

	// get the denom for staking
	stakingRewardDenom := k.stakingKeeper.BondDenom(ctx)

	// we fetch the community pool balance to ensure only accumulate rewards up to the current balance
	communityPoolBalance := sdkmath.LegacyNewDecFromInt(k.bankKeeper.GetBalance(ctx, k.moduleAddress, stakingRewardDenom).Amount)

	// calculate staking reward payout capped to community pool balance
	truncatedRewards, truncationError := calculateStakingRewards(
		currentBlockTime,
		state.LastAccumulationTime,
		state.LastTruncationError,
		params.StakingRewardsPerSecond,
		communityPoolBalance,
	)

	// only payout if the truncated rewards are non-zero
	if !truncatedRewards.IsZero() {
		transferAmount := sdk.NewCoins(sdk.NewCoin(stakingRewardDenom, truncatedRewards))

		if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, authtypes.FeeCollectorName, transferAmount); err != nil {
			// we check for a valid balance and rewards can never be negative so panic since this will only
			// occur in cases where the chain is running in an invalid state
			panic(err)
		}

		// emit event with amount transferred
		ctx.EventManager().EmitEvent(
			sdk.NewEvent(
				types.EventTypeStakingRewardsPaid,
				sdk.NewAttribute(types.AttributeKeyStakingRewardAmount, transferAmount.String()),
			),
		)

	}

	// update accumulation state
	state.LastAccumulationTime = currentBlockTime
	// if the community pool balance is zero, this also resets the truncation error
	state.LastTruncationError = truncationError

	// save state
	k.SetStakingRewardsState(ctx, state)
}

// calculateStakingRewards takes the currentBlockTime, state of last accumulation, rewards per second, and the community pool balance
// in order to calculate the total payout since the last accumulation time.  It returns the truncated payout amount and the truncation error.
func calculateStakingRewards(currentBlockTime, lastAccumulationTime time.Time, lastTruncationError, stakingRewardsPerSecond, communityPoolBalance sdkmath.LegacyDec) (sdkmath.Int, sdkmath.LegacyDec) {
	// we get the duration since we last accumulated, then use nanoseconds for full precision available
	durationSinceLastPayout := currentBlockTime.Sub(lastAccumulationTime)
	nanosecondsSinceLastPayout := sdkmath.LegacyNewDec(durationSinceLastPayout.Nanoseconds())

	// We multiply by nanoseconds first, then divide by conversion to avoid loss of precision.
	// This multiplication is also tested against very large values so we are safe from overflow
	// in normal operations.
	accumulatedRewards := nanosecondsSinceLastPayout.Mul(stakingRewardsPerSecond).QuoInt64(nanosecondsInOneSecond)
	// Ensure we add any error from previous truncations
	accumulatedRewards = accumulatedRewards.Add(lastTruncationError)

	if communityPoolBalance.LT(accumulatedRewards) {
		accumulatedRewards = communityPoolBalance
	}

	// we truncate since we can only transfer whole units
	truncatedRewards := accumulatedRewards.TruncateDec()
	// the truncation error to carry over to the next accumulation
	truncationError := accumulatedRewards.Sub(truncatedRewards)

	return truncatedRewards.TruncateInt(), truncationError
}
