package quic

import (
	"golang.org/x/exp/slices"
	"net"
	"net/netip"

	"github.com/metacubex/quic-go/internal/protocol"
	"github.com/metacubex/quic-go/internal/wire"
	"github.com/metacubex/quic-go/qlog"
)

// ConvertFrame converts a wire.Frame into a logging.Frame.
// This makes it possible for external packages to access the frames.
// Furthermore, it removes the data slices from CRYPTO and STREAM frames.
func toQlogFrame(frame wire.Frame) qlog.Frame {
	switch f := frame.(type) {
	case *wire.AckFrame:
		// We use a pool for ACK frames.
		// Implementations of the tracer interface may hold on to frames, so we need to make a copy here.
		return qlog.Frame{Frame: toQlogAckFrame(f)}
	case *wire.CryptoFrame:
		return qlog.Frame{
			Frame: &qlog.CryptoFrame{
				Offset: int64(f.Offset),
				Length: int64(len(f.Data)),
			},
		}
	case *wire.StreamFrame:
		return qlog.Frame{
			Frame: &qlog.StreamFrame{
				StreamID: f.StreamID,
				Offset:   int64(f.Offset),
				Length:   int64(f.DataLen()),
				Fin:      f.Fin,
			},
		}
	case *wire.DatagramFrame:
		return qlog.Frame{
			Frame: &qlog.DatagramFrame{
				Length: int64(len(f.Data)),
			},
		}
	default:
		return qlog.Frame{Frame: frame}
	}
}

func toQlogAckFrame(f *wire.AckFrame) *qlog.AckFrame {
	ack := &qlog.AckFrame{
		AckRanges: slices.Clone(f.AckRanges),
		DelayTime: f.DelayTime,
		ECNCE:     f.ECNCE,
		ECT0:      f.ECT0,
		ECT1:      f.ECT1,
	}
	return ack
}

func (c *Conn) logLongHeaderPacket(p *longHeaderPacket, ecn protocol.ECN, datagramID qlog.DatagramID) {
	// quic-go logging
	if c.logger.Debug() {
		p.header.Log(c.logger)
		if p.ack != nil {
			wire.LogFrame(c.logger, p.ack, true)
		}
		for _, frame := range p.frames {
			wire.LogFrame(c.logger, frame.Frame, true)
		}
		for _, frame := range p.streamFrames {
			wire.LogFrame(c.logger, frame.Frame, true)
		}
	}

	// tracing
	if c.qlogger != nil {
		numFrames := len(p.frames) + len(p.streamFrames)
		if p.ack != nil {
			numFrames++
		}
		frames := make([]qlog.Frame, 0, numFrames)
		if p.ack != nil {
			frames = append(frames, toQlogFrame(p.ack))
		}
		for _, f := range p.frames {
			frames = append(frames, toQlogFrame(f.Frame))
		}
		for _, f := range p.streamFrames {
			frames = append(frames, toQlogFrame(f.Frame))
		}
		c.qlogger.RecordEvent(qlog.PacketSent{
			Header: qlog.PacketHeader{
				PacketType:       toQlogPacketType(p.header.Type),
				KeyPhaseBit:      p.header.KeyPhase,
				PacketNumber:     p.header.PacketNumber,
				Version:          p.header.Version,
				SrcConnectionID:  p.header.SrcConnectionID,
				DestConnectionID: p.header.DestConnectionID,
			},
			Raw: qlog.RawInfo{
				Length:        int(p.length),
				PayloadLength: int(p.header.Length),
			},
			DatagramID: datagramID,
			Frames:     frames,
			ECN:        toQlogECN(ecn),
		})
	}
}

func (c *Conn) logShortHeaderPacket(p shortHeaderPacket, ecn protocol.ECN, size protocol.ByteCount) {
	c.logShortHeaderPacketWithDatagramID(p, ecn, size, false, 0)
}

func (c *Conn) logShortHeaderPacketWithDatagramID(p shortHeaderPacket, ecn protocol.ECN, size protocol.ByteCount, isCoalesced bool, datagramID qlog.DatagramID) {
	if c.logger.Debug() && !isCoalesced {
		c.logger.Debugf("-> Sending packet %d (%d bytes) for connection %s, 1-RTT (ECN: %s)", p.PacketNumber, size, c.logID, ecn)
	}
	// quic-go logging
	if c.logger.Debug() {
		wire.LogShortHeader(c.logger, p.DestConnID, p.PacketNumber, p.PacketNumberLen, p.KeyPhase)
		if p.Ack != nil {
			wire.LogFrame(c.logger, p.Ack, true)
		}
		for _, f := range p.Frames {
			wire.LogFrame(c.logger, f.Frame, true)
		}
		for _, f := range p.StreamFrames {
			wire.LogFrame(c.logger, f.Frame, true)
		}
	}

	// tracing
	if c.qlogger != nil {
		numFrames := len(p.Frames) + len(p.StreamFrames)
		if p.Ack != nil {
			numFrames++
		}
		fs := make([]qlog.Frame, 0, numFrames)
		if p.Ack != nil {
			fs = append(fs, toQlogFrame(p.Ack))
		}
		for _, f := range p.Frames {
			fs = append(fs, toQlogFrame(f.Frame))
		}
		for _, f := range p.StreamFrames {
			fs = append(fs, toQlogFrame(f.Frame))
		}
		c.qlogger.RecordEvent(qlog.PacketSent{
			Header: qlog.PacketHeader{
				PacketType:       qlog.PacketType1RTT,
				KeyPhaseBit:      p.KeyPhase,
				PacketNumber:     p.PacketNumber,
				Version:          c.version,
				DestConnectionID: p.DestConnID,
			},
			Raw: qlog.RawInfo{
				Length:        int(size),
				PayloadLength: int(size - wire.ShortHeaderLen(p.DestConnID, p.PacketNumberLen)),
			},
			DatagramID: datagramID,
			Frames:     fs,
			ECN:        toQlogECN(ecn),
		})
	}
}

func (c *Conn) logCoalescedPacket(packet *coalescedPacket, ecn protocol.ECN) {
	var datagramID qlog.DatagramID
	if c.qlogger != nil {
		datagramID = qlog.CalculateDatagramID(packet.buffer.Data)
	}
	if c.logger.Debug() {
		// There's a short period between dropping both Initial and Handshake keys and completion of the handshake,
		// during which we might call PackCoalescedPacket but just pack a short header packet.
		if len(packet.longHdrPackets) == 0 && packet.shortHdrPacket != nil {
			c.logShortHeaderPacketWithDatagramID(
				*packet.shortHdrPacket,
				ecn,
				packet.shortHdrPacket.Length,
				false,
				datagramID,
			)
			return
		}
		if len(packet.longHdrPackets) > 1 {
			c.logger.Debugf("-> Sending coalesced packet (%d parts, %d bytes) for connection %s", len(packet.longHdrPackets), packet.buffer.Len(), c.logID)
		} else {
			c.logger.Debugf("-> Sending packet %d (%d bytes) for connection %s, %s", packet.longHdrPackets[0].header.PacketNumber, packet.buffer.Len(), c.logID, packet.longHdrPackets[0].EncryptionLevel())
		}
	}
	for _, p := range packet.longHdrPackets {
		c.logLongHeaderPacket(p, ecn, datagramID)
	}
	if p := packet.shortHdrPacket; p != nil {
		c.logShortHeaderPacketWithDatagramID(*p, ecn, p.Length, true, datagramID)
	}
}

func (c *Conn) qlogTransportParameters(tp *wire.TransportParameters, sentBy protocol.Perspective, restore bool) {
	ev := qlog.ParametersSet{
		Restore:                         restore,
		OriginalDestinationConnectionID: tp.OriginalDestinationConnectionID,
		InitialSourceConnectionID:       tp.InitialSourceConnectionID,
		RetrySourceConnectionID:         tp.RetrySourceConnectionID,
		StatelessResetToken:             tp.StatelessResetToken,
		DisableActiveMigration:          tp.DisableActiveMigration,
		MaxIdleTimeout:                  tp.MaxIdleTimeout,
		MaxUDPPayloadSize:               tp.MaxUDPPayloadSize,
		AckDelayExponent:                tp.AckDelayExponent,
		MaxAckDelay:                     tp.MaxAckDelay,
		ActiveConnectionIDLimit:         tp.ActiveConnectionIDLimit,
		InitialMaxData:                  tp.InitialMaxData,
		InitialMaxStreamDataBidiLocal:   tp.InitialMaxStreamDataBidiLocal,
		InitialMaxStreamDataBidiRemote:  tp.InitialMaxStreamDataBidiRemote,
		InitialMaxStreamDataUni:         tp.InitialMaxStreamDataUni,
		InitialMaxStreamsBidi:           int64(tp.MaxBidiStreamNum),
		InitialMaxStreamsUni:            int64(tp.MaxUniStreamNum),
		MaxDatagramFrameSize:            tp.MaxDatagramFrameSize,
		EnableResetStreamAt:             tp.EnableResetStreamAt,
	}
	if sentBy == c.perspective {
		ev.Initiator = qlog.InitiatorLocal
	} else {
		ev.Initiator = qlog.InitiatorRemote
	}
	if tp.PreferredAddress != nil {
		ev.PreferredAddress = &qlog.PreferredAddress{
			IPv4:                tp.PreferredAddress.IPv4,
			IPv6:                tp.PreferredAddress.IPv6,
			ConnectionID:        tp.PreferredAddress.ConnectionID,
			StatelessResetToken: tp.PreferredAddress.StatelessResetToken,
		}
	}
	c.qlogger.RecordEvent(ev)
}

func toQlogECN(ecn protocol.ECN) qlog.ECN {
	//nolint:exhaustive // only need to handle the 3 valid values
	switch ecn {
	case protocol.ECT0:
		return qlog.ECT0
	case protocol.ECT1:
		return qlog.ECT1
	case protocol.ECNCE:
		return qlog.ECNCE
	default:
		return qlog.ECNUnsupported
	}
}

func toQlogPacketType(pt protocol.PacketType) qlog.PacketType {
	var qpt qlog.PacketType
	switch pt {
	case protocol.PacketTypeInitial:
		qpt = qlog.PacketTypeInitial
	case protocol.PacketTypeHandshake:
		qpt = qlog.PacketTypeHandshake
	case protocol.PacketType0RTT:
		qpt = qlog.PacketType0RTT
	case protocol.PacketTypeRetry:
		qpt = qlog.PacketTypeRetry
	}
	return qpt
}

func toPathEndpointInfo(addr *net.UDPAddr) qlog.PathEndpointInfo {
	if addr == nil {
		return qlog.PathEndpointInfo{}
	}

	var info qlog.PathEndpointInfo
	if addr.IP == nil || addr.IP.To4() != nil {
		addrPort := netip.AddrPortFrom(netip.AddrFrom4([4]byte(addr.IP.To4())), uint16(addr.Port))
		if addrPort.IsValid() {
			info.IPv4 = addrPort
		}
	} else {
		addrPort := netip.AddrPortFrom(netip.AddrFrom16([16]byte(addr.IP.To16())), uint16(addr.Port))
		if addrPort.IsValid() {
			info.IPv6 = addrPort
		}
	}
	return info
}

// startedConnectionEvent builds a StartedConnection event using consistent logic
// for both endpoints. If the local address is unspecified (e.g., dual-stack
// listener), it selects the family based on the remote address and uses the
// unspecified address of that family with the local port.
func startedConnectionEvent(local, remote *net.UDPAddr) qlog.StartedConnection {
	var localInfo, remoteInfo qlog.PathEndpointInfo
	if remote != nil {
		remoteInfo = toPathEndpointInfo(remote)
	}
	if local != nil {
		if local.IP == nil || local.IP.IsUnspecified() {
			// Choose local family based on the remote address family.
			if remote != nil && remote.IP.To4() != nil {
				ap := netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), uint16(local.Port))
				if ap.IsValid() {
					localInfo.IPv4 = ap
				}
			} else if remote != nil && remote.IP.To16() != nil && remote.IP.To4() == nil {
				ap := netip.AddrPortFrom(netip.AddrFrom16([16]byte{}), uint16(local.Port))
				if ap.IsValid() {
					localInfo.IPv6 = ap
				}
			}
		} else {
			localInfo = toPathEndpointInfo(local)
		}
	}
	return qlog.StartedConnection{Local: localInfo, Remote: remoteInfo}
}
