package v2

import (
	"bytes"
	"errors"
	"fmt"

	errorsmod "cosmossdk.io/errors"

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

	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/internal"
	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/types"
	clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
	channeltypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/v2/types"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/api"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
)

var (
	_ api.IBCModule            = (*IBCMiddleware)(nil)
	_ exported.Acknowledgement = (*RecvAcknowledgement)(nil)
)

// Create internal implementation of exported.Acknowledgement
// to pass the app acknowledgement bytes to contractKeeper IBCReceivePacketCallback
// interface
type RecvAcknowledgement []byte

// RecvPacket only passes callback to contract if the acknowledgement
// is successful. Thus, we can just return true here.
func (rack RecvAcknowledgement) Success() bool {
	return !bytes.Equal(rack, channeltypesv2.ErrorAcknowledgement[:])
}

// RecvPacket passes the application acknowledgment directly to contract
func (rack RecvAcknowledgement) Acknowledgement() []byte {
	return rack
}

// IBCMiddleware implements the IBC v2 middleware interface
// with the underlying application.
type IBCMiddleware struct {
	app             types.CallbacksCompatibleModuleV2
	writeAckWrapper api.WriteAcknowledgementWrapper

	contractKeeper types.ContractKeeper
	chanKeeperV2   types.ChannelKeeperV2

	// maxCallbackGas defines the maximum amount of gas that a callback actor can ask the
	// relayer to pay for. If a callback fails due to insufficient gas, the entire tx
	// is reverted if the relayer hadn't provided the minimum(userDefinedGas, maxCallbackGas).
	// If the actor hasn't defined a gas limit, then it is assumed to be the maxCallbackGas.
	maxCallbackGas uint64
}

// NewIBCMiddleware creates a new IBCMiddleware instance given the keeper and underlying application.
// The underlying application must implement the required callback interfaces.
func NewIBCMiddleware(
	app api.IBCModule, writeAckWrapper api.WriteAcknowledgementWrapper,
	contractKeeper types.ContractKeeper, chanKeeperV2 types.ChannelKeeperV2, maxCallbackGas uint64,
) IBCMiddleware {
	packetDataUnmarshalerApp, ok := app.(types.CallbacksCompatibleModuleV2)
	if !ok {
		panic(fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil)))
	}

	if contractKeeper == nil {
		panic(errors.New("contract keeper cannot be nil"))
	}

	if writeAckWrapper == nil {
		panic(errors.New("write acknowledgement wrapper cannot be nil"))
	}

	if chanKeeperV2 == nil {
		panic(errors.New("channel keeper v2 cannot be nil"))
	}

	if maxCallbackGas == 0 {
		panic(errors.New("maxCallbackGas cannot be zero"))
	}

	return IBCMiddleware{
		app:             packetDataUnmarshalerApp,
		writeAckWrapper: writeAckWrapper,
		contractKeeper:  contractKeeper,
		chanKeeperV2:    chanKeeperV2,
		maxCallbackGas:  maxCallbackGas,
	}
}

// WithWriteAckWrapper sets the WriteAcknowledgementWrapper for the middleware.
func (im *IBCMiddleware) WithWriteAckWrapper(writeAckWrapper api.WriteAcknowledgementWrapper) {
	im.writeAckWrapper = writeAckWrapper
}

// GetWriteAckWrapper returns the WriteAckWrapper
func (im *IBCMiddleware) GetWriteAckWrapper() api.WriteAcknowledgementWrapper {
	return im.writeAckWrapper
}

// OnSendPacket implements source callbacks for sending packets.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback returns an error, panics, or runs out of gas, then
// the packet send is rejected.
func (im IBCMiddleware) OnSendPacket(
	ctx sdk.Context,
	sourceClient string,
	destinationClient string,
	sequence uint64,
	payload channeltypesv2.Payload,
	signer sdk.AccAddress,
) error {
	err := im.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer)
	if err != nil {
		return err
	}

	packetData, err := im.app.UnmarshalPacketData(payload)
	// OnSendPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return nil
	}

	cbData, isCbPacket, err := types.GetCallbackData(
		packetData, payload.GetVersion(), payload.GetSourcePort(),
		ctx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey,
	)
	// OnSendPacket is not blocked if the packet does not opt-in to callbacks
	if !isCbPacket {
		return nil
	}
	// OnSendPacket is blocked if the packet opts-in to callbacks but the callback data is invalid
	if err != nil {
		return err
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		return im.contractKeeper.IBCSendPacketCallback(
			cachedCtx, payload.SourcePort, sourceClient, clienttypes.Height{}, 0, payload.Value, cbData.CallbackAddress, cbData.SenderAddress, payload.Version,
		)
	}

	err = internal.ProcessCallback(ctx, types.CallbackTypeSendPacket, cbData, callbackExecutor)
	// contract keeper is allowed to reject the packet send.
	if err != nil {
		return err
	}

	types.EmitCallbackEvent(ctx, payload.SourcePort, sourceClient, sequence, types.CallbackTypeSendPacket, cbData, nil)
	return nil
}

// OnRecvPacket implements the ReceivePacket destination callbacks for the ibc-callbacks middleware during
// synchronous packet acknowledgement.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are
// reverted via a panic.
func (im IBCMiddleware) OnRecvPacket(
	ctx sdk.Context,
	sourceClient string,
	destinationClient string,
	sequence uint64,
	payload channeltypesv2.Payload,
	relayer sdk.AccAddress,
) channeltypesv2.RecvPacketResult {
	recvResult := im.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer)
	// if ack is nil (asynchronous acknowledgements), then the callback will be handled in WriteAcknowledgement
	// if ack is not successful, all state changes are reverted. If a packet cannot be received, then there is
	// no need to execute a callback on the receiving chain.
	if recvResult.Status == channeltypesv2.PacketStatus_Async || recvResult.Status == channeltypesv2.PacketStatus_Failure {
		return recvResult
	}

	packetData, err := im.app.UnmarshalPacketData(payload)
	// OnRecvPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return recvResult
	}

	cbData, isCbPacket, err := types.GetCallbackData(
		packetData, payload.GetVersion(), payload.GetDestinationPort(),
		ctx.GasMeter().GasRemaining(), im.maxCallbackGas, types.DestinationCallbackKey,
	)
	// OnRecvPacket is not blocked if the packet does not opt-in to callbacks
	if !isCbPacket {
		return recvResult
	}
	// OnRecvPacket is blocked if the packet opts-in to callbacks but the callback data is invalid
	if err != nil {
		return channeltypesv2.RecvPacketResult{
			Status: channeltypesv2.PacketStatus_Failure,
		}
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		// reconstruct a channel v1 packet from the v2 packet
		// in order to preserve the same interface for the contract keeper
		packetv1 := channeltypes.Packet{
			Sequence:           sequence,
			SourcePort:         payload.SourcePort,
			SourceChannel:      sourceClient,
			DestinationPort:    payload.DestinationPort,
			DestinationChannel: destinationClient,
			Data:               payload.Value,
			TimeoutHeight:      clienttypes.Height{},
			TimeoutTimestamp:   0,
		}
		// wrap the individual acknowledgement into the RecvAcknowledgement since it implements the exported.Acknowledgement interface
		// since we return early on failure, we are guaranteed that the ack is a successful acknowledgement
		ack := RecvAcknowledgement(recvResult.Acknowledgement)
		return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packetv1, ack, cbData.CallbackAddress, payload.Version)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = internal.ProcessCallback(ctx, types.CallbackTypeReceivePacket, cbData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, payload.DestinationPort, destinationClient, sequence,
		types.CallbackTypeReceivePacket, cbData, err,
	)
	if err != nil {
		return channeltypesv2.RecvPacketResult{
			Status: channeltypesv2.PacketStatus_Failure,
		}
	}

	return recvResult
}

// OnAcknowledgementPacket implements source callbacks for acknowledgement packets.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are
// reverted via a panic.
func (im IBCMiddleware) OnAcknowledgementPacket(
	ctx sdk.Context,
	sourceClient string,
	destinationClient string,
	sequence uint64,
	acknowledgement []byte,
	payload channeltypesv2.Payload,
	relayer sdk.AccAddress,
) error {
	// we first call the underlying app to handle the acknowledgement
	err := im.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer)
	if err != nil {
		return err
	}

	packetData, err := im.app.UnmarshalPacketData(payload)
	// OnAcknowledgementPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return nil
	}

	cbData, isCbPacket, err := types.GetCallbackData(
		packetData, payload.GetVersion(), payload.GetSourcePort(),
		ctx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey,
	)
	// OnAcknowledgementPacket is not blocked if the packet does not opt-in to callbacks
	if !isCbPacket {
		return nil
	}
	// OnAcknowledgementPacket is blocked if the packet opts-in to callbacks but the callback data is invalid
	// This should never occur since this error is already checked `OnSendPacket`
	if err != nil {
		return err
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		// reconstruct a channel v1 packet from the v2 packet
		// in order to preserve the same interface for the contract keeper
		packetv1 := channeltypes.Packet{
			Sequence:           sequence,
			SourcePort:         payload.SourcePort,
			SourceChannel:      sourceClient,
			DestinationPort:    payload.DestinationPort,
			DestinationChannel: destinationClient,
			Data:               payload.Value,
			TimeoutHeight:      clienttypes.Height{},
			TimeoutTimestamp:   0,
		}
		// NOTE: The callback is receiving the acknowledgement that the application received for its particular payload.
		// In the case of a successful acknowledgement, this will be the acknowledgement sent by the counterparty application for the given payload
		// In the case of an error acknowledgement, this will be the sentinel error acknowledgement bytes defined by IBC v2 protocol.
		// Thus, the contract must be aware that the sentinel error acknowledgement signals a failed receive
		// and the contract must handle this error case and the corresponding success case (ie ack != ErrorAcknowledgement) accordingly.
		return im.contractKeeper.IBCOnAcknowledgementPacketCallback(
			cachedCtx, packetv1, acknowledgement, relayer, cbData.CallbackAddress, cbData.SenderAddress, payload.Version,
		)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = internal.ProcessCallback(ctx, types.CallbackTypeAcknowledgementPacket, cbData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, payload.SourcePort, sourceClient, sequence,
		types.CallbackTypeAcknowledgementPacket, cbData, err,
	)

	return nil
}

// OnTimeoutPacket implements timeout source callbacks for the ibc-callbacks middleware.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are
// reverted via a panic.
// OnTimeoutPacket is executed when a packet has timed out on the receiving chain.
func (im IBCMiddleware) OnTimeoutPacket(
	ctx sdk.Context,
	sourceClient string,
	destinationClient string,
	sequence uint64,
	payload channeltypesv2.Payload,
	relayer sdk.AccAddress,
) error {
	err := im.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer)
	if err != nil {
		return err
	}

	packetData, err := im.app.UnmarshalPacketData(payload)
	if err != nil {
		return err
	}

	cbData, isCbPacket, err := types.GetCallbackData(
		packetData, payload.GetVersion(), payload.GetSourcePort(),
		ctx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey,
	)
	// OnTimeoutPacket is not blocked if the packet does not opt-in to callbacks
	if !isCbPacket {
		return nil
	}
	// OnTimeoutPacket is blocked if the packet opts-in to callbacks but the callback data is invalid
	// This should never occur since this error is already checked `OnSendPacket`
	if err != nil {
		return err
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		// reconstruct a channel v1 packet from the v2 packet
		// in order to preserve the same interface for the contract keeper
		packetv1 := channeltypes.Packet{
			Sequence:           sequence,
			SourcePort:         payload.SourcePort,
			SourceChannel:      sourceClient,
			DestinationPort:    payload.DestinationPort,
			DestinationChannel: destinationClient,
			Data:               payload.Value,
			TimeoutHeight:      clienttypes.Height{},
			TimeoutTimestamp:   0,
		}
		return im.contractKeeper.IBCOnTimeoutPacketCallback(
			cachedCtx, packetv1, relayer, cbData.CallbackAddress, cbData.SenderAddress, payload.Version,
		)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = internal.ProcessCallback(ctx, types.CallbackTypeTimeoutPacket, cbData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, payload.SourcePort, sourceClient, sequence,
		types.CallbackTypeTimeoutPacket, cbData, err,
	)

	return nil
}

// WriteAcknowledgement implements the ReceivePacket destination callbacks for the ibc-callbacks middleware
// during asynchronous packet acknowledgement.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are
// reverted via a panic.
func (im IBCMiddleware) WriteAcknowledgement(
	ctx sdk.Context,
	clientID string,
	sequence uint64,
	ack channeltypesv2.Acknowledgement,
) error {
	packet, found := im.chanKeeperV2.GetAsyncPacket(ctx, clientID, sequence)
	if !found {
		return errorsmod.Wrapf(channeltypesv2.ErrInvalidAcknowledgement, "async packet not found for clientID (%s) and sequence (%d)", clientID, sequence)
	}

	err := im.writeAckWrapper.WriteAcknowledgement(ctx, clientID, sequence, ack)
	if err != nil {
		return err
	}

	// NOTE: use first payload as the payload that is being handled by callbacks middleware
	// must reconsider if multipacket data gets supported with async packets
	// TRACKING ISSUE: https://github.com/cosmos/ibc-go/issues/7950
	if len(packet.Payloads) != 1 {
		return errorsmod.Wrapf(channeltypesv2.ErrInvalidAcknowledgement, "async packet has multiple payloads")
	}
	payload := packet.Payloads[0]

	packetData, err := im.app.UnmarshalPacketData(payload)
	if err != nil {
		return err
	}

	cbData, isCbPacket, err := types.GetCallbackData(
		packetData, payload.GetVersion(), payload.GetDestinationPort(),
		ctx.GasMeter().GasRemaining(), im.maxCallbackGas, types.DestinationCallbackKey,
	)
	// WriteAcknowledgement is not blocked if the packet does not opt-in to callbacks
	if !isCbPacket {
		return nil
	}
	// WriteAcknowledgement is blocked if the packet opts-in to callbacks but the callback data is invalid
	// This should never occur since this error is already checked `OnRecvPacket`
	if err != nil {
		return err
	}

	recvResult := channeltypesv2.RecvPacketResult{
		Status:          channeltypesv2.PacketStatus_Success,
		Acknowledgement: ack.AppAcknowledgements[0],
	}
	callbackExecutor := func(cachedCtx sdk.Context) error {
		// reconstruct a channel v1 packet from the v2 packet
		// in order to preserve the same interface for the contract keeper
		packetv1 := channeltypes.Packet{
			Sequence:           sequence,
			SourcePort:         payload.SourcePort,
			SourceChannel:      packet.SourceClient,
			DestinationPort:    payload.DestinationPort,
			DestinationChannel: packet.DestinationClient,
			Data:               payload.Value,
			TimeoutHeight:      clienttypes.Height{},
			TimeoutTimestamp:   0,
		}
		// wrap the individual acknowledgement into the channeltypesv2.Acknowledgement since it implements the exported.Acknowledgement interface
		var ack channeltypesv2.Acknowledgement
		if recvResult.Status == channeltypesv2.PacketStatus_Failure {
			ack = channeltypesv2.NewAcknowledgement(channeltypesv2.ErrorAcknowledgement[:])
		} else {
			ack = channeltypesv2.NewAcknowledgement(recvResult.Acknowledgement)
		}
		return im.contractKeeper.IBCReceivePacketCallback(
			cachedCtx, packetv1, ack, cbData.CallbackAddress, payload.Version,
		)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = internal.ProcessCallback(ctx, types.CallbackTypeReceivePacket, cbData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, payload.DestinationPort, clientID, sequence,
		types.CallbackTypeReceivePacket, cbData, err,
	)

	return nil
}
