package autorelay

import (
	"context"
	"errors"
	"time"

	"github.com/libp2p/go-libp2p/core/peer"
)

// AutoRelay will call this function when it needs new candidates because it is
// not connected to the desired number of relays or we get disconnected from one
// of the relays. Implementations must send *at most* numPeers, and close the
// channel when they don't intend to provide any more peers. AutoRelay will not
// call the callback again until the channel is closed. Implementations should
// send new peers, but may send peers they sent before. AutoRelay implements a
// per-peer backoff (see WithBackoff). See WithMinInterval for setting the
// minimum interval between calls to the callback. The context.Context passed
// may be canceled when AutoRelay feels satisfied, it will be canceled when the
// node is shutting down. If the context is canceled you MUST close the output
// channel at some point.
type PeerSource func(ctx context.Context, num int) <-chan peer.AddrInfo

type config struct {
	clock      ClockWithInstantTimer
	peerSource PeerSource
	// minimum interval used to call the peerSource callback
	minInterval time.Duration
	// see WithMinCandidates
	minCandidates int
	// see WithMaxCandidates
	maxCandidates int
	// Delay until we obtain reservations with relays, if we have less than minCandidates candidates.
	// See WithBootDelay.
	bootDelay time.Duration
	// backoff is the time we wait after failing to obtain a reservation with a candidate
	backoff time.Duration
	// Number of relays we strive to obtain a reservation with.
	desiredRelays int
	// see WithMaxCandidateAge
	maxCandidateAge  time.Duration
	setMinCandidates bool
	// see WithMetricsTracer
	metricsTracer MetricsTracer
}

var defaultConfig = config{
	clock:           RealClock{},
	minCandidates:   4,
	maxCandidates:   20,
	bootDelay:       3 * time.Minute,
	backoff:         time.Hour,
	desiredRelays:   2,
	maxCandidateAge: 30 * time.Minute,
	minInterval:     30 * time.Second,
}

var (
	errAlreadyHavePeerSource = errors.New("can only use a single WithPeerSource or WithStaticRelays")
)

type Option func(*config) error

func WithStaticRelays(static []peer.AddrInfo) Option {
	return func(c *config) error {
		if c.peerSource != nil {
			return errAlreadyHavePeerSource
		}

		WithPeerSource(func(_ context.Context, numPeers int) <-chan peer.AddrInfo {
			if len(static) < numPeers {
				numPeers = len(static)
			}
			c := make(chan peer.AddrInfo, numPeers)
			defer close(c)

			for i := 0; i < numPeers; i++ {
				c <- static[i]
			}
			return c
		})(c)
		WithMinCandidates(len(static))(c)
		WithMaxCandidates(len(static))(c)
		WithNumRelays(len(static))(c)

		return nil
	}
}

// WithPeerSource defines a callback for AutoRelay to query for more relay candidates.
func WithPeerSource(f PeerSource) Option {
	return func(c *config) error {
		if c.peerSource != nil {
			return errAlreadyHavePeerSource
		}
		c.peerSource = f
		return nil
	}
}

// WithNumRelays sets the number of relays we strive to obtain reservations with.
func WithNumRelays(n int) Option {
	return func(c *config) error {
		c.desiredRelays = n
		return nil
	}
}

// WithMaxCandidates sets the number of relay candidates that we buffer.
func WithMaxCandidates(n int) Option {
	return func(c *config) error {
		c.maxCandidates = n
		if c.minCandidates > n {
			c.minCandidates = n
		}
		return nil
	}
}

// WithMinCandidates sets the minimum number of relay candidates we collect before to get a reservation
// with any of them (unless we've been running for longer than the boot delay).
// This is to make sure that we don't just randomly connect to the first candidate that we discover.
func WithMinCandidates(n int) Option {
	return func(c *config) error {
		if n > c.maxCandidates {
			n = c.maxCandidates
		}
		c.minCandidates = n
		c.setMinCandidates = true
		return nil
	}
}

// WithBootDelay set the boot delay for finding relays.
// We won't attempt any reservation if we've have less than a minimum number of candidates.
// This prevents us to connect to the "first best" relay, and allows us to carefully select the relay.
// However, in case we haven't found enough relays after the boot delay, we use what we have.
func WithBootDelay(d time.Duration) Option {
	return func(c *config) error {
		c.bootDelay = d
		return nil
	}
}

// WithBackoff sets the time we wait after failing to obtain a reservation with a candidate.
func WithBackoff(d time.Duration) Option {
	return func(c *config) error {
		c.backoff = d
		return nil
	}
}

// WithMaxCandidateAge sets the maximum age of a candidate.
// When we are connected to the desired number of relays, we don't ask the peer source for new candidates.
// This can lead to AutoRelay's candidate list becoming outdated, and means we won't be able
// to quickly establish a new relay connection if our existing connection breaks, if all the candidates
// have become stale.
func WithMaxCandidateAge(d time.Duration) Option {
	return func(c *config) error {
		c.maxCandidateAge = d
		return nil
	}
}

// InstantTimer is a timer that triggers at some instant rather than some duration
type InstantTimer interface {
	Reset(d time.Time) bool
	Stop() bool
	Ch() <-chan time.Time
}

// ClockWithInstantTimer is a clock that can create timers that trigger at some
// instant rather than some duration
type ClockWithInstantTimer interface {
	Now() time.Time
	Since(t time.Time) time.Duration
	InstantTimer(when time.Time) InstantTimer
}

type RealTimer struct{ t *time.Timer }

var _ InstantTimer = (*RealTimer)(nil)

func (t RealTimer) Ch() <-chan time.Time {
	return t.t.C
}

func (t RealTimer) Reset(d time.Time) bool {
	return t.t.Reset(time.Until(d))
}

func (t RealTimer) Stop() bool {
	return t.t.Stop()
}

type RealClock struct{}

var _ ClockWithInstantTimer = RealClock{}

func (RealClock) Now() time.Time {
	return time.Now()
}
func (RealClock) Since(t time.Time) time.Duration {
	return time.Since(t)
}
func (RealClock) InstantTimer(when time.Time) InstantTimer {
	t := time.NewTimer(time.Until(when))
	return &RealTimer{t}
}

func WithClock(cl ClockWithInstantTimer) Option {
	return func(c *config) error {
		c.clock = cl
		return nil
	}
}

// WithMinInterval sets the minimum interval after which peerSource callback will be called for more
// candidates even if AutoRelay needs new candidates.
func WithMinInterval(interval time.Duration) Option {
	return func(c *config) error {
		c.minInterval = interval
		return nil
	}
}

// WithMetricsTracer configures autorelay to use mt to track metrics
func WithMetricsTracer(mt MetricsTracer) Option {
	return func(c *config) error {
		c.metricsTracer = mt
		return nil
	}
}
