package keeper

import (
	"context"
	"sort"

	errorsmod "cosmossdk.io/errors"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/types/query"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

type QueryServer struct {
	keeper Keeper
}

// NewQueryServer returns an implementation of the pricefeed MsgServer interface
// for the provided Keeper.
func NewQueryServerImpl(keeper Keeper) types.QueryServer {
	return &QueryServer{keeper: keeper}
}

var _ types.QueryServer = QueryServer{}

// Params queries all parameters of the cdp module.
func (s QueryServer) Params(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(c)
	params := s.keeper.GetParams(sdkCtx)

	return &types.QueryParamsResponse{Params: params}, nil
}

// Accounts queries the CDP module accounts.
func (s QueryServer) Accounts(c context.Context, req *types.QueryAccountsRequest) (*types.QueryAccountsResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

	cdpAccAccount := s.keeper.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
	liquidatorAccAccount := s.keeper.accountKeeper.GetModuleAccount(ctx, types.LiquidatorMacc)

	accounts := []authtypes.ModuleAccount{
		*cdpAccAccount.(*authtypes.ModuleAccount),
		*liquidatorAccAccount.(*authtypes.ModuleAccount),
	}

	return &types.QueryAccountsResponse{Accounts: accounts}, nil
}

// TotalPrincipal queries the total principal of a given collateral type.
func (s QueryServer) TotalPrincipal(c context.Context, req *types.QueryTotalPrincipalRequest) (*types.QueryTotalPrincipalResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

	var queryCollateralTypes []string

	if req.CollateralType != "" {
		// Single collateralType provided
		queryCollateralTypes = append(queryCollateralTypes, req.CollateralType)
	} else {
		// No collateralType provided, respond with all of them
		keeperParams := s.keeper.GetParams(ctx)

		for _, collateral := range keeperParams.CollateralParams {
			queryCollateralTypes = append(queryCollateralTypes, collateral.Type)
		}
	}

	var collateralPrincipals types.TotalPrincipals

	for _, queryType := range queryCollateralTypes {
		// Hardcoded to default USDX
		principalAmount := s.keeper.GetTotalPrincipal(ctx, queryType, types.DefaultStableDenom)
		// Wrap it in an sdk.Coin
		totalAmountCoin := sdk.NewCoin(types.DefaultStableDenom, principalAmount)

		totalPrincipal := types.NewTotalPrincipal(queryType, totalAmountCoin)
		collateralPrincipals = append(collateralPrincipals, totalPrincipal)
	}

	return &types.QueryTotalPrincipalResponse{
		TotalPrincipal: collateralPrincipals,
	}, nil
}

// TotalCollateral queries the total collateral of a given collateral type.
func (s QueryServer) TotalCollateral(c context.Context, req *types.QueryTotalCollateralRequest) (*types.QueryTotalCollateralResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

	params := s.keeper.GetParams(ctx)
	denomCollateralTypes := make(map[string][]string)

	// collect collateral types for each denom
	for _, collateralParam := range params.CollateralParams {
		denomCollateralTypes[collateralParam.Denom] = append(denomCollateralTypes[collateralParam.Denom], collateralParam.Type)
	}

	// sort collateral types alphabetically
	for _, collateralTypes := range denomCollateralTypes {
		sort.Slice(collateralTypes, func(i int, j int) bool {
			return collateralTypes[i] < collateralTypes[j]
		})
	}

	// get total collateral in all cdps
	cdpAccount := s.keeper.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
	totalCdpCollateral := s.keeper.bankKeeper.GetAllBalances(ctx, cdpAccount.GetAddress())

	var totalCollaterals types.TotalCollaterals

	for denom, collateralTypes := range denomCollateralTypes {
		// skip any denoms that do not match the requested collateral type
		if req.CollateralType != "" {
			match := false
			for _, ctype := range collateralTypes {
				if ctype == req.CollateralType {
					match = true
				}
			}

			if !match {
				continue
			}
		}

		totalCollateral := totalCdpCollateral.AmountOf(denom)

		// we need to query individual cdps for denoms with more than one collateral type
		for i := len(collateralTypes) - 1; i > 0; i-- {
			cdps := s.keeper.GetAllCdpsByCollateralType(ctx, collateralTypes[i])

			collateral := sdk.ZeroInt()

			for _, cdp := range cdps {
				collateral = collateral.Add(cdp.Collateral.Amount)
			}

			totalCollateral = totalCollateral.Sub(collateral)

			// if we have no collateralType filter, or the filter matches, include it in the response
			if req.CollateralType == "" || collateralTypes[i] == req.CollateralType {
				totalCollaterals = append(totalCollaterals, types.NewTotalCollateral(collateralTypes[i], sdk.NewCoin(denom, collateral)))
			}

			// skip the rest of the cdp queries if we have a matching filter
			if collateralTypes[i] == req.CollateralType {
				break
			}
		}

		if req.CollateralType == "" || collateralTypes[0] == req.CollateralType {
			// all leftover total collateral belongs to the first collateral type
			totalCollaterals = append(totalCollaterals, types.NewTotalCollateral(collateralTypes[0], sdk.NewCoin(denom, totalCollateral)))
		}
	}

	// sort to ensure deterministic response
	sort.Slice(totalCollaterals, func(i int, j int) bool {
		return totalCollaterals[i].CollateralType < totalCollaterals[j].CollateralType
	})

	return &types.QueryTotalCollateralResponse{
		TotalCollateral: totalCollaterals,
	}, nil
}

// Cdps queries all active CDPs.
func (s QueryServer) Cdps(c context.Context, req *types.QueryCdpsRequest) (*types.QueryCdpsResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

	// Filter CDPs
	filteredCDPs, err := GrpcFilterCDPs(ctx, s.keeper, *req)
	if err != nil {
		return nil, err
	}

	return &types.QueryCdpsResponse{
		Cdps: filteredCDPs,
		// TODO: Use built in pagination and respond
		Pagination: nil,
	}, nil
}

// Cdp queries a CDP with the input owner address and collateral type.
func (s QueryServer) Cdp(c context.Context, req *types.QueryCdpRequest) (*types.QueryCdpResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

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

	_, valid := s.keeper.GetCollateral(ctx, req.CollateralType)
	if !valid {
		return nil, errorsmod.Wrap(types.ErrInvalidCollateral, req.CollateralType)
	}

	cdp, found := s.keeper.GetCdpByOwnerAndCollateralType(ctx, owner, req.CollateralType)
	if !found {
		return nil, errorsmod.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", req.Owner, req.CollateralType)
	}

	cdpResponse := s.keeper.LoadCDPResponse(ctx, cdp)

	return &types.QueryCdpResponse{
		Cdp: cdpResponse,
	}, nil
}

// Deposits queries deposits associated with the CDP owned by an address for a collateral type.
func (s QueryServer) Deposits(c context.Context, req *types.QueryDepositsRequest) (*types.QueryDepositsResponse, error) {
	ctx := sdk.UnwrapSDKContext(c)

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

	_, valid := s.keeper.GetCollateral(ctx, req.CollateralType)
	if !valid {
		return nil, errorsmod.Wrap(types.ErrInvalidCollateral, req.CollateralType)
	}

	cdp, found := s.keeper.GetCdpByOwnerAndCollateralType(ctx, owner, req.CollateralType)
	if !found {
		return nil, errorsmod.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", req.Owner, req.CollateralType)
	}

	deposits := s.keeper.GetDeposits(ctx, cdp.ID)

	return &types.QueryDepositsResponse{
		Deposits: deposits,
	}, nil
}

// FilterCDPs queries the store for all CDPs that match query req
func GrpcFilterCDPs(ctx sdk.Context, k Keeper, req types.QueryCdpsRequest) (types.CDPResponses, error) {
	// TODO: Ideally use query.Paginate() here over existing FilterCDPs. However
	// This is difficult to use different CDP indices and specific keeper
	// methods without iterating over all CDPs.
	page, limit, err := query.ParsePagination(req.Pagination)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, err.Error())
	}

	// Owner address is optional, only parse if it's provided otherwise it will
	// respond with an error
	var owner sdk.AccAddress
	if req.Owner != "" {
		owner, err = sdk.AccAddressFromBech32(req.Owner)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid owner address")
		}
	}

	ratio := sdk.ZeroDec()

	if req.Ratio != "" {
		ratio, err = sdk.NewDecFromStr(req.Ratio)
		if err != nil {
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "invalid ratio")
			}
		}
	}

	legacyParams := types.NewQueryCdpsParams(page, limit, req.CollateralType, owner, req.ID, ratio)

	cdps, err := FilterCDPs(ctx, k, legacyParams)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, err.Error())
	}

	var cdpResponses types.CDPResponses
	for _, cdp := range cdps {
		cdpResponse := types.CDPResponse{
			ID:                     cdp.ID,
			Owner:                  cdp.Owner.String(),
			Type:                   cdp.Type,
			Collateral:             cdp.Collateral,
			Principal:              cdp.Principal,
			AccumulatedFees:        cdp.AccumulatedFees,
			FeesUpdated:            cdp.FeesUpdated,
			InterestFactor:         cdp.InterestFactor.String(),
			CollateralValue:        cdp.CollateralValue,
			CollateralizationRatio: cdp.CollateralizationRatio.String(),
		}
		cdpResponses = append(cdpResponses, cdpResponse)
	}

	return cdpResponses, nil
}