package keeper

import (
	"errors"
	"fmt"

	channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
	host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
	wasmvmtypes "wasmvm.mleku.dev/types"

	errorsmod "cosmossdk.io/errors"

	"github.com/cosmos/cosmos-sdk/baseapp"
	"github.com/cosmos/cosmos-sdk/codec"
	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

	"wasmd.mleku.dev/x/wasm/types"
)

// msgEncoder is an extension point to customize encodings
type msgEncoder interface {
	// Encode converts wasmvm message to n cosmos message types
	Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error)
}

// MessageRouter ADR 031 request type routing
type MessageRouter interface {
	Handler(msg sdk.Msg) baseapp.MsgServiceHandler
}

// SDKMessageHandler can handles messages that can be encoded into sdk.Message types and routed.
type SDKMessageHandler struct {
	router   MessageRouter
	encoders msgEncoder
	cdc      codec.Codec
}

// NewDefaultMessageHandler constructor
func NewDefaultMessageHandler(
	keeper *Keeper,
	router MessageRouter,
	ics4Wrapper types.ICS4Wrapper,
	channelKeeper types.ChannelKeeper,
	capabilityKeeper types.CapabilityKeeper,
	bankKeeper types.Burner,
	cdc codec.Codec,
	portSource types.ICS20TransferPortSource,
	customEncoders ...*MessageEncoders,
) Messenger {
	encoders := DefaultEncoders(cdc, portSource)
	for _, e := range customEncoders {
		encoders = encoders.Merge(e)
	}
	return NewMessageHandlerChain(
		NewSDKMessageHandler(cdc, router, encoders),
		NewIBCRawPacketHandler(ics4Wrapper, keeper, channelKeeper, capabilityKeeper),
		NewBurnCoinMessageHandler(bankKeeper),
	)
}

func NewSDKMessageHandler(cdc codec.Codec, router MessageRouter, encoders msgEncoder) SDKMessageHandler {
	return SDKMessageHandler{
		cdc:      cdc,
		router:   router,
		encoders: encoders,
	}
}

func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, msgResponses [][]*codectypes.Any, err error) {
	sdkMsgs, err := h.encoders.Encode(ctx, contractAddr, contractIBCPortID, msg)
	if err != nil {
		return nil, nil, nil, err
	}
	for _, sdkMsg := range sdkMsgs {
		res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg)
		if err != nil {
			return nil, nil, nil, err
		}
		// append data and msgResponses
		data = append(data, res.Data)
		msgResponses = append(msgResponses, res.MsgResponses)
		// append events
		sdkEvents := make([]sdk.Event, len(res.Events))
		for i := range res.Events {
			sdkEvents[i] = sdk.Event(res.Events[i])
		}
		events = append(events, sdkEvents...)
	}
	return
}

func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) (*sdk.Result, error) {
	// todo: this block needs proper review from sdk team
	if m, ok := msg.(sdk.HasValidateBasic); ok {
		if err := m.ValidateBasic(); err != nil {
			return nil, err
		}
	}

	// make sure this account can send it
	signers, _, err := h.cdc.GetMsgV1Signers(msg)
	if err != nil {
		return nil, err
	}
	for _, acct := range signers {
		if !contractAddr.Equals(sdk.AccAddress(acct)) {
			return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission")
		}
	}
	// --- end block

	// find the handler and execute it
	if handler := h.router.Handler(msg); handler != nil {
		// ADR 031 request type routing
		msgResult, err := handler(ctx, msg)
		return msgResult, err
	}
	// legacy sdk.Msg routing
	// Assuming that the app developer has migrated all their Msgs to
	// proto messages and has registered all `Msg services`, then this
	// path should never be called, because all those Msgs should be
	// registered within the `msgServiceRouter` already.
	return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg)
}

type callDepthMessageHandler struct {
	Messenger
	MaxCallDepth uint32
}

func (h callDepthMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, msgResponses [][]*codectypes.Any, err error) {
	ctx, err = checkAndIncreaseCallDepth(ctx, h.MaxCallDepth)
	if err != nil {
		return nil, nil, nil, errorsmod.Wrap(err, "dispatch")
	}

	return h.Messenger.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg)
}

// MessageHandlerChain defines a chain of handlers that are called one by one until it can be handled.
type MessageHandlerChain struct {
	handlers []Messenger
}

func NewMessageHandlerChain(first Messenger, others ...Messenger) *MessageHandlerChain {
	r := &MessageHandlerChain{handlers: append([]Messenger{first}, others...)}
	for i := range r.handlers {
		if r.handlers[i] == nil {
			panic(fmt.Sprintf("handler must not be nil at position : %d", i))
		}
	}
	return r
}

// DispatchMsg dispatch message and calls chained handlers one after another in
// order to find the right one to process given message. If a handler cannot
// process given message (returns ErrUnknownMsg), its result is ignored and the
// next handler is executed.
func (m MessageHandlerChain) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) {
	for _, h := range m.handlers {
		events, data, msgResponses, err := h.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg)
		switch {
		case err == nil:
			return events, data, msgResponses, nil
		case errors.Is(err, types.ErrUnknownMsg):
			continue
		default:
			return events, data, msgResponses, err
		}
	}
	return nil, nil, nil, errorsmod.Wrap(types.ErrUnknownMsg, "no handler found")
}

// IBCRawPacketHandler handles IBC.SendPacket messages which are published to an IBC channel.
type IBCRawPacketHandler struct {
	ics4Wrapper      types.ICS4Wrapper
	wasmKeeper       types.IBCContractKeeper
	channelKeeper    types.ChannelKeeper
	capabilityKeeper types.CapabilityKeeper
}

// NewIBCRawPacketHandler constructor
func NewIBCRawPacketHandler(ics4Wrapper types.ICS4Wrapper, wasmKeeper types.IBCContractKeeper,
	channelKeeper types.ChannelKeeper, capabilityKeeper types.CapabilityKeeper) IBCRawPacketHandler {
	return IBCRawPacketHandler{
		ics4Wrapper:      ics4Wrapper,
		wasmKeeper:       wasmKeeper,
		channelKeeper:    channelKeeper,
		capabilityKeeper: capabilityKeeper,
	}
}

// DispatchMsg publishes a raw IBC packet onto the channel.
func (h IBCRawPacketHandler) DispatchMsg(ctx sdk.Context, _ sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) {
	if msg.IBC == nil {
		return nil, nil, nil, types.ErrUnknownMsg
	}
	switch {
	case msg.IBC.SendPacket != nil:
		if contractIBCPortID == "" {
			return nil, nil, nil, errorsmod.Wrapf(types.ErrUnsupportedForContract, "ibc not supported")
		}
		contractIBCChannelID := msg.IBC.SendPacket.ChannelID
		if contractIBCChannelID == "" {
			return nil, nil, nil, errorsmod.Wrapf(types.ErrEmpty, "ibc channel")
		}

		channelCap, ok := h.capabilityKeeper.GetCapability(ctx,
			host.ChannelCapabilityPath(contractIBCPortID, contractIBCChannelID))
		if !ok {
			return nil, nil, nil, errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound,
				"module does not own channel capability")
		}
		seq, err := h.ics4Wrapper.SendPacket(ctx, channelCap, contractIBCPortID, contractIBCChannelID,
			ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.IBC.SendPacket.Timeout.Block), msg.IBC.SendPacket.Timeout.Timestamp,
			msg.IBC.SendPacket.Data)
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "channel")
		}
		moduleLogger(ctx).Debug("ibc packet set", "seq", seq)

		resp := &types.MsgIBCSendResponse{Sequence: seq}
		val, err := resp.Marshal()
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "failed to marshal IBC send response")
		}
		any, err := codectypes.NewAnyWithValue(resp)
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "failed to convert IBC send response to Any")
		}
		msgResponses := [][]*codectypes.Any{{any}}

		return nil, [][]byte{val}, msgResponses, nil
	case msg.IBC.WriteAcknowledgement != nil:
		if contractIBCPortID == "" {
			return nil, nil, nil, errorsmod.Wrapf(types.ErrUnsupportedForContract, "ibc not supported")
		}
		contractIBCChannelID := msg.IBC.WriteAcknowledgement.ChannelID
		if contractIBCChannelID == "" {
			return nil, nil, nil, errorsmod.Wrapf(types.ErrEmpty, "ibc channel")
		}

		channelCap, ok := h.capabilityKeeper.GetCapability(ctx,
			host.ChannelCapabilityPath(contractIBCPortID, contractIBCChannelID))
		if !ok {
			return nil, nil, nil, errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound,
				"module does not own channel capability")
		}

		packet, err := h.wasmKeeper.LoadAsyncAckPacket(ctx, contractIBCPortID, contractIBCChannelID,
			msg.IBC.WriteAcknowledgement.PacketSequence)
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(types.ErrInvalid, "packet")
		}

		err = h.ics4Wrapper.WriteAcknowledgement(ctx, channelCap, packet,
			ContractConfirmStateAck(msg.IBC.WriteAcknowledgement.Ack.Data))
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "acknowledgement")
		}

		// Delete the packet from the store after acknowledgement.
		// This ensures WriteAcknowledgement can only be used once per packet
		// such that overriding the acknowledgement later on is not possible.
		h.wasmKeeper.DeleteAsyncAckPacket(ctx, contractIBCPortID, contractIBCChannelID,
			msg.IBC.WriteAcknowledgement.PacketSequence)

		resp := &types.MsgIBCWriteAcknowledgementResponse{}
		val, err := resp.Marshal()
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "failed to marshal IBC send response")
		}

		any, err := codectypes.NewAnyWithValue(resp)
		if err != nil {
			return nil, nil, nil, errorsmod.Wrap(err, "failed to convert IBC send response to Any")
		}
		msgResponses := [][]*codectypes.Any{{any}}

		return nil, [][]byte{val}, msgResponses, nil
	default:
		return nil, nil, nil, types.ErrUnknownMsg
	}
}

var _ Messenger = MessageHandlerFunc(nil)

// MessageHandlerFunc is a helper to construct a function based message handler.
type MessageHandlerFunc func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, msgResponses [][]*codectypes.Any, err error)

// DispatchMsg delegates dispatching of provided message into the MessageHandlerFunc.
func (m MessageHandlerFunc) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string,
	msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, msgResponses [][]*codectypes.Any, err error) {
	return m(ctx, contractAddr, contractIBCPortID, msg)
}

// NewBurnCoinMessageHandler handles wasmvm.BurnMsg messages
func NewBurnCoinMessageHandler(burner types.Burner) MessageHandlerFunc {
	return func(ctx sdk.Context, contractAddr sdk.AccAddress, _ string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event,
		data [][]byte, msgResponses [][]*codectypes.Any, err error) {
		if msg.Bank != nil && msg.Bank.Burn != nil {
			coins, err := ConvertWasmCoinsToSdkCoins(msg.Bank.Burn.Amount)
			if err != nil {
				return nil, nil, nil, err
			}
			if coins.IsZero() {
				return nil, nil, nil, types.ErrEmpty.Wrap("amount")
			}
			if err := burner.SendCoinsFromAccountToModule(ctx, contractAddr, types.ModuleName, coins); err != nil {
				return nil, nil, nil, errorsmod.Wrap(err, "transfer to module")
			}
			if err := burner.BurnCoins(ctx, types.ModuleName, coins); err != nil {
				return nil, nil, nil, errorsmod.Wrap(err, "burn coins")
			}
			moduleLogger(ctx).Info("Burned", "amount", coins)
			return nil, nil, nil, nil
		}
		return nil, nil, nil, types.ErrUnknownMsg
	}
}
