package http

import (
	"net/url"
	"sync"
	"time"
)

// unreachableRetryInterval is how long a server that failed to answer is tried
// last before a read probes it again.
const unreachableRetryInterval = 30 * time.Second

// unreachable maps a host to when it last failed to answer a request. A dead
// replica sits in the location list of every volume it held, so this is kept
// per host rather than per volume.
var unreachable sync.Map

func recordUnreachable(host string) {
	unreachable.Store(host, time.Now())
}

func recordReachable(host string) {
	unreachable.Delete(host)
}

// ReachableFirst orders urls so that hosts which recently failed to answer
// come last, keeping the order within each group. Once the retry interval has
// passed, the first read to ask claims the probe and tries that host first;
// the others keep it last until the probe settles. A read probes at most one
// host, so a claim is always followed by an attempt and several expired hosts
// are probed by successive reads.
func ReachableFirst(urls []string) []string {
	var probe, reachable, unanswered []string
	for _, u := range urls {
		host := hostOf(u)
		failedAt, failed := unreachable.Load(host)
		switch {
		case !failed:
			reachable = append(reachable, u)
		case time.Since(failedAt.(time.Time)) < unreachableRetryInterval:
			unanswered = append(unanswered, u)
		case probe == nil && unreachable.CompareAndSwap(host, failedAt, time.Now()):
			probe = append(probe, u)
		default:
			unanswered = append(unanswered, u)
		}
	}
	if probe == nil && unanswered == nil {
		return urls
	}
	return append(append(probe, reachable...), unanswered...)
}

func hostOf(rawUrl string) string {
	parsed, err := url.Parse(rawUrl)
	if err != nil {
		return ""
	}
	return parsed.Host
}
