package api

import (
	"sync"
	"time"
)

// The live answer to "is Emby answering right now", as opposed to the one-shot alerts in
// server_alerts.go that announce the moment it changed.
//
// These are two different jobs and both are worth having. An alert is news: it slides in,
// says Emby stopped communicating and goes away, which is right for someone already
// watching. This is *state*: a television switched on twenty minutes into an outage was
// never told anything, and the one thing it can usefully show is a bar saying so and how
// long until the next attempt. It is the reason /v1/status carries it rather than the
// alert list — an alert that has fallen out of its window is gone, and this has not.
const (
	// How many probes in a row must fail before the bar appears.
	//
	// Deliberately lower than embyFailureThreshold, which gates the announcement. The
	// announcement is a one-shot that cannot be taken back, so it waits to be sure; the
	// bar clears itself the moment Emby answers again, so being early costs a minute of
	// a red strip rather than a false claim left standing.
	embyOutageThreshold = 2
)

// embyHealth is the probe's finding, cached in memory for the status poll to read. Not
// persisted: it describes this instant, and a value restored from a restart would be a
// claim about a probe that never ran.
type embyHealth struct {
	mu    sync.RWMutex
	state embyHealthState
}

type embyHealthState struct {
	// monitored is false when MEMBY_EMBY_HEALTH_INTERVAL turns the probe off. The client
	// must then ignore reachable entirely rather than trust a value nothing updates.
	monitored   bool
	reachable   bool
	since       time.Time
	checkedAt   time.Time
	retryEvery  time.Duration
	consecutive int
}

func (h *embyHealth) get() embyHealthState {
	h.mu.RLock()
	defer h.mu.RUnlock()
	return h.state
}

// begin records that the probe is running and how often, so the bar can say when the next
// attempt is due before any probe has completed.
func (h *embyHealth) begin(interval time.Duration, now time.Time) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.state = embyHealthState{
		monitored: true, reachable: true, since: now, retryEvery: interval,
	}
}

// record folds one probe result in and reports whether the published verdict changed.
func (h *embyHealth) record(ok bool, now time.Time) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.state.checkedAt = now
	if ok {
		h.state.consecutive = 0
		if !h.state.reachable {
			h.state.reachable = true
			h.state.since = now
		}
		return
	}
	h.state.consecutive++
	if h.state.reachable && h.state.consecutive >= embyOutageThreshold {
		h.state.reachable = false
		// Dated from the probe that crossed the threshold rather than the first failure:
		// the bar reports what the server knows, and until the threshold was crossed it
		// did not know anything.
		h.state.since = now
	}
}

// embyHealthPayload is the shape /v1/status carries. Times are RFC3339 so an old client
// that cannot parse them still renders the bar from reachable and retrySeconds alone.
type embyHealthPayload struct {
	Monitored    bool   `json:"monitored"`
	Reachable    bool   `json:"reachable"`
	Since        string `json:"since,omitempty"`
	CheckedAt    string `json:"checkedAt,omitempty"`
	RetrySeconds int    `json:"retrySeconds"`
}

func embyHealthFor(state embyHealthState) embyHealthPayload {
	payload := embyHealthPayload{
		Monitored:    state.monitored,
		Reachable:    state.reachable || !state.monitored,
		RetrySeconds: int(state.retryEvery / time.Second),
	}
	if !state.since.IsZero() {
		payload.Since = state.since.UTC().Format(time.RFC3339)
	}
	if !state.checkedAt.IsZero() {
		payload.CheckedAt = state.checkedAt.UTC().Format(time.RFC3339)
	}
	return payload
}
