package autonat

import (
	"errors"
	"time"

	"github.com/libp2p/go-libp2p/core/host"
	"github.com/libp2p/go-libp2p/core/network"
)

// config holds configurable options for the autonat subsystem.
type config struct {
	host host.Host

	addressFunc       AddrFunc
	dialPolicy        dialPolicy
	dialer            network.Network
	forceReachability bool
	reachability      network.Reachability
	metricsTracer     MetricsTracer

	// client
	bootDelay          time.Duration
	retryInterval      time.Duration
	refreshInterval    time.Duration
	requestTimeout     time.Duration
	throttlePeerPeriod time.Duration

	// server
	dialTimeout         time.Duration
	maxPeerAddresses    int
	throttleGlobalMax   int
	throttlePeerMax     int
	throttleResetPeriod time.Duration
	throttleResetJitter time.Duration
}

var defaults = func(c *config) error {
	c.bootDelay = 15 * time.Second
	c.retryInterval = 90 * time.Second
	c.refreshInterval = 15 * time.Minute
	c.requestTimeout = 30 * time.Second
	c.throttlePeerPeriod = 90 * time.Second

	c.dialTimeout = 15 * time.Second
	c.maxPeerAddresses = 16
	c.throttleGlobalMax = 30
	c.throttlePeerMax = 3
	c.throttleResetPeriod = 1 * time.Minute
	c.throttleResetJitter = 15 * time.Second
	return nil
}

const maxRefreshInterval = 24 * time.Hour

// EnableService specifies that AutoNAT should be allowed to run a NAT service to help
// other peers determine their own NAT status. The provided Network should not be the
// default network/dialer of the host passed to `New`, as the NAT system will need to
// make parallel connections, and as such will modify both the associated peerstore
// and terminate connections of this dialer. The dialer provided
// should be compatible (TCP/UDP) however with the transports of the libp2p network.
func EnableService(dialer network.Network) Option {
	return func(c *config) error {
		if dialer == c.host.Network() || dialer.Peerstore() == c.host.Peerstore() {
			return errors.New("dialer should not be that of the host")
		}
		c.dialer = dialer
		return nil
	}
}

// WithReachability overrides autonat to simply report an over-ridden reachability
// status.
func WithReachability(reachability network.Reachability) Option {
	return func(c *config) error {
		c.forceReachability = true
		c.reachability = reachability
		return nil
	}
}

// UsingAddresses allows overriding which Addresses the AutoNAT client believes
// are "its own". Useful for testing, or for more exotic port-forwarding
// scenarios where the host may be listening on different ports than it wants
// to externally advertise or verify connectability on.
func UsingAddresses(addrFunc AddrFunc) Option {
	return func(c *config) error {
		if addrFunc == nil {
			return errors.New("invalid address function supplied")
		}
		c.addressFunc = addrFunc
		return nil
	}
}

// WithSchedule configures how aggressively probes will be made to verify the
// address of the host. retryInterval indicates how often probes should be made
// when the host lacks confidence about its address, while refreshInterval
// is the schedule of periodic probes when the host believes it knows its
// steady-state reachability.
func WithSchedule(retryInterval, refreshInterval time.Duration) Option {
	return func(c *config) error {
		c.retryInterval = retryInterval
		c.refreshInterval = refreshInterval
		return nil
	}
}

// WithoutStartupDelay removes the initial delay the NAT subsystem typically
// uses as a buffer for ensuring that connectivity and guesses as to the hosts
// local interfaces have settled down during startup.
func WithoutStartupDelay() Option {
	return func(c *config) error {
		c.bootDelay = 1
		return nil
	}
}

// WithoutThrottling indicates that this autonat service should not place
// restrictions on how many peers it is willing to help when acting as
// a server.
func WithoutThrottling() Option {
	return func(c *config) error {
		c.throttleGlobalMax = 0
		return nil
	}
}

// WithThrottling specifies how many peers (`amount`) it is willing to help
// ever `interval` amount of time when acting as a server.
func WithThrottling(amount int, interval time.Duration) Option {
	return func(c *config) error {
		c.throttleGlobalMax = amount
		c.throttleResetPeriod = interval
		c.throttleResetJitter = interval / 4
		return nil
	}
}

// WithPeerThrottling specifies a limit for the maximum number of IP checks
// this node will provide to an individual peer in each `interval`.
func WithPeerThrottling(amount int) Option {
	return func(c *config) error {
		c.throttlePeerMax = amount
		return nil
	}
}

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