package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"slices"
	"strings"

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

const (
	featureSchemaVersion       = 1
	featureSonarrPreroll       = "sonarr_preroll"
	featureAutomaticMyShows    = "automatic_my_shows"
	featureMyShowsNotification = "my_shows_notifications"
	featureHEVCDirectPlay      = "hevc_direct_play"
	featureInstallPermission   = "install_permission_prompt"
	featureSubtitleDownload    = "subtitle_download"
	featureTrickplay           = "trickplay"
	featureSkipIntro           = "skip_intro"
	featureEndCredits          = "end_credits"
	featureSeasonalThemes      = "seasonal_themes"
	featureSeasonalDecorations = "seasonal_decorations"
	featureGenreBrowser        = "genre_browser"
	featureTVCalendar          = "tv_calendar"
)

type featureDefinition struct {
	Key             string `json:"key"`
	Name            string `json:"name"`
	Description     string `json:"description"`
	Area            string `json:"area"`
	DefaultEnabled  bool   `json:"defaultEnabled"`
	MinimumProtocol int    `json:"minimumProtocol"`
	Capability      string `json:"capability"`
	Recovery        string `json:"recovery"`
}

var featureCatalogue = []featureDefinition{
	{
		Key: featureSonarrPreroll, Name: "Sonarr upcoming preroll", Area: "Playback",
		Description:    "Show the fan-art calendar before a fresh episode starts.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "sonarr_preroll_v1",
		Recovery: "Takes effect the next time a title is opened.",
	},
	{
		Key: featureAutomaticMyShows, Name: "Automatic My Shows", Area: "My Shows",
		Description:    "Follow a continuing Sonarr show after half an episode is watched.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "auto_my_shows_v1",
		Recovery: "Server-enforced; takes effect on the next playback report.",
	},
	{
		Key: featureMyShowsNotification, Name: "Automatic follow notification", Area: "Notifications",
		Description:    "Notify a viewer when a continuing show is automatically followed.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "auto_my_shows_v1",
		Recovery: "Server-enforced; disabling it never removes a saved show.",
	},
	{
		Key: featureHEVCDirectPlay, Name: "HEVC direct play", Area: "Playback",
		Description:    "Allow capable TVs to direct-play H.265/HEVC instead of requesting H.264.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "video_hevc_decode",
		Recovery: "Server-enforced; takes effect the next time playback starts or refreshes.",
	},
	{
		Key: featureSubtitleDownload, Name: "Download missing subtitles", Area: "Playback",
		Description: "Let a viewer fetch a subtitle through Bazarr from the player, for a " +
			"title the library has none for.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "subtitle_download_v1",
		Recovery: "Takes effect the next time playback starts; the option simply stops being offered.",
	},
	{
		Key: featureTrickplay, Name: "Seek preview thumbnails", Area: "Playback",
		Description: "Show the frame a skip will land on, from the preview images Emby " +
			"generates. Turn it off to stop the gateway reading them.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "trickplay_v1",
		Recovery: "Takes effect the next time playback starts; the preview simply stops appearing.",
	},
	{
		Key: featureSkipIntro, Name: "Skip the title sequence", Area: "Playback",
		Description: "Offer to jump past an episode's opening titles, from the intro " +
			"markers Emby writes. Turn it off to stop the gateway reading them.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "skip_intro_v1",
		Recovery: "Takes effect the next time playback starts; the button simply stops appearing.",
	},
	{
		Key: featureEndCredits, Name: "Speed through the credits", Area: "Playback",
		Description: "Shrink the picture and run the closing credits at double speed with " +
			"the next episode beside them, from the credits marker Emby writes. It is read " +
			"from the same chapter list as the title sequence, so turning this off saves no " +
			"request unless that is off too.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "end_credits_v1",
		Recovery: "Takes effect the next time playback starts; the credits simply play out full size.",
	},
	{
		// The only switch there is for seasonal themes, and it is deliberately the
		// operator's rather than the viewer's: a per-person opt-out is a thing somebody
		// turns off in October and never reconsiders, which is the same as the feature not
		// existing. Off here means every television falls back to its viewer's own choice
		// on the next status poll.
		Key: featureSeasonalThemes, Name: "Seasonal themes", Area: "Presentation",
		Description: "Put every television into the Halloween, Christmas or Easter palette " +
			"for its dates. Viewers cannot decline one; turning this off is the only way to " +
			"stop them.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "themes_v1",
		Recovery: "Takes effect on the next status poll, within ten seconds on an open TV.",
	},
	{
		// A second switch rather than a consequence of the one above, because the palette
		// and the animation have quite different costs. Snow drifting over the launcher is
		// the only thing in the app that animates continuously while somebody is browsing,
		// and these are weak boxes; an operator who finds it costs frames should be able to
		// keep December looking like December without it.
		Key: featureSeasonalDecorations, Name: "Seasonal decorations", Area: "Presentation",
		Description: "Drift snow, bats or blossom over the launcher while a seasonal theme " +
			"is on. Turning it off keeps the seasonal colours and stops the animation.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "seasonal_decorations_v1",
		Recovery: "Takes effect on the next status poll; the launcher simply stops drawing them.",
	},
	{
		Key: featureGenreBrowser, Name: "Genre browser", Area: "Presentation",
		Description:    "Show the full genre browser at the top of Movies and TV Shows.",
		DefaultEnabled: false, MinimumProtocol: 1, Capability: "genre_browser_v1",
		Recovery: "Takes effect on the next status poll; the browser is hidden when off.",
	},
	{
		Key: featureTVCalendar, Name: "TV calendar", Area: "Presentation",
		Description: "Show the month-by-month Sonarr calendar on the navigation rail. " +
			"Turning it off hides the destination and stops the gateway reading months.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "tv_calendar_v1",
		Recovery: "Takes effect on the next status poll; the rail entry simply disappears.",
	},
	{
		Key: featureInstallPermission, Name: "Ask TVs for install permission", Area: "Setup",
		Description: "Ask a signed-in TV that cannot install its own updates to grant the " +
			"permission, so a mandatory update is not the first time it comes up.",
		DefaultEnabled: true, MinimumProtocol: 1, Capability: "install_permission_v1",
		Recovery: "Appears on the next status poll, and only on a TV missing the permission.",
	},
}

type evaluatedFeature struct {
	featureDefinition
	Enabled    bool   `json:"enabled"`
	Source     string `json:"source"`
	Compatible bool   `json:"compatible"`
}

type featureResponse struct {
	SchemaVersion int                `json:"schemaVersion"`
	Revision      int64              `json:"revision"`
	SafeMode      bool               `json:"safeMode"`
	UpdatedAt     any                `json:"updatedAt,omitempty"`
	CanRollback   bool               `json:"canRollback"`
	Features      []evaluatedFeature `json:"features"`
}

func knownFeature(key string) (featureDefinition, bool) {
	for _, definition := range featureCatalogue {
		if definition.Key == key {
			return definition, true
		}
	}
	return featureDefinition{}, false
}

func evaluateFeature(policy store.FeaturePolicy, definition featureDefinition, protocol int) evaluatedFeature {
	enabled, source := definition.DefaultEnabled, "default"
	if override, ok := policy.Overrides[definition.Key]; ok {
		enabled, source = override, "override"
	}
	if policy.SafeMode {
		enabled, source = false, "safe_mode"
	}
	compatible := protocol >= definition.MinimumProtocol
	if !compatible {
		enabled, source = false, "incompatible_client"
	}
	return evaluatedFeature{featureDefinition: definition, Enabled: enabled, Source: source, Compatible: compatible}
}

func (s *Server) currentFeaturePolicy(ctx context.Context) store.FeaturePolicy {
	if s.store == nil {
		return store.DefaultFeaturePolicy()
	}
	policy, err := s.store.FeaturePolicy(ctx)
	if err != nil {
		s.loggerFor(ctx).Warn("feature policy unavailable; using safe defaults", "error", err)
		return store.DefaultFeaturePolicy()
	}
	return policy
}

func (s *Server) featureEnabled(ctx context.Context, key string) bool {
	definition, ok := knownFeature(key)
	if !ok {
		return false
	}
	return evaluateFeature(s.currentFeaturePolicy(ctx), definition, ProtocolVersion).Enabled
}

func featurePayload(policy store.FeaturePolicy, protocol int, capabilities ...[]string) featureResponse {
	features := make([]evaluatedFeature, 0, len(featureCatalogue))
	for _, definition := range featureCatalogue {
		evaluated := evaluateFeature(policy, definition, protocol)
		if len(capabilities) > 0 && definition.Capability != "" &&
			!slices.Contains(capabilities[0], definition.Capability) {
			evaluated.Enabled = false
			evaluated.Compatible = false
			evaluated.Source = "missing_capability"
		}
		features = append(features, evaluated)
	}
	return featureResponse{
		SchemaVersion: featureSchemaVersion, Revision: policy.Revision,
		SafeMode: policy.SafeMode, UpdatedAt: policy.UpdatedAt,
		CanRollback: policy.Previous != nil, Features: features,
	}
}

func featureMap(policy store.FeaturePolicy, protocol int, capabilities []string) map[string]bool {
	result := make(map[string]bool, len(featureCatalogue))
	for _, feature := range featurePayload(policy, protocol, capabilities).Features {
		result[feature.Key] = feature.Enabled
	}
	return result
}

func (s *Server) handleFeatures(w http.ResponseWriter, r *http.Request, _ store.Session) {
	writeJSON(w, http.StatusOK, featurePayload(
		s.currentFeaturePolicy(r.Context()), clientProtocolNumber(r), clientCapabilities(r),
	))
}

type featurePolicyRequest struct {
	Action           string          `json:"action"`
	ExpectedRevision int64           `json:"expectedRevision"`
	Overrides        map[string]bool `json:"overrides"`
}

func (s *Server) handleAdminFeaturePolicy(w http.ResponseWriter, r *http.Request) {
	var req featurePolicyRequest
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}
	current := s.currentFeaturePolicy(r.Context())
	if req.ExpectedRevision != current.Revision {
		writeError(w, http.StatusConflict, "feature flags changed in another admin session; refresh before saving")
		return
	}
	next := store.FeaturePolicy{Overrides: map[string]bool{}, SafeMode: current.SafeMode}
	switch strings.TrimSpace(req.Action) {
	case "save":
		for key, enabled := range req.Overrides {
			if _, ok := knownFeature(key); !ok {
				writeError(w, http.StatusBadRequest, "unknown feature flag: "+key)
				return
			}
			next.Overrides[key] = enabled
		}
	case "safe-mode":
		next.Overrides = current.Overrides
		next.SafeMode = true
	case "leave-safe-mode":
		next.Overrides = current.Overrides
		next.SafeMode = false
	case "reset":
		next.SafeMode = false
	case "rollback":
		if current.Previous == nil {
			writeError(w, http.StatusConflict, "there is no previous feature revision to restore")
			return
		}
		next.Overrides = current.Previous.Overrides
		next.SafeMode = current.Previous.SafeMode
	default:
		writeError(w, http.StatusBadRequest, "unknown feature policy action")
		return
	}
	stored, err := s.store.SetFeaturePolicy(r.Context(), next, req.ExpectedRevision)
	if err != nil {
		if errors.Is(err, store.ErrFeaturePolicyConflict) {
			writeError(w, http.StatusConflict, "feature flags changed in another admin session; refresh before saving")
			return
		}
		s.loggerFor(r.Context()).Error("feature policy write failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not save feature flags")
		return
	}
	s.loggerFor(r.Context()).Info("feature policy changed", "action", req.Action, "revision", stored.Revision,
		"safe_mode", stored.SafeMode, "overrides", len(stored.Overrides))
	writeJSON(w, http.StatusOK, featurePayload(stored, ProtocolVersion))
}

func parseCapabilities(raw string) []string {
	seen := map[string]bool{}
	values := []string{}
	for _, value := range strings.Split(raw, ",") {
		value = strings.ToLower(strings.TrimSpace(value))
		if value == "" || len(value) > 64 || seen[value] {
			continue
		}
		seen[value] = true
		values = append(values, value)
	}
	slices.Sort(values)
	return values
}

func clientCapabilities(r *http.Request) []string {
	return parseCapabilities(r.Header.Get("X-Memby-Capabilities"))
}
