package datachannel

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pion/datachannel"

	"github.com/livekit/protocol/utils/mono"
)

const (
	singleWriteTimeout = 50 * time.Millisecond
)

var (
	ErrDataDroppedBySlowReader         = errors.New("data dropped by slow reader")
	ErrDataDroppedByHighBufferedAmount = errors.New("data dropped due to high buffered amount")
)

type BufferedAmountGetter interface {
	BufferedAmount() uint64
}

type DataChannelWriter[T BufferedAmountGetter] struct {
	bufferGetter      T
	rawDC             datachannel.ReadWriteCloserDeadliner
	slowThreshold     int
	rate              *BitrateCalculator
	reliable          bool
	targetLatency     time.Duration
	minBufferedAmount uint64
}

// NewDataChannelWriterReliable creates a new DataChannelWriter for reliable data channel by
// detaching it, when writing to the datachanel times out, it will block and retry if the
// receiver's bitrate is above the slowThreshold or drop the data if it's below the threshold.
// If the slowThreshold is 0, it will never retry on write timeout.
func NewDataChannelWriterReliable[T BufferedAmountGetter](bufferGetter T, rawDC datachannel.ReadWriteCloserDeadliner, slowThreshold int) *DataChannelWriter[T] {
	var rate *BitrateCalculator
	if slowThreshold > 0 {
		rate = NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeBusyOnly)
	}
	return &DataChannelWriter[T]{
		bufferGetter:  bufferGetter,
		rawDC:         rawDC,
		slowThreshold: slowThreshold,
		rate:          rate,
		reliable:      true,
	}
}

// NewDataChannelWriterUnreliable creates a new DataChannelWriter for unreliable data channel.
// It will drop data when the buffered amount is too high to maintain the target latency.
// The latency is estimated based on the bitrate in past 1 second. If targetLatency is 0, no
// buffering control is applied.
func NewDataChannelWriterUnreliable[T BufferedAmountGetter](bufferGetter T, rawDC datachannel.ReadWriteCloserDeadliner, targetLatency time.Duration, minBufferedAmount uint64) *DataChannelWriter[T] {
	var rate *BitrateCalculator
	if targetLatency > 0 {
		rate = NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeBusyOnly)
	}
	return &DataChannelWriter[T]{
		bufferGetter:      bufferGetter,
		rawDC:             rawDC,
		rate:              rate,
		targetLatency:     targetLatency,
		minBufferedAmount: minBufferedAmount,
		reliable:          false,
	}
}

func (w *DataChannelWriter[T]) BufferedAmountGetter() T {
	return w.bufferGetter
}

func (w *DataChannelWriter[T]) Write(p []byte) (n int, err error) {
	if w.reliable {
		return w.writeReliable(p)
	} else {
		return w.writeUnreliable(p)
	}
}

func (w *DataChannelWriter[T]) writeReliable(p []byte) (n int, err error) {
	for {
		err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
		if err != nil {
			return 0, err
		}
		n, err = w.rawDC.Write(p)
		if w.slowThreshold == 0 {
			return
		}

		now := mono.Now()
		w.rate.AddBytes(n, int(w.bufferGetter.BufferedAmount()), now)
		// retry if the write timed out on a non-slow receiver
		if errors.Is(err, context.DeadlineExceeded) {
			if bitrate, ok := w.rate.Bitrate(now); !ok || bitrate >= w.slowThreshold {
				continue
			} else {
				err = fmt.Errorf("%w: bitrate %d, threshold %d", ErrDataDroppedBySlowReader, bitrate, w.slowThreshold)
			}
		}

		return
	}
}

func (w *DataChannelWriter[T]) writeUnreliable(p []byte) (n int, err error) {
	if w.targetLatency == 0 {
		err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
		if err != nil {
			return 0, err
		}
		return w.rawDC.Write(p)
	}

	if bitrate, ok := w.rate.Bitrate(time.Now()); ok {
		// control buffer latency to target
		bufferedAmount := w.bufferGetter.BufferedAmount()
		targetLatencyLimit := uint64(time.Duration(bitrate) * w.targetLatency / 8 / time.Second)
		if bufferedAmount > targetLatencyLimit && bufferedAmount > w.minBufferedAmount {
			return 0, fmt.Errorf(
				"%w: bitrate %d, buffered amount %d, target latency limit %d, min buffered amount %d",
				ErrDataDroppedByHighBufferedAmount,
				bitrate,
				bufferedAmount,
				targetLatencyLimit,
				w.minBufferedAmount,
			)
		}
	}

	err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
	if err != nil {
		return 0, err
	}
	n, err = w.rawDC.Write(p)
	// Track drained bytes on every write (not just failures) so the bitrate
	// calculator has samples; otherwise Bitrate() has no data and the
	// latency-based drop threshold above collapses to the static minBufferedAmount.
	w.rate.AddBytes(n, int(w.bufferGetter.BufferedAmount()), mono.Now())

	return
}

func (w *DataChannelWriter[T]) Close() error {
	return w.rawDC.Close()
}
