package udp

import (
	"bytes"
	"errors"
	"sort"

	"git.difuse.io/Difuse/Mellaris/analyzer"
	"git.difuse.io/Difuse/Mellaris/analyzer/internal"
	"git.difuse.io/Difuse/Mellaris/analyzer/udp/internal/quic"
	"git.difuse.io/Difuse/Mellaris/analyzer/utils"
)

const (
	quicInvalidCountThreshold = 16
	quicMaxCryptoDataLen      = 256 * 1024
)

var (
	_ analyzer.UDPAnalyzer = (*QUICAnalyzer)(nil)
	_ analyzer.UDPStream   = (*quicStream)(nil)
)

type QUICAnalyzer struct{}

func (a *QUICAnalyzer) Name() string {
	return "quic"
}

func (a *QUICAnalyzer) Limit() int {
	return 0
}

func (a *QUICAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
	return &quicStream{
		logger: logger,
		frames: make(map[int64][]byte),
	}
}

type quicStream struct {
	logger       analyzer.Logger
	invalidCount int
	debugCount   int
	frames       map[int64][]byte
	maxEnd       int64
	connIDs      [][]byte
	lastHint     quic.DecryptSuccessHint
	hasLastHint  bool
}

func (s *quicStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
	// minimal data size: protocol version (2 bytes) + random (32 bytes) +
	//   + session ID (1 byte) + cipher suites (4 bytes) +
	//   + compression methods (2 bytes) + no extensions
	const minDataSize = 41

	var parsedHdr *quic.Header
	if hdr, _, err := quic.ParseInitialHeader(data); err == nil {
		parsedHdr = hdr
		s.rememberConnID(hdr.DestConnectionID)
		s.rememberConnID(hdr.SrcConnectionID)
	}
	hint := quic.DecryptSuccessHint{}
	opts := &quic.ReadCryptoFramesOptions{
		AdditionalConnectionIDs: s.connIDs,
		TryServerSecret:         true,
		SuccessHint:             &hint,
	}
	if s.hasLastHint {
		opts.PreferredConnectionID = append([]byte(nil), s.lastHint.ConnectionID...)
		opts.PreferredSecretLabel = s.lastHint.SecretLabel
		opts.PreferredPNMax = s.lastHint.PacketNumberMax
		opts.HasPreferredPNMax = true
	}
	frs, err := quic.ReadCryptoFramesWithOptions(data, opts)
	if err != nil {
		if errors.Is(err, quic.ErrNotInitialPacket) {
			return nil, false
		}
		if s.debugCount < 4 {
			if parsedHdr != nil {
				s.logger.Debugf(
					"failed to read QUIC CRYPTO frames: version=%x dcid_len=%d scid_len=%d token_len=%d conn_id_hints=%d err=%v",
					parsedHdr.Version,
					len(parsedHdr.DestConnectionID),
					len(parsedHdr.SrcConnectionID),
					len(parsedHdr.Token),
					len(s.connIDs),
					err,
				)
			} else {
				s.logger.Debugf("failed to read QUIC CRYPTO frames: %v", err)
			}
			s.debugCount++
		}
		s.invalidCount++
		return nil, s.invalidCount >= quicInvalidCountThreshold
	}
	if len(hint.ConnectionID) > 0 {
		s.lastHint = hint
		s.hasLastHint = true
		s.rememberConnID(hint.ConnectionID)
	}
	if len(frs) == 0 {
		s.invalidCount++
		return nil, s.invalidCount >= quicInvalidCountThreshold
	}
	s.invalidCount = 0
	for _, f := range frs {
		s.mergeFrame(f.Offset, f.Data)
	}
	pl := s.contiguousPayloadFromZero()
	if len(pl) < 4 {
		return nil, false
	}

	if pl[0] != internal.TypeClientHello {
		// Not a ClientHello (e.g. server-direction CRYPTO); ignore.
		if s.debugCount < 4 {
			s.logger.Debugf("CRYPTO payload does not start with ClientHello: type=%d", pl[0])
			s.debugCount++
		}
		return nil, false
	}

	chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
	if chLen < minDataSize {
		s.invalidCount++
		return nil, s.invalidCount >= quicInvalidCountThreshold
	}
	if len(pl) < 4+chLen {
		// Wait for more CRYPTO data from subsequent packets.
		return nil, false
	}

	m := internal.ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: pl[4 : 4+chLen]})
	if m == nil {
		if s.debugCount < 4 {
			s.logger.Debugf("failed to parse TLS ClientHello from QUIC CRYPTO payload")
			s.debugCount++
		}
		s.invalidCount++
		return nil, s.invalidCount >= quicInvalidCountThreshold
	}

	return &analyzer.PropUpdate{
		Type: analyzer.PropUpdateMerge,
		M:    analyzer.PropMap{"req": m},
	}, true
}

func (s *quicStream) Close(limited bool) *analyzer.PropUpdate {
	return nil
}

func (s *quicStream) mergeFrame(offset int64, data []byte) {
	if len(data) == 0 || offset < 0 {
		return
	}
	if s.frames == nil {
		s.frames = make(map[int64][]byte)
	}
	if _, exists := s.frames[offset]; exists {
		return
	}
	s.frames[offset] = append([]byte(nil), data...)
	end := offset + int64(len(data))
	if end > s.maxEnd {
		s.maxEnd = end
	}
}

func (s *quicStream) contiguousPayloadFromZero() []byte {
	if len(s.frames) == 0 || s.maxEnd <= 0 || s.maxEnd > quicMaxCryptoDataLen {
		return nil
	}
	keys := make([]int64, 0, len(s.frames))
	for k := range s.frames {
		keys = append(keys, k)
	}
	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
	if keys[0] != 0 {
		return nil
	}
	out := make([]byte, 0, s.maxEnd)
	next := int64(0)
	for _, k := range keys {
		if k > next {
			break
		}
		frame := s.frames[k]
		frameEnd := k + int64(len(frame))
		if frameEnd <= next {
			continue
		}
		start := next - k
		out = append(out, frame[start:]...)
		next = frameEnd
		if next >= quicMaxCryptoDataLen {
			break
		}
	}
	return out
}

func (s *quicStream) rememberConnID(cid []byte) {
	if len(cid) == 0 {
		return
	}
	for _, existing := range s.connIDs {
		if bytes.Equal(existing, cid) {
			return
		}
	}
	// Keep this bounded; QUIC CID rotations are small for handshake parsing.
	if len(s.connIDs) >= 8 {
		s.connIDs = s.connIDs[1:]
	}
	s.connIDs = append(s.connIDs, append([]byte(nil), cid...))
}
