package telemetry

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/getsentry/sentry-go/internal/protocol"
	"github.com/getsentry/sentry-go/internal/ratelimit"
	"github.com/getsentry/sentry-go/report"
)

const defaultCapacity = 100

// RingBuffer is a thread-safe ring buffer with overflow policies.
type RingBuffer[T any] struct {
	mu       sync.RWMutex
	items    []T
	head     int
	tail     int
	size     int
	capacity int

	category       ratelimit.Category
	priority       ratelimit.Priority
	overflowPolicy OverflowPolicy
	recorder       report.ClientReportRecorder

	batchSize     int
	timeout       time.Duration
	lastFlushTime time.Time

	offered   int64
	dropped   int64
	onDropped func(item T, reason string)
}

func NewRingBuffer[T any](category ratelimit.Category, capacity int, overflowPolicy OverflowPolicy, batchSize int, timeout time.Duration, recorder report.ClientReportRecorder) *RingBuffer[T] {
	if capacity <= 0 {
		capacity = defaultCapacity
	}

	if batchSize <= 0 {
		batchSize = 1
	}

	if timeout < 0 {
		timeout = 0
	}

	if recorder == nil {
		recorder = report.NoopRecorder()
	}

	return &RingBuffer[T]{
		items:          make([]T, capacity),
		capacity:       capacity,
		category:       category,
		priority:       category.GetPriority(),
		overflowPolicy: overflowPolicy,
		recorder:       recorder,
		batchSize:      batchSize,
		timeout:        timeout,
		lastFlushTime:  time.Now(),
	}
}

func (b *RingBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.onDropped = callback
}

func (b *RingBuffer[T]) Offer(item T) bool {
	atomic.AddInt64(&b.offered, 1)

	b.mu.Lock()
	defer b.mu.Unlock()

	if b.size < b.capacity {
		b.items[b.tail] = item
		b.tail = (b.tail + 1) % b.capacity
		b.size++
		return true
	}

	switch b.overflowPolicy {
	case OverflowPolicyDropOldest:
		oldItem := b.items[b.head]
		b.items[b.head] = item
		b.head = (b.head + 1) % b.capacity
		b.tail = (b.tail + 1) % b.capacity

		atomic.AddInt64(&b.dropped, 1)
		b.recordDroppedItem(oldItem)
		if b.onDropped != nil {
			b.onDropped(oldItem, "buffer_full_drop_oldest")
		}
		return true

	case OverflowPolicyDropNewest:
		atomic.AddInt64(&b.dropped, 1)
		b.recordDroppedItem(item)
		if b.onDropped != nil {
			b.onDropped(item, "buffer_full_drop_newest")
		}
		return false

	default:
		atomic.AddInt64(&b.dropped, 1)
		b.recordDroppedItem(item)
		if b.onDropped != nil {
			b.onDropped(item, "unknown_overflow_policy")
		}
		return false
	}
}

func (b *RingBuffer[T]) Poll() (T, bool) {
	b.mu.Lock()
	defer b.mu.Unlock()

	var zero T
	if b.size == 0 {
		return zero, false
	}

	item := b.items[b.head]
	b.items[b.head] = zero
	b.head = (b.head + 1) % b.capacity
	b.size--

	return item, true
}

func (b *RingBuffer[T]) PollBatch(maxItems int) []T {
	if maxItems <= 0 {
		return nil
	}

	b.mu.Lock()
	defer b.mu.Unlock()

	if b.size == 0 {
		return nil
	}

	itemCount := maxItems
	if itemCount > b.size {
		itemCount = b.size
	}

	result := make([]T, itemCount)
	var zero T

	for i := 0; i < itemCount; i++ {
		result[i] = b.items[b.head]
		b.items[b.head] = zero
		b.head = (b.head + 1) % b.capacity
		b.size--
	}

	return result
}

func (b *RingBuffer[T]) Drain() []T {
	b.mu.Lock()
	defer b.mu.Unlock()

	if b.size == 0 {
		return nil
	}

	result := make([]T, b.size)
	index := 0
	var zero T

	for i := 0; i < b.size; i++ {
		pos := (b.head + i) % b.capacity
		result[index] = b.items[pos]
		b.items[pos] = zero
		index++
	}

	b.head = 0
	b.tail = 0
	b.size = 0

	return result
}

func (b *RingBuffer[T]) Peek() (T, bool) {
	b.mu.RLock()
	defer b.mu.RUnlock()

	var zero T
	if b.size == 0 {
		return zero, false
	}

	return b.items[b.head], true
}

func (b *RingBuffer[T]) Size() int {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.size
}

func (b *RingBuffer[T]) Capacity() int {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.capacity
}

func (b *RingBuffer[T]) Category() ratelimit.Category {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.category
}

func (b *RingBuffer[T]) Priority() ratelimit.Priority {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.priority
}

func (b *RingBuffer[T]) IsEmpty() bool {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.size == 0
}

func (b *RingBuffer[T]) IsFull() bool {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.size == b.capacity
}

func (b *RingBuffer[T]) Utilization() float64 {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return float64(b.size) / float64(b.capacity)
}

func (b *RingBuffer[T]) OfferedCount() int64 {
	return atomic.LoadInt64(&b.offered)
}

func (b *RingBuffer[T]) DroppedCount() int64 {
	return atomic.LoadInt64(&b.dropped)
}

func (b *RingBuffer[T]) AcceptedCount() int64 {
	return b.OfferedCount() - b.DroppedCount()
}

func (b *RingBuffer[T]) DropRate() float64 {
	offered := b.OfferedCount()
	if offered == 0 {
		return 0.0
	}
	return float64(b.DroppedCount()) / float64(offered)
}

func (b *RingBuffer[T]) Clear() {
	b.mu.Lock()
	defer b.mu.Unlock()

	var zero T
	for i := 0; i < b.capacity; i++ {
		b.items[i] = zero
	}

	b.head = 0
	b.tail = 0
	b.size = 0
}

func (b *RingBuffer[T]) GetMetrics() BufferMetrics {
	b.mu.RLock()
	size := b.size
	util := float64(b.size) / float64(b.capacity)
	b.mu.RUnlock()

	return BufferMetrics{
		Category:      b.category,
		Priority:      b.priority,
		Capacity:      b.capacity,
		Size:          size,
		Utilization:   util,
		OfferedCount:  b.OfferedCount(),
		DroppedCount:  b.DroppedCount(),
		AcceptedCount: b.AcceptedCount(),
		DropRate:      b.DropRate(),
		LastUpdated:   time.Now(),
	}
}

func (b *RingBuffer[T]) IsReadyToFlush() bool {
	b.mu.RLock()
	defer b.mu.RUnlock()

	if b.size == 0 {
		return false
	}

	if b.size >= b.batchSize {
		return true
	}

	if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout {
		return true
	}

	return false
}

func (b *RingBuffer[T]) MarkFlushed() {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.lastFlushTime = time.Now()
}

func (b *RingBuffer[T]) PollIfReady() []T {
	b.mu.Lock()
	defer b.mu.Unlock()

	if b.size == 0 {
		return nil
	}

	ready := b.size >= b.batchSize ||
		(b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout)

	if !ready {
		return nil
	}

	itemCount := b.batchSize
	if itemCount > b.size {
		itemCount = b.size
	}

	result := make([]T, itemCount)
	var zero T

	for i := 0; i < itemCount; i++ {
		result[i] = b.items[b.head]
		b.items[b.head] = zero
		b.head = (b.head + 1) % b.capacity
		b.size--
	}

	b.lastFlushTime = time.Now()
	return result
}

func (b *RingBuffer[T]) recordDroppedItem(item T) {
	if ti, ok := any(item).(protocol.TelemetryItem); ok {
		b.recorder.RecordItem(report.ReasonBufferOverflow, ti)
	} else {
		b.recorder.RecordOne(report.ReasonBufferOverflow, b.category)
	}
}

type BufferMetrics struct {
	Category      ratelimit.Category `json:"category"`
	Priority      ratelimit.Priority `json:"priority"`
	Capacity      int                `json:"capacity"`
	Size          int                `json:"size"`
	Utilization   float64            `json:"utilization"`
	OfferedCount  int64              `json:"offered_count"`
	DroppedCount  int64              `json:"dropped_count"`
	AcceptedCount int64              `json:"accepted_count"`
	DropRate      float64            `json:"drop_rate"`
	LastUpdated   time.Time          `json:"last_updated"`
}

// OverflowPolicy defines how the ring buffer handles overflow.
type OverflowPolicy int

const (
	OverflowPolicyDropOldest OverflowPolicy = iota
	OverflowPolicyDropNewest
)

func (op OverflowPolicy) String() string {
	switch op {
	case OverflowPolicyDropOldest:
		return "drop_oldest"
	case OverflowPolicyDropNewest:
		return "drop_newest"
	default:
		return "unknown"
	}
}
