package relay

import (
	"errors"
	"sync"
	"time"

	asnutil "github.com/libp2p/go-libp2p-asn-util"
	"github.com/libp2p/go-libp2p/core/peer"

	ma "github.com/multiformats/go-multiaddr"
	manet "github.com/multiformats/go-multiaddr/net"
)

var validity = 30 * time.Minute

var (
	errTooManyReservations        = errors.New("too many reservations")
	errTooManyReservationsForPeer = errors.New("too many reservations for peer")
	errTooManyReservationsForIP   = errors.New("too many peers for IP address")
	errTooManyReservationsForASN  = errors.New("too many peers for ASN")
)

// constraints implements various reservation constraints
type constraints struct {
	rc *Resources

	mutex sync.Mutex
	total []time.Time
	peers map[peer.ID][]time.Time
	ips   map[string][]time.Time
	asns  map[uint32][]time.Time
}

// newConstraints creates a new constraints object.
// The methods are *not* thread-safe; an external lock must be held if synchronization
// is required.
func newConstraints(rc *Resources) *constraints {
	return &constraints{
		rc:    rc,
		peers: make(map[peer.ID][]time.Time),
		ips:   make(map[string][]time.Time),
		asns:  make(map[uint32][]time.Time),
	}
}

// AddReservation adds a reservation for a given peer with a given multiaddr.
// If adding this reservation violates IP constraints, an error is returned.
func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	now := time.Now()
	c.cleanup(now)

	if len(c.total) >= c.rc.MaxReservations {
		return errTooManyReservations
	}

	ip, err := manet.ToIP(a)
	if err != nil {
		return errors.New("no IP address associated with peer")
	}

	peerReservations := c.peers[p]
	if len(peerReservations) >= c.rc.MaxReservationsPerPeer {
		return errTooManyReservationsForPeer
	}

	ipReservations := c.ips[ip.String()]
	if len(ipReservations) >= c.rc.MaxReservationsPerIP {
		return errTooManyReservationsForIP
	}

	var asnReservations []time.Time
	var asn uint32
	if ip.To4() == nil {
		asn = asnutil.AsnForIPv6(ip)
		if asn != 0 {
			asnReservations = c.asns[asn]
			if len(asnReservations) >= c.rc.MaxReservationsPerASN {
				return errTooManyReservationsForASN
			}
		}
	}

	expiry := now.Add(validity)
	c.total = append(c.total, expiry)

	peerReservations = append(peerReservations, expiry)
	c.peers[p] = peerReservations

	ipReservations = append(ipReservations, expiry)
	c.ips[ip.String()] = ipReservations

	if asn != 0 {
		asnReservations = append(asnReservations, expiry)
		c.asns[asn] = asnReservations
	}
	return nil
}

func (c *constraints) cleanupList(l []time.Time, now time.Time) []time.Time {
	var index int
	for i, t := range l {
		if t.After(now) {
			break
		}
		index = i + 1
	}
	return l[index:]
}

func (c *constraints) cleanup(now time.Time) {
	c.total = c.cleanupList(c.total, now)
	for k, peerReservations := range c.peers {
		c.peers[k] = c.cleanupList(peerReservations, now)
	}
	for k, ipReservations := range c.ips {
		c.ips[k] = c.cleanupList(ipReservations, now)
	}
	for k, asnReservations := range c.asns {
		c.asns[k] = c.cleanupList(asnReservations, now)
	}
}
