package server

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// RadarrConfig is populated from CC_RADARR_* env in main.go. Empty BaseURL or
// Token disables the /api/radarr routes (handlers 503).
type RadarrConfig struct {
	BaseURL     string // e.g. https://host/user/radarr
	Token       string // Radarr API key (X-Api-Key)
	RootFolder  string // where movies are stored, e.g. /media/.../media/movies
	ProfileName string // quality profile to add with, e.g. "Any"
}

func (s *Server) radarrReady() bool {
	return s.radarr != nil && s.radarr.BaseURL != "" && s.radarr.Token != ""
}

func (s *Server) radarrRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(s.radarr.BaseURL, "/")+path, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Api-Key", s.radarr.Token)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	return (&http.Client{Timeout: 25 * time.Second}).Do(req)
}

type radarrSearchResult struct {
	Title    string `json:"title"`
	Year     int    `json:"year"`
	TmdbID   int    `json:"tmdb_id"`
	Overview string `json:"overview"`
	Poster   string `json:"poster"`
	Added    bool   `json:"added"` // already in the Radarr library
}

// handleRadarrSearch proxies Radarr's movie/lookup so the operator can search
// from the Command Center without opening Radarr.
func (s *Server) handleRadarrSearch(w http.ResponseWriter, r *http.Request) {
	if !s.radarrReady() {
		http.Error(w, "radarr not configured", http.StatusServiceUnavailable)
		return
	}
	term := strings.TrimSpace(r.URL.Query().Get("term"))
	if term == "" {
		http.Error(w, "term required", http.StatusBadRequest)
		return
	}
	resp, err := s.radarrRequest(r.Context(), http.MethodGet, "/api/v3/movie/lookup?term="+url.QueryEscape(term), nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		http.Error(w, fmt.Sprintf("radarr lookup: status %d", resp.StatusCode), http.StatusBadGateway)
		return
	}
	var raw []struct {
		Title    string `json:"title"`
		Year     int    `json:"year"`
		TmdbID   int    `json:"tmdbId"`
		Overview string `json:"overview"`
		ID       int    `json:"id"` // >0 means already added
		Images   []struct {
			CoverType string `json:"coverType"`
			RemoteURL string `json:"remoteUrl"`
		} `json:"images"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 8<<20)).Decode(&raw); err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	out := make([]radarrSearchResult, 0, len(raw))
	for i, m := range raw {
		if i >= 20 || m.TmdbID == 0 {
			continue
		}
		poster := ""
		for _, img := range m.Images {
			if img.CoverType == "poster" {
				poster = img.RemoteURL
				break
			}
		}
		out = append(out, radarrSearchResult{
			Title: m.Title, Year: m.Year, TmdbID: m.TmdbID,
			Overview: m.Overview, Poster: poster, Added: m.ID > 0,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"results": out})
}

type radarrAddReq struct {
	TmdbID int `json:"tmdb_id"`
}

// handleRadarrAdd adds a movie by tmdbId with the configured profile + root
// folder, monitored, and triggers an immediate search.
func (s *Server) handleRadarrAdd(w http.ResponseWriter, r *http.Request) {
	if !s.radarrReady() {
		http.Error(w, "radarr not configured", http.StatusServiceUnavailable)
		return
	}
	var req radarrAddReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 1024)).Decode(&req); err != nil || req.TmdbID == 0 {
		http.Error(w, "tmdb_id required", http.StatusBadRequest)
		return
	}

	// 0. Already in the library? Radarr's /movie/lookup/tmdb returns id=null
	// even for added movies, so we query the library directly by tmdbId.
	if mid, ok := s.radarrLibraryMovieID(r.Context(), req.TmdbID); ok {
		writeJSON(w, http.StatusOK, map[string]any{"ok": true, "already_added": true, "movie_id": mid})
		return
	}

	// 1. Full movie object from a tmdb lookup (for the add payload).
	lr, err := s.radarrRequest(r.Context(), http.MethodGet, "/api/v3/movie/lookup/tmdb?tmdbId="+strconv.Itoa(req.TmdbID), nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer lr.Body.Close()
	if lr.StatusCode != http.StatusOK {
		http.Error(w, fmt.Sprintf("radarr tmdb lookup: status %d", lr.StatusCode), http.StatusInternalServerError)
		return
	}
	var movie map[string]any
	if err := json.NewDecoder(io.LimitReader(lr.Body, 8<<20)).Decode(&movie); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 2. Resolve the quality profile id by name (fallback: first profile).
	qpID, err := s.radarrQualityProfileID(r.Context())
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 3. Decorate the lookup object with the add fields and POST it.
	movie["qualityProfileId"] = qpID
	movie["rootFolderPath"] = s.radarr.RootFolder
	movie["monitored"] = true
	movie["minimumAvailability"] = "released"
	// Add the movie but do NOT auto-grab — the operator picks the release via
	// the interactive picker (/api/radarr/releases + /grab).
	movie["addOptions"] = map[string]any{"searchForMovie": false}

	payload, _ := json.Marshal(movie)
	ar, err := s.radarrRequest(r.Context(), http.MethodPost, "/api/v3/movie", bytes.NewReader(payload))
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer ar.Body.Close()
	if ar.StatusCode/100 != 2 {
		b, _ := io.ReadAll(io.LimitReader(ar.Body, 1024))
		// Race / stale check: if Radarr says it's already added, recover the
		// library id and treat it as success so the picker can proceed.
		if ar.StatusCode == http.StatusBadRequest && bytes.Contains(bytes.ToLower(b), []byte("already")) {
			if mid, ok := s.radarrLibraryMovieID(r.Context(), req.TmdbID); ok {
				writeJSON(w, http.StatusOK, map[string]any{"ok": true, "already_added": true, "movie_id": mid})
				return
			}
		}
		http.Error(w, fmt.Sprintf("radarr add: status %d: %s", ar.StatusCode, b), http.StatusInternalServerError)
		return
	}
	var created struct {
		ID int `json:"id"`
	}
	_ = json.NewDecoder(io.LimitReader(ar.Body, 8<<20)).Decode(&created)
	s.logger.Info().Int("tmdb_id", req.TmdbID).Int("movie_id", created.ID).Msg("radarr: movie added (no auto-search)")
	writeJSON(w, http.StatusAccepted, map[string]any{"ok": true, "title": movie["title"], "movie_id": created.ID})
}

type radarrRelease struct {
	GUID      string `json:"guid"`
	IndexerID int    `json:"indexer_id"`
	Title     string `json:"title"`
	Size      int64  `json:"size"`
	Quality   string `json:"quality"`
	Seeders   int    `json:"seeders"`
	Indexer   string `json:"indexer"`
	Freeleech bool   `json:"freeleech"`
	Rejected  bool   `json:"rejected"`
	InfoURL   string `json:"info_url"` // tracker details page (screenshots/description)
}

// handleRadarrReleases runs Radarr's interactive search for a movie and returns
// the candidate releases so the operator can pick one (size/quality/freeleech).
func (s *Server) handleRadarrReleases(w http.ResponseWriter, r *http.Request) {
	if !s.radarrReady() {
		http.Error(w, "radarr not configured", http.StatusServiceUnavailable)
		return
	}
	movieID := strings.TrimSpace(r.URL.Query().Get("movie_id"))
	if movieID == "" {
		http.Error(w, "movie_id required", http.StatusBadRequest)
		return
	}
	// Interactive search can take a while (live indexer query).
	ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
	defer cancel()
	resp, err := s.radarrRequest(ctx, http.MethodGet, "/api/v3/release?movieId="+url.QueryEscape(movieID), nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		http.Error(w, fmt.Sprintf("radarr release search: status %d", resp.StatusCode), http.StatusBadGateway)
		return
	}
	var raw []struct {
		GUID         string   `json:"guid"`
		IndexerID    int      `json:"indexerId"`
		Title        string   `json:"title"`
		Size         int64    `json:"size"`
		Seeders      int      `json:"seeders"`
		Indexer      string   `json:"indexer"`
		Rejected     bool     `json:"rejected"`
		InfoURL      string   `json:"infoUrl"`
		IndexerFlags []string `json:"indexerFlags"`
		Quality      struct {
			Quality struct {
				Name string `json:"name"`
			} `json:"quality"`
		} `json:"quality"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 16<<20)).Decode(&raw); err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	out := make([]radarrRelease, 0, len(raw))
	for _, rel := range raw {
		fl := false
		for _, f := range rel.IndexerFlags {
			if strings.Contains(strings.ToLower(f), "freeleech") {
				fl = true
				break
			}
		}
		out = append(out, radarrRelease{
			GUID: rel.GUID, IndexerID: rel.IndexerID, Title: rel.Title,
			Size: rel.Size, Quality: rel.Quality.Quality.Name, Seeders: rel.Seeders,
			Indexer: rel.Indexer, Freeleech: fl, Rejected: rel.Rejected,
			InfoURL: rel.InfoURL,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"releases": out})
}

type radarrGrabReq struct {
	GUID      string `json:"guid"`
	IndexerID int    `json:"indexer_id"`
}

// handleRadarrGrab tells Radarr to grab a specific release the operator picked.
func (s *Server) handleRadarrGrab(w http.ResponseWriter, r *http.Request) {
	if !s.radarrReady() {
		http.Error(w, "radarr not configured", http.StatusServiceUnavailable)
		return
	}
	var req radarrGrabReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&req); err != nil || req.GUID == "" {
		http.Error(w, "guid + indexer_id required", http.StatusBadRequest)
		return
	}
	payload, _ := json.Marshal(map[string]any{"guid": req.GUID, "indexerId": req.IndexerID})
	resp, err := s.radarrRequest(r.Context(), http.MethodPost, "/api/v3/release", bytes.NewReader(payload))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode/100 != 2 {
		b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		http.Error(w, fmt.Sprintf("radarr grab: status %d: %s", resp.StatusCode, b), http.StatusBadGateway)
		return
	}
	s.logger.Info().Str("guid", req.GUID).Msg("radarr: release grabbed")
	writeJSON(w, http.StatusAccepted, map[string]any{"ok": true})
}

func (s *Server) radarrQualityProfileID(ctx context.Context) (int, error) {
	resp, err := s.radarrRequest(ctx, http.MethodGet, "/api/v3/qualityprofile", nil)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	var profiles []struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&profiles); err != nil {
		return 0, err
	}
	if len(profiles) == 0 {
		return 0, fmt.Errorf("radarr: no quality profiles")
	}
	for _, p := range profiles {
		if strings.EqualFold(p.Name, s.radarr.ProfileName) {
			return p.ID, nil
		}
	}
	return profiles[0].ID, nil // fallback
}

// radarrLibraryMovieID returns the library movie id for a tmdbId if the movie
// is already added (reliable, unlike the lookup/tmdb 'id' field).
func (s *Server) radarrLibraryMovieID(ctx context.Context, tmdbID int) (int, bool) {
	resp, err := s.radarrRequest(ctx, http.MethodGet, "/api/v3/movie?tmdbId="+strconv.Itoa(tmdbID), nil)
	if err != nil {
		return 0, false
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return 0, false
	}
	var movies []struct {
		ID int `json:"id"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 8<<20)).Decode(&movies); err != nil {
		return 0, false
	}
	if len(movies) > 0 && movies[0].ID > 0 {
		return movies[0].ID, true
	}
	return 0, false
}
