package api

import (
	"context"
	"net/http"
	"sync"
	"time"

	"github.com/ponzischeme89/memby/server/internal/buildinfo"
	"github.com/ponzischeme89/memby/server/internal/store"
)

// maintenanceState caches the operator switch in memory so the hot path never queries
// Postgres, while Postgres stays the source of truth across restarts.
type maintenanceState struct {
	mu    sync.RWMutex
	value store.Maintenance
}

func (m *maintenanceState) get() store.Maintenance {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.value
}

func (m *maintenanceState) set(value store.Maintenance) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.value = value
}

// LoadMaintenance primes the cached switch. Called at boot, and after every toggle.
func (s *Server) LoadMaintenance(ctx context.Context) error {
	state, err := s.store.Maintenance(ctx)
	if err != nil {
		return err
	}
	s.maintenance.set(state)
	if state.Enabled {
		s.log.Warn("starting in maintenance mode",
			"component", "maintenance", "message", state.Message)
	}
	return nil
}

// WatchMaintenance re-reads the switch periodically, so a change made directly in the
// database (or by another instance) is picked up without a restart.
func (s *Server) WatchMaintenance(ctx context.Context, interval time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := s.LoadMaintenance(ctx); err != nil {
				s.log.Warn("maintenance refresh failed", "component", "maintenance", "error", err)
			}
		}
	}
}

// maintenanceGate turns the whole client API off, independently of Emby.
//
// 503 with a machine-readable `maintenance: true` so the TV can show the operator's
// message rather than a generic network error. Admin routes and health checks are
// deliberately outside this gate — you need them most while the app is down.
func (s *Server) maintenanceGate(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		state := s.maintenance.get()
		if !state.Enabled {
			next.ServeHTTP(w, r)
			return
		}

		message := state.Message
		if message == "" {
			message = store.DefaultMaintenanceMessage
		}
		// Retry-After keeps well-behaved clients from hammering a service that has
		// already said it is unavailable.
		w.Header().Set("Retry-After", "300")
		writeJSON(w, http.StatusServiceUnavailable, map[string]any{
			"error":       message,
			"maintenance": true,
			"message":     message,
		})
	})
}

// handleServiceStatus is the live control channel clients poll while the app is open.
// It sits outside maintenanceGate so maintenance can interrupt playback rather than only
// being discovered the next time a content request happens to run.
// It also carries informational alerts, because this poll is the one thing the app is
// already listening to — a push channel would be a second connection for a banner.
func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, sess store.Session) {
	state := s.maintenance.get()
	message := state.Message
	if state.Enabled && message == "" {
		message = store.DefaultMaintenanceMessage
	}
	alerts := []clientAlert{}
	// Nothing to celebrate while the service is down, and the client is showing the
	// maintenance screen anyway.
	if !state.Enabled {
		if found := mergeAlerts(
			s.publishedAlerts(r.Context()),
			s.sonarrAiredAlerts(r.Context()),
		); len(found) > 0 {
			alerts = found
		}
	}
	compatible, compatibilityMessage := compatibilityFor(r)
	featurePolicy := s.currentFeaturePolicy(r.Context())
	writeJSON(w, http.StatusOK, map[string]any{
		"maintenance":          state.Enabled,
		"message":              message,
		"alerts":               alerts,
		"compatible":           compatible,
		"compatibilityMessage": compatibilityMessage,
		"clientVersion":        clientVersion(r),
		"clientProtocol":       clientProtocol(r),
		"gatewayVersion":       buildinfo.Version(),
		"serverProtocol":       ProtocolVersion,
		"featureSchemaVersion": featureSchemaVersion,
		"featureRevision":      featurePolicy.Revision,
		"safeMode":             featurePolicy.SafeMode,
		"features":             featureMap(featurePolicy, clientProtocolNumber(r), clientCapabilities(r)),
		// Whether Emby itself is answering, as distinct from whether Memby is. The TV
		// direct-plays from Emby, so this is the difference between "the film stopped for
		// no reason" and a bar saying what happened and when the next attempt is due.
		"emby": embyHealthFor(s.embyHealth.get()),
		// One number, not the document: the TV compares it with what it has and only
		// fetches /v1/preferences when they differ. That is what turns this poll into the
		// delivery channel for an operator pushing someone's settings.
		"preferencesRevision": s.preferenceRevisionFor(r, sess),
		// The theme, as an id and a revision rather than the palette itself — the
		// preferencesRevision precedent, for the same reason. The set refetches /v1/theme
		// only when one of these moves, which is what makes a season arriving at midnight
		// cost one request per television instead of a palette on every ten-second poll.
		// It rides the poll rather than the sign-in because that is the whole point: a
		// season has to reach a set that is already switched on, without anybody doing
		// anything.
		"theme": themeStatus(s.themeFor(r.Context(), sess)),
	})
}
