package keeper

import (
	"context"
	"strings"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

const (
	RewardTypeHard        = "hard"
	RewardTypeUSDXMinting = "usdx_minting"
	RewardTypeDelegator   = "delegator"
	RewardTypeSwap        = "swap"
	RewardTypeSavings     = "savings"
	RewardTypeEarn        = "earn"
)

type queryServer struct {
	keeper Keeper
}

var _ types.QueryServer = queryServer{}

// NewQueryServerImpl creates a new server for handling gRPC queries.
func NewQueryServerImpl(keeper Keeper) types.QueryServer {
	return &queryServer{
		keeper: keeper,
	}
}

func (s queryServer) Params(
	ctx context.Context,
	req *types.QueryParamsRequest,
) (*types.QueryParamsResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	return &types.QueryParamsResponse{
		Params: s.keeper.GetParams(sdkCtx),
	}, nil
}

func (s queryServer) Rewards(
	ctx context.Context,
	req *types.QueryRewardsRequest,
) (*types.QueryRewardsResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	res := types.QueryRewardsResponse{}

	hasOwner := req.Owner != ""

	var owner sdk.AccAddress
	if hasOwner {
		addr, err := sdk.AccAddressFromBech32(req.Owner)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err)
		}

		owner = addr
	}

	if err := s.queryRewards(sdkCtx, &res, owner, hasOwner, req.RewardType); err != nil {
		return nil, err
	}

	if !req.Unsynchronized {
		if err := s.synchronizeRewards(sdkCtx, &res); err != nil {
			return nil, err
		}
	}

	return &res, nil
}

func (s queryServer) RewardFactors(
	ctx context.Context,
	req *types.QueryRewardFactorsRequest,
) (*types.QueryRewardFactorsResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	var usdxFactors types.RewardIndexes
	s.keeper.IterateUSDXMintingRewardFactors(sdkCtx, func(collateralType string, factor sdk.Dec) (stop bool) {
		usdxFactors = usdxFactors.With(collateralType, factor)
		return false
	})

	var supplyFactors types.MultiRewardIndexes
	s.keeper.IterateHardSupplyRewardIndexes(sdkCtx, func(denom string, indexes types.RewardIndexes) (stop bool) {
		supplyFactors = supplyFactors.With(denom, indexes)
		return false
	})

	var borrowFactors types.MultiRewardIndexes
	s.keeper.IterateHardBorrowRewardIndexes(sdkCtx, func(denom string, indexes types.RewardIndexes) (stop bool) {
		borrowFactors = borrowFactors.With(denom, indexes)
		return false
	})

	var delegatorFactors types.MultiRewardIndexes
	s.keeper.IterateDelegatorRewardIndexes(sdkCtx, func(denom string, indexes types.RewardIndexes) (stop bool) {
		delegatorFactors = delegatorFactors.With(denom, indexes)
		return false
	})

	var swapFactors types.MultiRewardIndexes
	s.keeper.IterateSwapRewardIndexes(sdkCtx, func(poolID string, indexes types.RewardIndexes) (stop bool) {
		swapFactors = swapFactors.With(poolID, indexes)
		return false
	})

	var savingsFactors types.MultiRewardIndexes
	s.keeper.IterateSavingsRewardIndexes(sdkCtx, func(denom string, indexes types.RewardIndexes) (stop bool) {
		savingsFactors = savingsFactors.With(denom, indexes)
		return false
	})

	var earnFactors types.MultiRewardIndexes
	s.keeper.IterateEarnRewardIndexes(sdkCtx, func(denom string, indexes types.RewardIndexes) (stop bool) {
		earnFactors = earnFactors.With(denom, indexes)
		return false
	})

	return &types.QueryRewardFactorsResponse{
		UsdxMintingRewardFactors: usdxFactors,
		HardSupplyRewardFactors:  supplyFactors,
		HardBorrowRewardFactors:  borrowFactors,
		DelegatorRewardFactors:   delegatorFactors,
		SwapRewardFactors:        swapFactors,
		SavingsRewardFactors:     savingsFactors,
		EarnRewardFactors:        earnFactors,
	}, nil
}

func (s queryServer) Apy(
	ctx context.Context,
	req *types.QueryApyRequest,
) (*types.QueryApyResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	params := s.keeper.GetParams(sdkCtx)
	var apys types.APYs

	// bkava APY (staking + incentive rewards)
	stakingAPR, err := GetStakingAPR(sdkCtx, s.keeper, params)
	if err != nil {
		return nil, err
	}

	apys = append(apys, types.NewAPY(liquidtypes.DefaultDerivativeDenom, stakingAPR))

	// Incentive only APYs
	for _, param := range params.EarnRewardPeriods {
		// Skip bkava as it's calculated earlier with staking rewards
		if param.CollateralType == liquidtypes.DefaultDerivativeDenom {
			continue
		}

		// Value in the vault in the same denom as CollateralType
		vaultTotalValue, err := s.keeper.earnKeeper.GetVaultTotalValue(sdkCtx, param.CollateralType)
		if err != nil {
			return nil, err
		}
		apy, err := GetAPYFromMultiRewardPeriod(sdkCtx, s.keeper, param.CollateralType, param, vaultTotalValue.Amount)
		if err != nil {
			return nil, err
		}

		apys = append(apys, types.NewAPY(param.CollateralType, apy))
	}

	return &types.QueryApyResponse{
		Earn: apys,
	}, nil
}

// queryRewards queries the rewards for a given owner and reward type, updating
// the response with the results in place.
func (s queryServer) queryRewards(
	ctx sdk.Context,
	res *types.QueryRewardsResponse,
	owner sdk.AccAddress,
	hasOwner bool,
	rewardType string,
) error {
	rewardType = strings.ToLower(rewardType)
	isAllRewards := rewardType == ""

	if !rewardTypeIsValid(rewardType) {
		return status.Errorf(codes.InvalidArgument, "invalid reward type for owner %s: %s", owner, rewardType)
	}

	if isAllRewards || rewardType == RewardTypeUSDXMinting {
		if hasOwner {
			usdxMintingClaim, foundUsdxMintingClaim := s.keeper.GetUSDXMintingClaim(ctx, owner)
			if foundUsdxMintingClaim {
				res.USDXMintingClaims = append(res.USDXMintingClaims, usdxMintingClaim)
			}
		} else {
			usdxMintingClaims := s.keeper.GetAllUSDXMintingClaims(ctx)
			res.USDXMintingClaims = append(res.USDXMintingClaims, usdxMintingClaims...)
		}
	}

	if isAllRewards || rewardType == RewardTypeHard {
		if hasOwner {
			hardClaim, foundHardClaim := s.keeper.GetHardLiquidityProviderClaim(ctx, owner)
			if foundHardClaim {
				res.HardLiquidityProviderClaims = append(res.HardLiquidityProviderClaims, hardClaim)
			}
		} else {
			hardClaims := s.keeper.GetAllHardLiquidityProviderClaims(ctx)
			res.HardLiquidityProviderClaims = append(res.HardLiquidityProviderClaims, hardClaims...)
		}
	}

	if isAllRewards || rewardType == RewardTypeDelegator {
		if hasOwner {
			delegatorClaim, foundDelegatorClaim := s.keeper.GetDelegatorClaim(ctx, owner)
			if foundDelegatorClaim {
				res.DelegatorClaims = append(res.DelegatorClaims, delegatorClaim)
			}
		} else {
			delegatorClaims := s.keeper.GetAllDelegatorClaims(ctx)
			res.DelegatorClaims = append(res.DelegatorClaims, delegatorClaims...)
		}
	}

	if isAllRewards || rewardType == RewardTypeSwap {
		if hasOwner {
			swapClaim, foundSwapClaim := s.keeper.GetSwapClaim(ctx, owner)
			if foundSwapClaim {
				res.SwapClaims = append(res.SwapClaims, swapClaim)
			}
		} else {
			swapClaims := s.keeper.GetAllSwapClaims(ctx)
			res.SwapClaims = append(res.SwapClaims, swapClaims...)
		}
	}

	if isAllRewards || rewardType == RewardTypeSavings {
		if hasOwner {
			savingsClaim, foundSavingsClaim := s.keeper.GetSavingsClaim(ctx, owner)
			if foundSavingsClaim {
				res.SavingsClaims = append(res.SavingsClaims, savingsClaim)
			}
		} else {
			savingsClaims := s.keeper.GetAllSavingsClaims(ctx)
			res.SavingsClaims = append(res.SavingsClaims, savingsClaims...)
		}
	}

	if isAllRewards || rewardType == RewardTypeEarn {
		if hasOwner {
			earnClaim, foundEarnClaim := s.keeper.GetEarnClaim(ctx, owner)
			if foundEarnClaim {
				res.EarnClaims = append(res.EarnClaims, earnClaim)
			}
		} else {
			earnClaims := s.keeper.GetAllEarnClaims(ctx)
			res.EarnClaims = append(res.EarnClaims, earnClaims...)
		}
	}

	return nil
}

// synchronizeRewards synchronizes all non-empty rewards in place.
func (s queryServer) synchronizeRewards(
	ctx sdk.Context,
	res *types.QueryRewardsResponse,
) error {
	// Synchronize all non-empty rewards
	for i, claim := range res.USDXMintingClaims {
		res.USDXMintingClaims[i] = s.keeper.SimulateUSDXMintingSynchronization(ctx, claim)
	}

	for i, claim := range res.HardLiquidityProviderClaims {
		res.HardLiquidityProviderClaims[i] = s.keeper.SimulateHardSynchronization(ctx, claim)
	}

	for i, claim := range res.DelegatorClaims {
		res.DelegatorClaims[i] = s.keeper.SimulateDelegatorSynchronization(ctx, claim)
	}

	for i, claim := range res.SwapClaims {
		syncedClaim, found := s.keeper.GetSynchronizedSwapClaim(ctx, claim.Owner)
		if !found {
			return status.Errorf(codes.Internal, "previously found swap claim for owner %s should still be found", claim.Owner)
		}
		res.SwapClaims[i] = syncedClaim
	}

	for i, claim := range res.SavingsClaims {
		syncedClaim, found := s.keeper.GetSynchronizedSavingsClaim(ctx, claim.Owner)
		if !found {
			return status.Errorf(codes.Internal, "previously found savings claim for owner %s should still be found", claim.Owner)
		}
		res.SavingsClaims[i] = syncedClaim
	}

	for i, claim := range res.EarnClaims {
		syncedClaim, found := s.keeper.GetSynchronizedEarnClaim(ctx, claim.Owner)
		if !found {
			return status.Errorf(codes.Internal, "previously found earn claim for owner %s should still be found", claim.Owner)
		}
		res.EarnClaims[i] = syncedClaim
	}

	return nil
}

func rewardTypeIsValid(rewardType string) bool {
	return rewardType == "" ||
		rewardType == RewardTypeHard ||
		rewardType == RewardTypeUSDXMinting ||
		rewardType == RewardTypeDelegator ||
		rewardType == RewardTypeSwap ||
		rewardType == RewardTypeSavings ||
		rewardType == RewardTypeEarn
}