package keeper

import (
	"fmt"

	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"

	earntypes "github.com/kava-labs/kava/x/earn/types"
	"github.com/kava-labs/kava/x/incentive/types"
	liquidtypes "github.com/kava-labs/kava/x/liquid/types"
)

const (
	SecondsPerYear = 31536000
)

// GetStakingAPR returns the total APR for staking and incentive rewards
func GetStakingAPR(ctx sdk.Context, k Keeper, params types.Params) (sdk.Dec, error) {
	// Get staking APR + incentive APR
	inflationRate := k.mintKeeper.GetMinter(ctx).Inflation
	communityTax := k.distrKeeper.GetCommunityTax(ctx)

	bondedTokens := k.stakingKeeper.TotalBondedTokens(ctx)
	circulatingSupply := k.bankKeeper.GetSupply(ctx, types.BondDenom)

	// Staking APR = (Inflation Rate * (1 - Community Tax)) / (Bonded Tokens / Circulating Supply)
	stakingAPR := inflationRate.
		Mul(sdk.OneDec().Sub(communityTax)).
		Quo(sdk.NewDecFromInt(bondedTokens).
			Quo(sdk.NewDecFromInt(circulatingSupply.Amount)))

	// Get incentive APR
	bkavaRewardPeriod, found := params.EarnRewardPeriods.GetMultiRewardPeriod(liquidtypes.DefaultDerivativeDenom)
	if !found {
		// No incentive rewards for bkava, only staking rewards
		return stakingAPR, nil
	}

	// Total amount of bkava in earn vaults, this may be lower than total bank
	// supply of bkava as some bkava may not be deposited in earn vaults
	totalEarnBkavaDeposited := sdk.ZeroInt()

	var iterErr error
	k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) {
		if !k.liquidKeeper.IsDerivativeDenom(ctx, record.TotalShares.Denom) {
			return false
		}

		vaultValue, err := k.earnKeeper.GetVaultTotalValue(ctx, record.TotalShares.Denom)
		if err != nil {
			iterErr = err
			return false
		}

		totalEarnBkavaDeposited = totalEarnBkavaDeposited.Add(vaultValue.Amount)

		return false
	})

	if iterErr != nil {
		return sdk.ZeroDec(), iterErr
	}

	// Incentive APR = rewards per second * seconds per year / total supplied to earn vaults
	// Override collateral type to use "kava" instead of "bkava" when fetching
	incentiveAPY, err := GetAPYFromMultiRewardPeriod(ctx, k, types.BondDenom, bkavaRewardPeriod, totalEarnBkavaDeposited)
	if err != nil {
		return sdk.ZeroDec(), err
	}

	totalAPY := stakingAPR.Add(incentiveAPY)
	return totalAPY, nil
}

// GetAPYFromMultiRewardPeriod calculates the APY for a given MultiRewardPeriod
func GetAPYFromMultiRewardPeriod(
	ctx sdk.Context,
	k Keeper,
	collateralType string,
	rewardPeriod types.MultiRewardPeriod,
	totalSupply sdkmath.Int,
) (sdk.Dec, error) {
	if totalSupply.IsZero() {
		return sdk.ZeroDec(), nil
	}

	// Get USD value of collateral type
	collateralUSDValue, err := k.pricefeedKeeper.GetCurrentPrice(ctx, getMarketID(collateralType))
	if err != nil {
		return sdk.ZeroDec(), fmt.Errorf(
			"failed to get price for incentive collateralType %s with market ID %s: %w",
			collateralType, getMarketID(collateralType), err,
		)
	}

	// Total USD value of the collateral type total supply
	totalSupplyUSDValue := sdk.NewDecFromInt(totalSupply).Mul(collateralUSDValue.Price)

	totalUSDRewardsPerSecond := sdk.ZeroDec()

	// In many cases, RewardsPerSecond are assets that are different from the
	// CollateralType, so we need to use the USD value of CollateralType and
	// RewardsPerSecond to determine the APY.
	for _, reward := range rewardPeriod.RewardsPerSecond {
		// Get USD value of 1 unit of reward asset type, using TWAP
		rewardDenomUSDValue, err := k.pricefeedKeeper.GetCurrentPrice(ctx, getMarketID(reward.Denom))
		if err != nil {
			return sdk.ZeroDec(), fmt.Errorf("failed to get price for RewardsPerSecond asset %s: %w", reward.Denom, err)
		}

		rewardPerSecond := sdk.NewDecFromInt(reward.Amount).Mul(rewardDenomUSDValue.Price)
		totalUSDRewardsPerSecond = totalUSDRewardsPerSecond.Add(rewardPerSecond)
	}

	// APY = USD rewards per second * seconds per year / USD total supplied
	apy := totalUSDRewardsPerSecond.
		MulInt64(SecondsPerYear).
		Quo(totalSupplyUSDValue)

	return apy, nil
}

func getMarketID(denom string) string {
	// Rewrite denoms as pricefeed has different names for some assets,
	// e.g. "ukava" -> "kava", "erc20/multichain/usdc" -> "usdc"
	// bkava is not included as it is handled separately

	// TODO: Replace hardcoded conversion with possible params set somewhere
	// to be more flexible. E.g. a map of denoms to pricefeed market denoms in
	// pricefeed params.
	switch denom {
	case types.BondDenom:
		denom = "kava"
	case "erc20/multichain/usdc":
		denom = "usdc"
	case "erc20/multichain/usdt":
		denom = "usdt"
	case "erc20/multichain/dai":
		denom = "dai"
	}

	return fmt.Sprintf("%s:usd:30", denom)
}