package keeper

import (
	"encoding/json"
	"fmt"
	"slices"

	wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types"

	errorsmod "cosmossdk.io/errors"
	storetypes "cosmossdk.io/store/types"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	abci "git.cw.tr/mukan-network/mukan-consensus/abci/types"

	"github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types"
)

/*
`queryHandler` is a contextual querier which references the global `ibcwasm.QueryPluginsI`
to handle queries. The global `ibcwasm.QueryPluginsI` points to a `types.QueryPlugins` which
contains two sub-queriers: `types.CustomQuerier` and `types.StargateQuerier`. These sub-queriers
can be replaced by the user through the options api in the keeper.

In addition, the `types.StargateQuerier` references a global `types.QueryRouter` which points
to `baseapp.GRPCQueryRouter`.

This design is based on wasmd's (v0.50.0) querier plugin design.
*/

var _ wasmvmtypes.Querier = (*queryHandler)(nil)

// defaultAcceptList defines a set of default allowed queries made available to the Querier.
var defaultAcceptList = []string{
	"/ibc.core.client.v1.Query/VerifyMembership",
}

// queryHandler is a wrapper around the sdk.Context and the CallerID that calls
// into the query plugins.
type queryHandler struct {
	Ctx      sdk.Context
	Plugins  QueryPlugins
	CallerID string
}

// newQueryHandler returns a default querier that can be used in the contract.
func newQueryHandler(ctx sdk.Context, plugins QueryPlugins, callerID string) *queryHandler {
	return &queryHandler{
		Ctx:      ctx,
		Plugins:  plugins,
		CallerID: callerID,
	}
}

// GasConsumed implements the wasmvmtypes.Querier interface.
func (q *queryHandler) GasConsumed() uint64 {
	return VMGasRegister.ToWasmVMGas(q.Ctx.GasMeter().GasConsumed())
}

// Query implements the wasmvmtypes.Querier interface.
func (q *queryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) ([]byte, error) {
	sdkGas := VMGasRegister.FromWasmVMGas(gasLimit)

	// discard all changes/events in subCtx by not committing the cached context
	subCtx, _ := q.Ctx.WithGasMeter(storetypes.NewGasMeter(sdkGas)).CacheContext()

	// make sure we charge the higher level context even on panic
	defer func() {
		q.Ctx.GasMeter().ConsumeGas(subCtx.GasMeter().GasConsumed(), "contract sub-query")
	}()

	res, err := q.Plugins.HandleQuery(subCtx, q.CallerID, request)
	if err == nil {
		return res, nil
	}

	moduleLogger(q.Ctx).Debug("Redacting query error", "cause", err)
	return nil, redactError(err)
}

type (
	CustomQuerier   func(ctx sdk.Context, request json.RawMessage) ([]byte, error)
	StargateQuerier func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error)

	// QueryPlugins is a list of queriers that can be used to extend the default querier.
	QueryPlugins struct {
		Custom   CustomQuerier
		Stargate StargateQuerier
	}
)

// Merge merges the query plugin with a provided one.
func (e QueryPlugins) Merge(x *QueryPlugins) QueryPlugins {
	// only update if this is non-nil and then only set values
	if x == nil {
		return e
	}

	if x.Custom != nil {
		e.Custom = x.Custom
	}

	if x.Stargate != nil {
		e.Stargate = x.Stargate
	}

	return e
}

// HandleQuery implements the ibcwasm.QueryPluginsI interface.
func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller string, request wasmvmtypes.QueryRequest) ([]byte, error) {
	if request.Stargate != nil {
		return e.Stargate(ctx, request.Stargate)
	}

	if request.Custom != nil {
		return e.Custom(ctx, request.Custom)
	}

	return nil, wasmvmtypes.UnsupportedRequest{Kind: "Unsupported query request"}
}

// NewDefaultQueryPlugins returns the default set of query plugins
func NewDefaultQueryPlugins(queryRouter types.QueryRouter) QueryPlugins {
	return QueryPlugins{
		Custom:   RejectCustomQuerier(),
		Stargate: AcceptListStargateQuerier([]string{}, queryRouter),
	}
}

// AcceptListStargateQuerier allows all queries that are in the provided accept list.
// This function returns protobuf encoded responses in bytes.
func AcceptListStargateQuerier(acceptedQueries []string, queryRouter types.QueryRouter) func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) {
	return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) {
		// append user defined accepted queries to default list defined above.
		acceptedQueries = append(defaultAcceptList, acceptedQueries...)

		isAccepted := slices.Contains(acceptedQueries, request.Path)
		if !isAccepted {
			return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)}
		}

		route := queryRouter.Route(request.Path)
		if route == nil {
			return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)}
		}

		res, err := route(ctx, &abci.RequestQuery{
			Data: request.Data,
			Path: request.Path,
		})
		if err != nil {
			return nil, err
		}
		if res == nil || res.Value == nil {
			return nil, wasmvmtypes.InvalidResponse{Err: "Query response is empty"}
		}

		return res.Value, nil
	}
}

// RejectCustomQuerier rejects all custom queries
func RejectCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) {
	return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) {
		return nil, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}
	}
}

// Wasmd Issue [#759](https://github.com/CosmWasm/wasmd/issues/759)
// Don't return error string for worries of non-determinism
func redactError(err error) error {
	// Do not redact system errors
	// SystemErrors must be created in 08-wasm and we can ensure determinism
	if wasmvmtypes.ToSystemError(err) != nil {
		return err
	}

	codespace, code, _ := errorsmod.ABCIInfo(err, false)
	return fmt.Errorf("codespace: %s, code: %d", codespace, code)
}
