package api

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

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

// The console's half of subtitle downloads.
//
// The operator has two providers to choose between and they need different things said
// about them. Bazarr is a service the household already runs, so the console can only turn
// it on or off — its address is deployment configuration and stays an environment
// variable. OpenSubtitles is an account, so its credentials live here and can be entered,
// replaced or removed without a redeployment.
//
// Nothing on this page ever returns a credential. The console is told whether a key is
// saved and whether an account is attached, which is what an operator needs to answer
// "why is this not working", and never the values themselves — the stance the MDBList page
// already takes.

// subtitleAdminSettings is the page's whole view of the policy.
type subtitleAdminSettings struct {
	// BazarrConfigured is whether this deployment has a Bazarr at all. It is separate from
	// BazarrEnabled so the page can say "no address configured" rather than drawing a
	// switch that would do nothing.
	BazarrConfigured bool   `json:"bazarrConfigured"`
	BazarrEnabled    bool   `json:"bazarrEnabled"`
	BazarrURL        string `json:"bazarrUrl,omitempty"`

	OpenSubtitlesEnabled       bool `json:"openSubtitlesEnabled"`
	OpenSubtitlesKeyConfigured bool `json:"openSubtitlesKeyConfigured"`
	// OpenSubtitlesAccount is whether a username and password are saved. It matters more
	// than it looks: without one, downloads go against the anonymous allowance, which is a
	// handful of files a day and fails in front of a television rather than in a log.
	OpenSubtitlesAccount  bool   `json:"openSubtitlesAccount"`
	OpenSubtitlesUsername string `json:"openSubtitlesUsername,omitempty"`

	// FeatureEnabled is the `subtitle_download` flag. It is reported here because it
	// overrides both providers, and an operator who has turned it off on the features page
	// should not have to guess why these switches do nothing.
	FeatureEnabled bool `json:"featureEnabled"`
	// Available is the answer a television gets: the feature is on and at least one
	// provider can be asked.
	Available bool `json:"available"`

	Stored store.DownloadedSubtitleStats `json:"stored"`
}

func (s *Server) subtitleAdminSettings(ctx context.Context) subtitleAdminSettings {
	policy := s.subtitlePolicy(ctx)
	sources := s.subtitleSources(ctx)
	settings := subtitleAdminSettings{
		BazarrConfigured:           s.bazarr != nil,
		BazarrEnabled:              policy.BazarrEnabled,
		BazarrURL:                  s.cfg.BazarrURL,
		OpenSubtitlesEnabled:       policy.OpenSubtitlesEnabled,
		OpenSubtitlesKeyConfigured: policy.OpenSubtitlesAPIKey != "",
		OpenSubtitlesAccount:       policy.OpenSubtitlesUsername != "" && policy.OpenSubtitlesPassword != "",
		OpenSubtitlesUsername:      policy.OpenSubtitlesUsername,
		FeatureEnabled:             s.featureEnabled(ctx, featureSubtitleDownload),
		Available:                  sources.any(),
	}
	if stats, err := s.store.DownloadedSubtitleStats(ctx); err == nil {
		settings.Stored = stats
	} else {
		s.loggerFor(ctx).Warn("downloaded subtitle stats failed", "error", err)
	}
	return settings
}

type subtitleSettingsRequest struct {
	Action string `json:"action"`

	BazarrEnabled        bool `json:"bazarrEnabled"`
	OpenSubtitlesEnabled bool `json:"openSubtitlesEnabled"`

	// A blank key keeps whatever is saved, so an operator changing one switch does not
	// have to paste a credential back in to do it. Clearing is its own flag, because
	// "leave it alone" and "remove it" cannot both be the empty string.
	OpenSubtitlesAPIKey      string `json:"openSubtitlesApiKey"`
	ClearOpenSubtitlesAPIKey bool   `json:"clearOpenSubtitlesApiKey"`
	OpenSubtitlesUsername    string `json:"openSubtitlesUsername"`
	OpenSubtitlesPassword    string `json:"openSubtitlesPassword"`
	ClearOpenSubtitlesLogin  bool   `json:"clearOpenSubtitlesLogin"`
}

func (s *Server) handleAdminSubtitleSettings(w http.ResponseWriter, r *http.Request) {
	var req subtitleSettingsRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}
	ctx := r.Context()

	// Emptying the store is the page's one destructive control, and it is safe in the way
	// a cache purge is: every file can be fetched again, at the cost of the provider
	// allowance that fetched it. It is a separate action rather than a checkbox on the
	// save, so it cannot happen as a side effect of changing a switch.
	if strings.TrimSpace(req.Action) == "clear-stored" {
		removed, err := s.store.ClearDownloadedSubtitles(ctx)
		if err != nil {
			s.loggerFor(ctx).Error("clearing stored subtitles failed", "error", err)
			writeError(w, http.StatusInternalServerError, "could not clear stored subtitles")
			return
		}
		s.loggerFor(ctx).Info("stored subtitles cleared", "removed", removed)
		writeJSON(w, http.StatusOK, s.subtitleAdminSettings(ctx))
		return
	}

	current, err := s.store.SubtitlePolicy(ctx)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "could not read subtitle settings")
		return
	}
	next := store.SubtitlePolicy{
		BazarrEnabled:         req.BazarrEnabled,
		OpenSubtitlesEnabled:  req.OpenSubtitlesEnabled,
		OpenSubtitlesAPIKey:   current.OpenSubtitlesAPIKey,
		OpenSubtitlesUsername: current.OpenSubtitlesUsername,
		OpenSubtitlesPassword: current.OpenSubtitlesPassword,
	}
	if req.ClearOpenSubtitlesAPIKey {
		next.OpenSubtitlesAPIKey = ""
	} else if replacement := strings.TrimSpace(req.OpenSubtitlesAPIKey); replacement != "" {
		next.OpenSubtitlesAPIKey = replacement
	}
	if req.ClearOpenSubtitlesLogin {
		next.OpenSubtitlesUsername, next.OpenSubtitlesPassword = "", ""
	} else if username := strings.TrimSpace(req.OpenSubtitlesUsername); username != "" {
		next.OpenSubtitlesUsername = username
		// The password only moves when one was typed. Changing a username without
		// retyping the password is an ordinary edit, and the field is blank on every load.
		if password := req.OpenSubtitlesPassword; password != "" {
			next.OpenSubtitlesPassword = password
		}
	}

	if err := s.store.SetSubtitlePolicy(ctx, next); err != nil {
		s.loggerFor(ctx).Error("subtitle policy write failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not save subtitle settings")
		return
	}
	s.loggerFor(ctx).Info("subtitle providers changed",
		"bazarr", next.BazarrEnabled,
		"opensubtitles", next.OpenSubtitlesEnabled,
		"opensubtitles_account", next.OpenSubtitlesUsername != "",
	)
	writeJSON(w, http.StatusOK, s.subtitleAdminSettings(ctx))
}

// handleAdminSubtitleTest asks each enabled provider whether it is actually reachable.
//
// It exists because every other symptom of a wrong key looks identical from a television:
// the search comes back empty. One button that says "the key is rejected" is the whole
// difference between a five-minute fix and an evening of guessing.
func (s *Server) handleAdminSubtitleTest(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	type probe struct {
		Provider string `json:"provider"`
		OK       bool   `json:"ok"`
		Message  string `json:"message"`
	}
	results := []probe{}

	if s.bazarr != nil {
		result := probe{Provider: "Bazarr", OK: true, Message: "Reachable."}
		if err := s.bazarr.Ping(ctx); err != nil {
			result.OK, result.Message = false, "Did not answer: "+err.Error()
		}
		results = append(results, result)
	}
	if client := s.openSubtitlesClient(ctx); client != nil {
		result := probe{Provider: "OpenSubtitles", OK: true}
		if err := client.Ping(ctx); err != nil {
			result.OK, result.Message = false, "Did not answer: "+err.Error()
		} else if client.HasAccount() {
			result.Message = "Reachable, signed in."
		} else {
			// Worth saying rather than reporting a plain success: an anonymous key works
			// perfectly for searching and runs out after a few downloads, which is the
			// failure this page exists to make findable.
			result.Message = "Reachable, but with no account — downloads use the small anonymous allowance."
		}
		results = append(results, result)
	}
	writeJSON(w, http.StatusOK, map[string]any{"results": results})
}
