package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"sync"

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

type requestCandidate struct {
	MediaType    string `json:"mediaType"`
	ForeignID    int    `json:"foreignId"`
	Title        string `json:"title"`
	Year         int    `json:"year"`
	Overview     string `json:"overview"`
	PosterURL    string `json:"posterUrl,omitempty"`
	AlreadyAdded bool   `json:"alreadyAdded"`
}

type requestLookupResponse struct {
	Candidates []requestCandidate `json:"candidates"`
}

func (s *Server) requestAllowed(r *http.Request, sess store.Session) bool {
	if s.store == nil || (s.sonarr == nil && s.radarr == nil) {
		return false
	}
	policy, err := s.store.RequestPolicy(r.Context())
	if err != nil {
		s.loggerFor(r.Context()).Error("request policy read failed", "error", err)
		return false
	}
	return policy.Allows(sess.EmbyUserID)
}

func (s *Server) handleRequestLookup(w http.ResponseWriter, r *http.Request, sess store.Session) {
	if !s.requestAllowed(r, sess) {
		writeError(w, http.StatusForbidden, "media requests are not enabled for this user")
		return
	}
	term := strings.TrimSpace(r.URL.Query().Get("q"))
	if len(term) < 2 || len(term) > 160 {
		writeError(w, http.StatusBadRequest, "query must be between 2 and 160 characters")
		return
	}

	var movieCandidates []requestCandidate
	var seriesCandidates []requestCandidate
	var wg sync.WaitGroup
	if s.radarr != nil {
		wg.Add(1)
		go func() {
			defer wg.Done()
			movies, err := s.radarr.Lookup(r.Context(), term)
			if err != nil {
				s.loggerFor(r.Context()).Warn("Radarr request lookup failed", "error", err)
				return
			}
			for _, movie := range movies {
				if movie.TMDBID == 0 || len(movieCandidates) >= 5 {
					continue
				}
				movieCandidates = append(movieCandidates, requestCandidate{
					MediaType: "movie", ForeignID: movie.TMDBID, Title: movie.Title,
					Year: movie.Year, Overview: movie.Overview,
					PosterURL: radarrCoverURL(movie.Images, "poster"), AlreadyAdded: movie.ID > 0,
				})
			}
		}()
	}
	if s.sonarr != nil {
		wg.Add(1)
		go func() {
			defer wg.Done()
			series, err := s.sonarr.Lookup(r.Context(), term)
			if err != nil {
				s.loggerFor(r.Context()).Warn("Sonarr request lookup failed", "error", err)
				return
			}
			for _, show := range series {
				if show.TVDBID == 0 || len(seriesCandidates) >= 5 {
					continue
				}
				seriesCandidates = append(seriesCandidates, requestCandidate{
					MediaType: "series", ForeignID: show.TVDBID, Title: show.Title,
					Year: show.Year, Overview: show.Overview,
					PosterURL: sonarrCoverURL(show.Images, "poster"), AlreadyAdded: show.ID > 0,
				})
			}
		}()
	}
	wg.Wait()
	candidates := append(movieCandidates, seriesCandidates...)
	sort.SliceStable(candidates, func(i, j int) bool {
		return requestMatchScore(term, candidates[i].Title) < requestMatchScore(term, candidates[j].Title)
	})
	writeJSON(w, http.StatusOK, requestLookupResponse{Candidates: candidates})
}

func requestMatchScore(term, title string) int {
	term = strings.ToLower(strings.TrimSpace(term))
	title = strings.ToLower(strings.TrimSpace(title))
	switch {
	case title == term:
		return 0
	case strings.HasPrefix(title, term):
		return 1
	case strings.Contains(title, term):
		return 2
	default:
		return 3
	}
}

func radarrCoverURL(images []radarr.Image, kind string) string {
	for _, image := range images {
		if image.CoverType == kind {
			if image.RemoteURL != "" {
				return image.RemoteURL
			}
			return image.URL
		}
	}
	return ""
}

func sonarrCoverURL(images []sonarr.Image, kind string) string {
	for _, image := range images {
		if image.CoverType == kind {
			if image.RemoteURL != "" {
				return image.RemoteURL
			}
			return image.URL
		}
	}
	return ""
}

type requestPayload struct {
	MediaType string `json:"mediaType"`
	ForeignID int    `json:"foreignId"`
}

func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess store.Session) {
	if !s.requestAllowed(r, sess) {
		writeError(w, http.StatusForbidden, "media requests are not enabled for this user")
		return
	}
	var req requestPayload
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}
	if req.ForeignID <= 0 {
		writeError(w, http.StatusBadRequest, "foreignId is required")
		return
	}

	switch req.MediaType {
	case "movie":
		if s.radarr == nil {
			writeError(w, http.StatusServiceUnavailable, "Radarr is not configured")
			return
		}
		movies, err := s.radarr.Lookup(r.Context(), "tmdb:"+strconv.Itoa(req.ForeignID))
		if err != nil {
			s.writeRequestUpstreamError(r.Context(), w, err, "Radarr lookup failed")
			return
		}
		for _, movie := range movies {
			if movie.TMDBID != req.ForeignID {
				continue
			}
			if movie.ID > 0 {
				// Idempotent under a lost response: OkHttp may replay a repeatable POST after
				// a connection reset. If the first request already added it, the retry is the
				// same successful action rather than an error shown to the viewer.
				writeJSON(w, http.StatusOK, map[string]any{"status": "already_added", "title": movie.Title})
				return
			}
			added, err := s.radarr.AddUnmonitored(r.Context(), movie)
			if err != nil {
				s.writeRequestUpstreamError(r.Context(), w, err, "could not add movie to Radarr")
				return
			}
			s.loggerFor(r.Context()).Info("media requested", "user", sess.Username, "type", "movie", "title", added.Title)
			writeJSON(w, http.StatusCreated, map[string]any{"status": "requested", "title": added.Title})
			return
		}
	case "series":
		if s.sonarr == nil {
			writeError(w, http.StatusServiceUnavailable, "Sonarr is not configured")
			return
		}
		series, err := s.sonarr.Lookup(r.Context(), "tvdb:"+strconv.Itoa(req.ForeignID))
		if err != nil {
			s.writeRequestUpstreamError(r.Context(), w, err, "Sonarr lookup failed")
			return
		}
		for _, show := range series {
			if show.TVDBID != req.ForeignID {
				continue
			}
			if show.ID > 0 {
				writeJSON(w, http.StatusOK, map[string]any{"status": "already_added", "title": show.Title})
				return
			}
			added, err := s.sonarr.AddUnmonitored(r.Context(), show)
			if err != nil {
				s.writeRequestUpstreamError(r.Context(), w, err, "could not add series to Sonarr")
				return
			}
			s.loggerFor(r.Context()).Info("media requested", "user", sess.Username, "type", "series", "title", added.Title)
			writeJSON(w, http.StatusCreated, map[string]any{"status": "requested", "title": added.Title})
			return
		}
	default:
		writeError(w, http.StatusBadRequest, `mediaType must be "movie" or "series"`)
		return
	}
	writeError(w, http.StatusNotFound, "title was not found")
}

func (s *Server) writeRequestUpstreamError(
	ctx context.Context, w http.ResponseWriter, err error, message string,
) {
	var radarrErr *radarr.APIError
	var sonarrErr *sonarr.APIError
	if (errors.As(err, &radarrErr) && radarrErr.StatusCode == http.StatusBadRequest) ||
		(errors.As(err, &sonarrErr) && sonarrErr.StatusCode == http.StatusBadRequest) {
		writeError(w, http.StatusConflict, "the title could not be added; it may already exist")
		return
	}
	s.loggerFor(ctx).Error(message, "error", err)
	writeError(w, http.StatusBadGateway, message)
}
