package dispatcher

import (
	"bytes"
	"crypto/sha256"
	"errors"
	"sync"
	"time"
)

const MaxPendingMessages = 64

type pendingMessage struct {
	digest  [32]byte
	total   uint16
	parts   map[uint16][]byte
	created time.Time
}

type Reassembler struct {
	mu      sync.Mutex
	pending map[[16]byte]*pendingMessage
	ttl     time.Duration
	now     func() time.Time
}

func NewReassembler(ttl time.Duration) *Reassembler {
	return &Reassembler{
		pending: make(map[[16]byte]*pendingMessage),
		ttl:     ttl,
		now:     time.Now,
	}
}

func (r *Reassembler) Add(frame Frame) ([]byte, bool, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	r.expireLocked()
	pending, ok := r.pending[frame.ID]
	if !ok {
		if len(r.pending) >= MaxPendingMessages {
			return nil, false, errors.New("too many pending messages")
		}
		pending = &pendingMessage{
			digest:  frame.Digest,
			total:   frame.Total,
			parts:   make(map[uint16][]byte, frame.Total),
			created: r.now(),
		}
		r.pending[frame.ID] = pending
	}
	if pending.total != frame.Total || pending.digest != frame.Digest {
		delete(r.pending, frame.ID)
		return nil, false, errors.New("inconsistent frame metadata")
	}
	if _, duplicate := pending.parts[frame.Index]; !duplicate {
		pending.parts[frame.Index] = bytes.Clone(frame.Data)
	}
	if len(pending.parts) != int(pending.total) {
		return nil, false, nil
	}

	var assembled bytes.Buffer
	for index := uint16(0); index < pending.total; index++ {
		part, exists := pending.parts[index]
		if !exists {
			return nil, false, nil
		}
		if assembled.Len()+len(part) > MaxEnvelopeBytes {
			delete(r.pending, frame.ID)
			return nil, false, errors.New("reassembled envelope is too large")
		}
		assembled.Write(part)
	}
	delete(r.pending, frame.ID)
	result := assembled.Bytes()
	if sha256.Sum256(result) != pending.digest {
		return nil, false, errors.New("reassembled envelope digest mismatch")
	}
	return result, true, nil
}

func (r *Reassembler) expireLocked() {
	cutoff := r.now().Add(-r.ttl)
	for id, pending := range r.pending {
		if pending.created.Before(cutoff) {
			delete(r.pending, id)
		}
	}
}
