package tun

import (
	"encoding/binary"
	"net/netip"

	"github.com/sagernet/sing-tun/gtcpip/header"
)

type flowKey struct {
	protocol    uint8
	source      netip.AddrPort
	destination netip.AddrPort
}

func (k flowKey) reversed() flowKey {
	return flowKey{protocol: k.protocol, source: k.destination, destination: k.source}
}

type forwardPacket struct {
	ipVersion   uint8
	protocol    uint8
	network     header.Network
	transport   []byte
	source      netip.AddrPort
	destination netip.AddrPort
	tcpFlags    header.TCPFlags
	icmpType    uint8
	fragment    bool
	hasFlow     bool
}

func (p *forwardPacket) flowKey() flowKey {
	return flowKey{protocol: p.protocol, source: p.source, destination: p.destination}
}

func (p *forwardPacket) isTCPSyn() bool {
	return p.protocol == uint8(header.TCPProtocolNumber) && p.tcpFlags&header.TCPFlagSyn != 0
}

func parseForwardPacket(packet []byte) (forwardPacket, bool) {
	switch header.IPVersion(packet) {
	case header.IPv4Version:
		ipHdr := header.IPv4(packet)
		if !ipHdr.IsValid(len(packet)) {
			return forwardPacket{}, false
		}
		parsed := forwardPacket{
			ipVersion:   4,
			protocol:    uint8(ipHdr.TransportProtocol()),
			network:     ipHdr,
			source:      netip.AddrPortFrom(ipHdr.SourceAddr(), 0),
			destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0),
		}
		if ipHdr.More() || ipHdr.FragmentOffset() != 0 {
			parsed.fragment = true
			return parsed, true
		}
		parsed.parseTransport(ipHdr.Payload())
		return parsed, true
	case header.IPv6Version:
		ipHdr := header.IPv6(packet)
		if !ipHdr.IsValid(len(packet)) {
			return forwardPacket{}, false
		}
		protocol, payload, fragment, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), ipHdr.Payload())
		parsed := forwardPacket{
			ipVersion:   6,
			protocol:    protocol,
			network:     ipHdr,
			source:      netip.AddrPortFrom(ipHdr.SourceAddr(), 0),
			destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0),
			fragment:    fragment,
		}
		if fragment || !transportPresent {
			return parsed, true
		}
		parsed.parseTransport(payload)
		return parsed, true
	default:
		return forwardPacket{}, false
	}
}

func skipIPv6ExtensionHeaders(protocol uint8, payload []byte) (uint8, []byte, bool, bool) {
	for {
		switch header.IPv6ExtensionHeaderIdentifier(protocol) {
		case header.IPv6HopByHopOptionsExtHdrIdentifier, header.IPv6RoutingExtHdrIdentifier, header.IPv6DestinationOptionsExtHdrIdentifier:
			if len(payload) < 2 {
				return protocol, payload, false, false
			}
			extensionLength := (int(payload[1]) + 1) * 8
			if len(payload) < extensionLength {
				return protocol, payload, false, false
			}
			protocol = payload[0]
			payload = payload[extensionLength:]
		case header.IPv6FragmentExtHdrIdentifier:
			return protocol, payload, true, false
		default:
			return protocol, payload, false, true
		}
	}
}

func (p *forwardPacket) parseTransport(payload []byte) {
	p.transport = payload
	switch p.protocol {
	case uint8(header.TCPProtocolNumber):
		if len(payload) < header.TCPMinimumSize {
			return
		}
		tcpHdr := header.TCP(payload)
		p.source = netip.AddrPortFrom(p.source.Addr(), tcpHdr.SourcePort())
		p.destination = netip.AddrPortFrom(p.destination.Addr(), tcpHdr.DestinationPort())
		p.tcpFlags = tcpHdr.Flags()
		p.hasFlow = true
	case uint8(header.UDPProtocolNumber):
		if len(payload) < header.UDPMinimumSize {
			return
		}
		udpHdr := header.UDP(payload)
		p.source = netip.AddrPortFrom(p.source.Addr(), udpHdr.SourcePort())
		p.destination = netip.AddrPortFrom(p.destination.Addr(), udpHdr.DestinationPort())
		p.hasFlow = true
	case uint8(header.ICMPv4ProtocolNumber):
		if len(payload) < header.ICMPv4MinimumSize {
			return
		}
		icmpHdr := header.ICMPv4(payload)
		p.icmpType = uint8(icmpHdr.Type())
		switch icmpHdr.Type() {
		case header.ICMPv4Echo, header.ICMPv4EchoReply:
			identifier := icmpHdr.Ident()
			p.source = netip.AddrPortFrom(p.source.Addr(), identifier)
			p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier)
			p.hasFlow = true
		}
	case uint8(header.ICMPv6ProtocolNumber):
		if len(payload) < header.ICMPv6MinimumSize {
			return
		}
		icmpHdr := header.ICMPv6(payload)
		p.icmpType = uint8(icmpHdr.Type())
		switch icmpHdr.Type() {
		case header.ICMPv6EchoRequest, header.ICMPv6EchoReply:
			identifier := icmpHdr.Ident()
			p.source = netip.AddrPortFrom(p.source.Addr(), identifier)
			p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier)
			p.hasFlow = true
		}
	}
}

func (p *forwardPacket) isICMPError() bool {
	switch p.protocol {
	case uint8(header.ICMPv4ProtocolNumber):
		switch header.ICMPv4Type(p.icmpType) {
		case header.ICMPv4DstUnreachable, header.ICMPv4SrcQuench, header.ICMPv4Redirect, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem:
			return true
		}
		return false
	case uint8(header.ICMPv6ProtocolNumber):
		return header.ICMPv6Type(p.icmpType).IsErrorType()
	default:
		return false
	}
}

func (p *forwardPacket) icmpErrorInner() ([]byte, bool) {
	var innerOffset int
	switch p.protocol {
	case uint8(header.ICMPv4ProtocolNumber):
		innerOffset = header.ICMPv4MinimumSize
	case uint8(header.ICMPv6ProtocolNumber):
		innerOffset = header.ICMPv6ErrorHeaderSize
	default:
		return nil, false
	}
	if len(p.transport) <= innerOffset {
		return nil, false
	}
	return p.transport[innerOffset:], true
}

type embeddedPacket struct {
	network     header.Network
	payload     []byte
	protocol    uint8
	source      netip.AddrPort
	destination netip.AddrPort
}

func (p *embeddedPacket) flowKey() flowKey {
	return flowKey{protocol: p.protocol, source: p.source, destination: p.destination}
}

func parseEmbedded(inner []byte) (embeddedPacket, bool) {
	switch header.IPVersion(inner) {
	case header.IPv4Version:
		if len(inner) < header.IPv4MinimumSize {
			return embeddedPacket{}, false
		}
		ipHdr := header.IPv4(inner)
		headerLength := int(ipHdr.HeaderLength())
		if headerLength < header.IPv4MinimumSize || headerLength > len(inner) {
			return embeddedPacket{}, false
		}
		return parseEmbeddedTransport(ipHdr, inner[headerLength:], uint8(ipHdr.TransportProtocol()), ipHdr.SourceAddr(), ipHdr.DestinationAddr())
	case header.IPv6Version:
		if len(inner) < header.IPv6MinimumSize {
			return embeddedPacket{}, false
		}
		ipHdr := header.IPv6(inner)
		protocol, payload, _, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), inner[header.IPv6MinimumSize:])
		if !transportPresent {
			return embeddedPacket{}, false
		}
		return parseEmbeddedTransport(ipHdr, payload, protocol, ipHdr.SourceAddr(), ipHdr.DestinationAddr())
	default:
		return embeddedPacket{}, false
	}
}

func parseEmbeddedTransport(network header.Network, payload []byte, protocol uint8, source, destination netip.Addr) (embeddedPacket, bool) {
	embedded := embeddedPacket{
		network:  network,
		payload:  payload,
		protocol: protocol,
	}
	switch protocol {
	case uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber):
		if len(payload) < 4 {
			return embeddedPacket{}, false
		}
		embedded.source = netip.AddrPortFrom(source, binary.BigEndian.Uint16(payload[0:]))
		embedded.destination = netip.AddrPortFrom(destination, binary.BigEndian.Uint16(payload[2:]))
	case uint8(header.ICMPv4ProtocolNumber):
		if len(payload) < header.ICMPv4MinimumSize {
			return embeddedPacket{}, false
		}
		identifier := header.ICMPv4(payload).Ident()
		embedded.source = netip.AddrPortFrom(source, identifier)
		embedded.destination = netip.AddrPortFrom(destination, identifier)
	case uint8(header.ICMPv6ProtocolNumber):
		if len(payload) < header.ICMPv6MinimumSize {
			return embeddedPacket{}, false
		}
		identifier := header.ICMPv6(payload).Ident()
		embedded.source = netip.AddrPortFrom(source, identifier)
		embedded.destination = netip.AddrPortFrom(destination, identifier)
	default:
		return embeddedPacket{}, false
	}
	return embedded, true
}
