package roundrobin

import (
	"sort"
	"sync"
	"time"

	"github.com/micro/go-micro/v2/router"
	"github.com/micro/go-micro/v2/selector"
)

var routeTTL = time.Minute * 15

// NewSelector returns an initalised round robin selector
func NewSelector(opts ...selector.Option) selector.Selector {
	r := &roundrobin{
		routes: make(map[uint64]time.Time),
		ticker: time.NewTicker(time.Minute),
	}
	go r.cleanRoutes()
	return r
}

type roundrobin struct {
	ticker *time.Ticker

	// routes is a map with the key being a route's hash and the value being the last time it
	// was used to perform a request
	routes map[uint64]time.Time
	sync.Mutex
}

func (r *roundrobin) Init(opts ...selector.Option) error {
	return nil
}

func (r *roundrobin) Options() selector.Options {
	return selector.Options{}
}

func (r *roundrobin) Select(routes []router.Route, opts ...selector.SelectOption) (*router.Route, error) {
	// parse the options
	options := selector.NewSelectOptions(opts...)

	// apply the filters
	for _, f := range options.Filters {
		routes = f(routes)
	}

	if len(routes) == 0 {
		return nil, selector.ErrNoneAvailable
	}

	r.Lock()
	defer r.Unlock()

	// setLastUsed will update the last used time for a route
	setLastUsed := func(hash uint64) {
		r.routes[hash] = time.Now()
	}

	// if a route hasn't yet been seen, prioritise it
	for _, route := range routes {
		if _, ok := r.routes[route.Hash()]; !ok {
			setLastUsed(route.Hash())
			return &route, nil
		}
	}

	// sort the services by the time they were last used
	sort.SliceStable(routes, func(i, j int) bool {
		iLastSeen := r.routes[routes[i].Hash()]
		jLastSeen := r.routes[routes[j].Hash()]
		return iLastSeen.UnixNano() < jLastSeen.UnixNano()
	})

	// return the route which was last used
	setLastUsed(routes[0].Hash())
	return &routes[0], nil
}

func (r *roundrobin) Record(srv router.Route, err error) error {
	return nil
}

func (r *roundrobin) Close() error {
	r.ticker.Stop()
	return nil
}

func (r *roundrobin) String() string {
	return "roundrobin"
}

func (r *roundrobin) cleanRoutes() {
	for {
		// watch for ticks until the ticker is closed
		if _, ok := <-r.ticker.C; !ok {
			return
		}

		r.Lock()

		// copy the slice to prevent concurrent map iteration and map write
		rts := r.routes

		for hash, t := range rts {
			if t.Unix() < time.Now().Add(-routeTTL).Unix() {
				delete(r.routes, hash)
			}
		}
		r.Unlock()
	}
}