package ibctesting

import (
	"encoding/hex"
	"errors"
	"fmt"
	"slices"
	"strconv"

	"github.com/cosmos/gogoproto/proto"
	testifysuite "github.com/stretchr/testify/suite"

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

	clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	connectiontypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/03-connection/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"
)

// ParseClientIDFromEvents parses events emitted from a MsgCreateClient and returns the
// client identifier.
func ParseClientIDFromEvents(events []abci.Event) (string, error) {
	for _, ev := range events {
		if ev.Type == clienttypes.EventTypeCreateClient {
			if attribute, found := attributeByKey(ev.Attributes, clienttypes.AttributeKeyClientID); found {
				return attribute.Value, nil
			}
		}
	}
	return "", errors.New("client identifier event attribute not found")
}

// ParseConnectionIDFromEvents parses events emitted from a MsgConnectionOpenInit or
// MsgConnectionOpenTry and returns the connection identifier.
func ParseConnectionIDFromEvents(events []abci.Event) (string, error) {
	for _, ev := range events {
		if ev.Type == connectiontypes.EventTypeConnectionOpenInit ||
			ev.Type == connectiontypes.EventTypeConnectionOpenTry {
			if attribute, found := attributeByKey(ev.Attributes, connectiontypes.AttributeKeyConnectionID); found {
				return attribute.Value, nil
			}
		}
	}
	return "", errors.New("connection identifier event attribute not found")
}

// ParseChannelIDFromEvents parses events emitted from a MsgChannelOpenInit or
// MsgChannelOpenTry or a MsgCreateChannel and returns the channel identifier.
func ParseChannelIDFromEvents(events []abci.Event) (string, error) {
	for _, ev := range events {
		if ev.Type == channeltypes.EventTypeChannelOpenInit || ev.Type == channeltypes.EventTypeChannelOpenTry {
			if attribute, found := attributeByKey(ev.Attributes, channeltypes.AttributeKeyChannelID); found {
				return attribute.Value, nil
			}
		}
	}
	return "", errors.New("channel identifier event attribute not found")
}

// ParseV1PacketFromEvents parses events emitted from a send packet and returns
// the first EventTypeSendPacket packet found.
// Returns an error if no packet is found.
func ParseV1PacketFromEvents(events []abci.Event) (channeltypes.Packet, error) {
	packets, err := ParseIBCV1Packets(channeltypes.EventTypeSendPacket, events)
	if err != nil {
		return channeltypes.Packet{}, err
	}
	return packets[0], nil
}

// ParseRecvV1PacketFromEvents parses events emitted from a MsgRecvPacket and returns
// the first EventTypeRecvPacket packet found.
// Returns an error if no packet is found.
func ParseRecvV1PacketFromEvents(events []abci.Event) (channeltypes.Packet, error) {
	packets, err := ParseIBCV1Packets(channeltypes.EventTypeRecvPacket, events)
	if err != nil {
		return channeltypes.Packet{}, err
	}
	return packets[0], nil
}

// ParseIBCV1Packets parses events and returns all the v1 packets found.
// Returns an error if no v1 packet is found.
func ParseIBCV1Packets(eventType string, events []abci.Event) ([]channeltypes.Packet, error) {
	ferr := func(err error) ([]channeltypes.Packet, error) {
		return nil, fmt.Errorf("ibctesting.ParseIBCV1Packets: %w", err)
	}
	var packets []channeltypes.Packet
	for _, ev := range events {
		if ev.Type != eventType {
			continue
		}

		var packet channeltypes.Packet
		for _, attr := range ev.Attributes {
			switch attr.Key {
			case channeltypes.AttributeKeyDataHex:
				data, err := hex.DecodeString(attr.Value)
				if err != nil {
					return ferr(err)
				}
				packet.Data = data
			case channeltypes.AttributeKeySequence:
				seq, err := strconv.ParseUint(attr.Value, 10, 64)
				if err != nil {
					return ferr(err)
				}

				packet.Sequence = seq

			case channeltypes.AttributeKeySrcPort:
				packet.SourcePort = attr.Value

			case channeltypes.AttributeKeySrcChannel:
				packet.SourceChannel = attr.Value

			case channeltypes.AttributeKeyDstPort:
				packet.DestinationPort = attr.Value

			case channeltypes.AttributeKeyDstChannel:
				packet.DestinationChannel = attr.Value

			case channeltypes.AttributeKeyTimeoutHeight:
				height, err := clienttypes.ParseHeight(attr.Value)
				if err != nil {
					return ferr(err)
				}

				packet.TimeoutHeight = height

			case channeltypes.AttributeKeyTimeoutTimestamp:
				timestamp, err := strconv.ParseUint(attr.Value, 10, 64)
				if err != nil {
					return ferr(err)
				}

				packet.TimeoutTimestamp = timestamp

			default:
				continue
			}
		}

		packets = append(packets, packet)
	}
	if len(packets) == 0 {
		return ferr(errors.New("acknowledgement event attribute not found"))
	}
	return packets, nil
}

// ParseIBCV2Packets parses events and returns all the v2 packets found.
// Returns an error if no v2 packet is found.
func ParseIBCV2Packets(eventType string, events []abci.Event) ([]channeltypesv2.Packet, error) {
	packets := make([]channeltypesv2.Packet, 0)
	for _, event := range events {
		if event.Type != eventType {
			continue
		}

		var packet channeltypesv2.Packet
	Loop:
		for _, attr := range event.Attributes {
			switch attr.Key {
			// If we find a complete packet, we unmarshall it. We don't need to check for any other
			// attributes from this event.
			case channeltypesv2.AttributeKeyEncodedPacketHex:
				data, err := hex.DecodeString(attr.Value)
				if err != nil {
					return nil, fmt.Errorf("ibctesting.ParseIBCV2Packets: %w", err)
				}
				if err := proto.Unmarshal(data, &packet); err != nil {
					return nil, fmt.Errorf("ibctesting.ParseIBCV2Packets: %w", err)
				}
				break Loop

			case channeltypesv2.AttributeKeySequence:
				seq, err := strconv.ParseUint(attr.Value, 10, 64)
				if err != nil {
					return nil, fmt.Errorf("ibctesting.ParseIBCV2Packets: %w", err)
				}
				packet.Sequence = seq

			case channeltypesv2.AttributeKeyTimeoutTimestamp:
				timestamp, err := strconv.ParseUint(attr.Value, 10, 64)
				if err != nil {
					return nil, fmt.Errorf("ibctesting.ParseIBCV2Packets: %w", err)
				}
				packet.TimeoutTimestamp = timestamp

			case channeltypesv2.AttributeKeyDstClient:
				packet.DestinationClient = attr.Value

			case channeltypesv2.AttributeKeySrcClient:
				packet.SourceClient = attr.Value
			}
		}
		packets = append(packets, packet)
	}

	if len(packets) == 0 {
		return nil, errors.New("no IBC v2 packets found in events")
	}

	return packets, nil
}

// ParseAckFromEvents parses events emitted from a MsgRecvPacket and returns the
// acknowledgement.
func ParseAckFromEvents(events []abci.Event) ([]byte, error) {
	for _, ev := range events {
		if ev.Type == channeltypes.EventTypeWriteAck {
			if attribute, found := attributeByKey(ev.Attributes, channeltypes.AttributeKeyAckHex); found {
				value, err := hex.DecodeString(attribute.Value)
				if err != nil {
					return nil, err
				}
				return value, nil
			}
		}
	}
	return nil, errors.New("acknowledgement event attribute not found")
}

// ParseProposalIDFromEvents parses events emitted from MsgSubmitProposal and returns proposalID
func ParseProposalIDFromEvents(events []abci.Event) (uint64, error) {
	for _, event := range events {
		if attribute, found := attributeByKey(event.Attributes, "proposal_id"); found {
			return strconv.ParseUint(attribute.Value, 10, 64)
		}
	}
	return 0, errors.New("proposalID event attribute not found")
}

// ParsePacketSequenceFromEvents parses events emitted from MsgRecvPacket and returns the packet sequence
func ParsePacketSequenceFromEvents(events []abci.Event) (uint64, error) {
	for _, event := range events {
		if attribute, found := attributeByKey(event.Attributes, "packet_sequence"); found {
			return strconv.ParseUint(attribute.Value, 10, 64)
		}
	}
	return 0, errors.New("packet sequence event attribute not found")
}

// AssertEvents asserts that expected events are present in the actual events.
func AssertEvents(
	suite *testifysuite.Suite,
	expected []abci.Event,
	actual []abci.Event,
) {
	foundEvents := make(map[int]bool)

	for i, expectedEvent := range expected {
		for _, actualEvent := range actual {
			if shouldProcessEvent(expectedEvent, actualEvent) {
				attributeMatch := true
				for _, expectedAttr := range expectedEvent.Attributes {
					// any expected attributes that are not contained in the actual events will cause this event
					// not to match
					attributeMatch = attributeMatch && containsAttribute(actualEvent.Attributes, expectedAttr.Key, expectedAttr.Value)
				}

				if attributeMatch {
					foundEvents[i] = true
				}
			}
		}
	}

	for i, expectedEvent := range expected {
		suite.Require().True(foundEvents[i], "event: %s was not found in events", expectedEvent.Type)
	}
}

// shouldProcessEvent returns true if the given expected event should be processed based on event type.
func shouldProcessEvent(expectedEvent abci.Event, actualEvent abci.Event) bool {
	if expectedEvent.Type != actualEvent.Type {
		return false
	}
	// the actual event will have an extra attribute added automatically
	// by Cosmos SDK since v0.50, that's why we subtract 1 when comparing
	// with the number of attributes in the expected event.
	if containsAttributeKey(actualEvent.Attributes, "msg_index") {
		return len(expectedEvent.Attributes) == len(actualEvent.Attributes)-1
	}

	return len(expectedEvent.Attributes) == len(actualEvent.Attributes)
}

// containsAttribute returns true if the given key/value pair is contained in the given attributes.
// NOTE: this ignores the indexed field, which can be set or unset depending on how the events are retrieved.
func containsAttribute(attrs []abci.EventAttribute, key, value string) bool {
	return slices.ContainsFunc(attrs, func(attr abci.EventAttribute) bool {
		return attr.Key == key && attr.Value == value
	})
}

// containsAttributeKey returns true if the given key is contained in the given attributes.
func containsAttributeKey(attrs []abci.EventAttribute, key string) bool {
	_, found := attributeByKey(attrs, key)
	return found
}

// attributeByKey returns the event attribute's value keyed by the given key and a boolean indicating its presence in the given attributes.
func attributeByKey(attributes []abci.EventAttribute, key string) (abci.EventAttribute, bool) {
	idx := slices.IndexFunc(attributes, func(a abci.EventAttribute) bool { return a.Key == key })
	if idx == -1 {
		return abci.EventAttribute{}, false
	}
	return attributes[idx], true
}

// ParsePacketFromEvents parses events emitted from a send packet and returns
// the first EventTypeSendPacket packet found.
// Returns an error if no packet is found.
//
// Deprecated: This function will be removed in the next major release. Use
// ParseV1PacketFromEvents instead
func ParsePacketFromEvents(events []abci.Event) (channeltypes.Packet, error) {
	return ParseV1PacketFromEvents(events)
}

// ParseRecvPacketFromEvents parses events emitted from a MsgRecvPacket and returns
// the first EventTypeRecvPacket packet found.
// Returns an error if no packet is found.
//
// Deprecated: This function will be removed in the next major release. Use
// ParseRecvV1PacketFromEvents instead
func ParseRecvPacketFromEvents(events []abci.Event) (channeltypes.Packet, error) {
	return ParseRecvV1PacketFromEvents(events)
}

// ParsePacketsFromEvents parses events emitted from a MsgRecvPacket and returns
// all the packets found.
// Returns an error if no packet is found.
//
// Deprecated: This function will be removed in the next major release. Use ParseIBCV1Packets instead.
func ParsePacketsFromEvents(eventType string, events []abci.Event) ([]channeltypes.Packet, error) {
	return ParseIBCV1Packets(eventType, events)
}
