package keeper

import (
	"context"
	"fmt"

	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"
	vestingexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

type queryServer struct {
	keeper Keeper
}

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

var _ types.QueryServer = queryServer{}

func (s queryServer) DelegatedBalance(
	goCtx context.Context,
	req *types.QueryDelegatedBalanceRequest,
) (*types.QueryDelegatedBalanceResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	delegator, err := sdk.AccAddressFromBech32(req.Delegator)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid delegator address: %s", err)
	}

	delegated := s.getDelegatedBalance(ctx, delegator)

	bondDenom := s.keeper.stakingKeeper.BondDenom(ctx)
	vesting := s.getVesting(ctx, delegator).AmountOf(bondDenom)

	vestingDelegated := sdk.MinInt(vesting, delegated)
	vestedDelegated := delegated.Sub(vestingDelegated)

	res := types.QueryDelegatedBalanceResponse{
		Vested:  sdk.NewCoin(bondDenom, vestedDelegated),
		Vesting: sdk.NewCoin(bondDenom, vestingDelegated),
	}
	return &res, nil
}

func (s queryServer) TotalSupply(
	goCtx context.Context,
	req *types.QueryTotalSupplyRequest,
) (*types.QueryTotalSupplyResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	totalValue, err := s.keeper.GetTotalDerivativeValue(ctx)
	if err != nil {
		return nil, err
	}

	return &types.QueryTotalSupplyResponse{
		Height: ctx.BlockHeight(),
		Result: []sdk.Coin{totalValue},
	}, nil
}

func (s queryServer) getDelegatedBalance(ctx sdk.Context, delegator sdk.AccAddress) sdkmath.Int {
	balance := sdk.ZeroDec()

	s.keeper.stakingKeeper.IterateDelegatorDelegations(ctx, delegator, func(delegation stakingtypes.Delegation) bool {
		validator, found := s.keeper.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr())
		if !found {
			panic(fmt.Sprintf("validator %s for delegation not found", delegation.GetValidatorAddr()))
		}
		tokens := validator.TokensFromSharesTruncated(delegation.GetShares())
		balance = balance.Add(tokens)

		return false
	})
	return balance.TruncateInt()
}

func (s queryServer) getVesting(ctx sdk.Context, delegator sdk.AccAddress) sdk.Coins {
	acc := s.keeper.accountKeeper.GetAccount(ctx, delegator)
	if acc == nil {
		// account doesn't exist so amount vesting is 0
		return nil
	}
	vestAcc, ok := acc.(vestingexported.VestingAccount)
	if !ok {
		// account is not vesting type, so amount vesting is 0
		return nil
	}
	return vestAcc.GetVestingCoins(ctx.BlockTime())
}