// Package common provides building blocks shared by the video-track based
// transports (videochannel, seichannel) — fragment/reassembly, ack waiters,
// and per-peer random IDs. vp8channel does its own KCP-based framing and
// only consumes RandomID.
package common

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"hash/crc32"
	"sync"
	"time"
)

// RandomID returns 8 random hex characters for use as a per-peer suffix on
// track and stream IDs. Required for Jitsi: msid collisions between
// participants cause Jicofo to reject session-accept.
func RandomID() string {
	var b [4]byte
	if _, err := rand.Read(b[:]); err != nil {
		return fmt.Sprintf("%08x", time.Now().UnixNano())
	}
	return hex.EncodeToString(b[:])
}

// FragmentPayload splits data into chunks of at most maxSize bytes. An empty
// payload produces a single empty fragment so the caller can still ack a
// zero-byte message round-trip.
func FragmentPayload(data []byte, maxSize int) [][]byte {
	if len(data) == 0 {
		return [][]byte{{}}
	}
	out := make([][]byte, 0, (len(data)+maxSize-1)/maxSize)
	for start := 0; start < len(data); start += maxSize {
		end := start + maxSize
		if end > len(data) {
			end = len(data)
		}
		chunk := make([]byte, end-start)
		copy(chunk, data[start:end])
		out = append(out, chunk)
	}
	return out
}

// Fragment describes one piece of a fragmented message on the wire.
type Fragment struct {
	Seq       uint32
	CRC       uint32
	TotalLen  uint32
	FragIdx   uint16
	FragTotal uint16
	Payload   []byte
}

// InboundMessage tracks reassembly state for one inbound message.
type InboundMessage struct {
	TotalLen uint32
	CRC      uint32
	frags    [][]byte
	remain   int
}

// Reassembler holds inbound message state and a sliding window of recently
// delivered (seq, crc) pairs so duplicate fragments resolve to a fresh ack
// rather than a re-delivery.
type Reassembler struct {
	mu        sync.Mutex
	inbound   map[uint32]*InboundMessage
	delivered map[uint32]uint32
	maxRecent int
}

// NewReassembler creates a reassembler with the given recent-delivery cap.
// When the delivered map exceeds maxRecent entries it is reset; a value of
// 256 is a reasonable default for the video transports.
func NewReassembler(maxRecent int) *Reassembler {
	if maxRecent <= 0 {
		maxRecent = 256
	}
	return &Reassembler{
		inbound:   make(map[uint32]*InboundMessage),
		delivered: make(map[uint32]uint32),
		maxRecent: maxRecent,
	}
}

// Result classifies what Push computed for a fragment.
type Result int

const (
	// ResultIgnore means the fragment was malformed or out of range.
	ResultIgnore Result = iota
	// ResultPartial means the fragment was stored but the message is not
	// fully reassembled yet.
	ResultPartial
	// ResultDuplicate means the message identified by (Seq, CRC) was
	// already delivered. Caller should re-ack without invoking OnData.
	ResultDuplicate
	// ResultDelivered means the message is complete; Data carries the
	// reassembled payload.
	ResultDelivered
)

// Push integrates fragment into reassembly state and returns one of the
// Result values. When ResultDelivered, the second return holds the
// reassembled payload bytes; otherwise it is nil.
func (r *Reassembler) Push(fragment Fragment) (Result, []byte) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if crc, ok := r.delivered[fragment.Seq]; ok && crc == fragment.CRC {
		return ResultDuplicate, nil
	}

	msg := r.upsert(fragment)
	if int(fragment.FragIdx) >= len(msg.frags) {
		return ResultIgnore, nil
	}
	r.storeChunk(msg, fragment)
	if msg.remain > 0 {
		return ResultPartial, nil
	}
	return r.deliver(fragment.Seq, msg)
}

// upsert returns the inbound message tracking entry for fragment.Seq,
// creating a fresh entry if no compatible one is present.
func (r *Reassembler) upsert(fragment Fragment) *InboundMessage {
	msg, ok := r.inbound[fragment.Seq]
	if ok && msg.CRC == fragment.CRC && msg.TotalLen == fragment.TotalLen &&
		len(msg.frags) == int(fragment.FragTotal) {
		return msg
	}
	msg = &InboundMessage{
		TotalLen: fragment.TotalLen,
		CRC:      fragment.CRC,
		frags:    make([][]byte, fragment.FragTotal),
		remain:   int(fragment.FragTotal),
	}
	r.inbound[fragment.Seq] = msg
	return msg
}

func (r *Reassembler) storeChunk(msg *InboundMessage, fragment Fragment) {
	if msg.frags[fragment.FragIdx] != nil {
		return
	}
	chunk := make([]byte, len(fragment.Payload))
	copy(chunk, fragment.Payload)
	msg.frags[fragment.FragIdx] = chunk
	msg.remain--
}

func (r *Reassembler) deliver(seq uint32, msg *InboundMessage) (Result, []byte) {
	delete(r.inbound, seq)
	data := assemble(msg)
	if crc32.ChecksumIEEE(data) != msg.CRC {
		return ResultIgnore, nil
	}
	if len(r.delivered) > r.maxRecent {
		r.delivered = make(map[uint32]uint32)
	}
	r.delivered[seq] = msg.CRC
	return ResultDelivered, data
}

func assemble(msg *InboundMessage) []byte {
	out := make([]byte, 0, msg.TotalLen)
	for _, frag := range msg.frags {
		out = append(out, frag...)
	}
	if uint32(len(out)) > msg.TotalLen { //nolint:gosec // G115: bounded by allocation size
		out = out[:msg.TotalLen]
	}
	return out
}

// AckRegistry tracks in-flight Send calls waiting for their peer ack. Each
// Send registers a waiter keyed by sequence number and reads from it; the
// receive loop calls Resolve when an ack arrives.
type AckRegistry struct {
	mu      sync.Mutex
	waiters map[uint32]chan uint32
}

// NewAckRegistry creates an empty ack registry.
func NewAckRegistry() *AckRegistry {
	return &AckRegistry{waiters: make(map[uint32]chan uint32)}
}

// Register installs a waiter for seq and returns its channel. The caller
// must drop the waiter via Unregister when it is done.
func (a *AckRegistry) Register(seq uint32) chan uint32 {
	ch := make(chan uint32, 1)
	a.mu.Lock()
	a.waiters[seq] = ch
	a.mu.Unlock()
	return ch
}

// Unregister drops the waiter for seq.
func (a *AckRegistry) Unregister(seq uint32) {
	a.mu.Lock()
	delete(a.waiters, seq)
	a.mu.Unlock()
}

// Resolve delivers crc to the waiter for seq, if present. A missing waiter
// is silently ignored — the sender has already moved on.
func (a *AckRegistry) Resolve(seq, crc uint32) {
	a.mu.Lock()
	waiter := a.waiters[seq]
	a.mu.Unlock()
	if waiter == nil {
		return
	}
	select {
	case waiter <- crc:
	default:
	}
}
