package api

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"syscall"

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

// allowedImageTypes guards the path segment we forward to Emby.
var allowedImageTypes = map[string]string{
	"backdrop": "Backdrop",
	"primary":  "Primary",
	"logo":     "Logo",
	"thumb":    "Thumb",
}

// handleImage proxies artwork.
//
// Going through the gateway means the TV's image URLs carry a gateway token instead of a
// live Emby api_key, and it gives Emby's resized output a cacheable home. Images are
// tag-addressed, so a hit can be cached hard by any layer in front of this.
func (s *Server) handleImage(w http.ResponseWriter, r *http.Request, sess store.Session) {
	itemID := r.PathValue("itemId")
	imageType, ok := allowedImageTypes[strings.ToLower(r.PathValue("imageType"))]
	if !ok || itemID == "" {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	if strings.HasPrefix(itemID, "sonarr:") {
		s.handleSonarrImage(w, r, itemID, imageType)
		return
	}
	if strings.HasPrefix(itemID, "radarr:") {
		s.handleRadarrImage(w, r, itemID, imageType)
		return
	}

	params := url.Values{}
	for _, key := range []string{"tag", "maxWidth", "maxHeight", "quality"} {
		if v := r.URL.Query().Get(key); v != "" {
			params.Set(key, v)
		}
	}
	if writeNotModifiedForTag(w, r, params.Get("tag")) {
		return
	}

	resp, err := s.emby.ImageResponse(r.Context(), credentials(sess), itemID, imageType, params)
	if err != nil {
		s.writeUpstreamError(r.Context(), w, err, "could not load the image")
		return
	}
	defer resp.Body.Close()

	if ct := resp.Header.Get("Content-Type"); ct != "" {
		w.Header().Set("Content-Type", ct)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		w.Header().Set("Content-Length", cl)
	}
	// A tag identifies exact image bytes, so it can be cached indefinitely. Without one,
	// stay conservative.
	if params.Get("tag") != "" {
		w.Header().Set("Cache-Control", "private, max-age=31536000, immutable")
		w.Header().Set("ETag", imageETag(params.Get("tag")))
	} else {
		w.Header().Set("Cache-Control", "private, max-age=3600")
	}

	w.WriteHeader(http.StatusOK)
	copyImage(w, r, resp.Body, s.loggerFor(r.Context()),
		"source", "emby", "item_id", itemID, "image_type", imageType)
}

func (s *Server) handleRadarrImage(w http.ResponseWriter, r *http.Request, itemID, imageType string) {
	if s.radarr == nil {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	parts := strings.Split(itemID, ":")
	if len(parts) != 2 {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	movieID, err := strconv.Atoi(parts[1])
	if err != nil || movieID <= 0 {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	coverType := map[string]string{"Primary": "poster", "Backdrop": "fanart"}[imageType]
	if coverType == "" {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	resp, err := s.radarr.MediaCover(r.Context(), movieID, coverType)
	if err != nil {
		var apiErr *radarr.APIError
		if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
			writeError(w, http.StatusNotFound, "image not found")
			return
		}
		s.log.Warn("radarr image failed", "movie_id", movieID, "type", coverType, "error", err)
		writeError(w, http.StatusBadGateway, "could not load the image")
		return
	}
	defer resp.Body.Close()
	if ct := resp.Header.Get("Content-Type"); ct != "" {
		w.Header().Set("Content-Type", ct)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		w.Header().Set("Content-Length", cl)
	}
	w.Header().Set("Cache-Control", "private, max-age=3600")
	w.WriteHeader(http.StatusOK)
	copyImage(w, r, resp.Body, s.loggerFor(r.Context()),
		"source", "radarr", "item_id", itemID, "image_type", imageType)
}

func (s *Server) handleSonarrImage(w http.ResponseWriter, r *http.Request, itemID, imageType string) {
	if s.sonarr == nil {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	parts := strings.Split(itemID, ":")
	if len(parts) != 3 {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	seriesID, err := strconv.Atoi(parts[1])
	if err != nil || seriesID <= 0 {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	coverType := map[string]string{"Primary": "poster", "Backdrop": "fanart"}[imageType]
	if coverType == "" {
		writeError(w, http.StatusNotFound, "unknown image")
		return
	}
	resp, err := s.sonarr.MediaCover(r.Context(), seriesID, coverType)
	if err != nil {
		var apiErr *sonarr.APIError
		if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
			writeError(w, http.StatusNotFound, "image not found")
			return
		}
		s.log.Warn("sonarr image failed", "series_id", seriesID, "type", coverType, "error", err)
		writeError(w, http.StatusBadGateway, "could not load the image")
		return
	}
	defer resp.Body.Close()
	if ct := resp.Header.Get("Content-Type"); ct != "" {
		w.Header().Set("Content-Type", ct)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		w.Header().Set("Content-Length", cl)
	}
	w.Header().Set("Cache-Control", "private, max-age=3600")
	w.WriteHeader(http.StatusOK)
	copyImage(w, r, resp.Body, s.loggerFor(r.Context()),
		"source", "sonarr", "item_id", itemID, "image_type", imageType)
}

func copyImage(
	w io.Writer,
	r *http.Request,
	body io.Reader,
	log *slog.Logger,
	attributes ...any,
) {
	written, err := io.Copy(w, body)
	if err == nil {
		return
	}
	if expectedClientDisconnect(r, err) {
		// Image loaders cancel work aggressively as cards leave the viewport. That is a
		// successful resource-saving decision by the TV, not an unhealthy gateway.
		return
	}
	fields := append([]any{"bytes_written", written, "error", err}, attributes...)
	log.Warn("image stream interrupted", fields...)
}

func expectedClientDisconnect(r *http.Request, err error) bool {
	if r.Context().Err() != nil ||
		errors.Is(err, context.Canceled) ||
		errors.Is(err, net.ErrClosed) ||
		errors.Is(err, syscall.EPIPE) ||
		errors.Is(err, syscall.ECONNRESET) {
		return true
	}
	message := strings.ToLower(err.Error())
	for _, fragment := range []string{
		"broken pipe",
		"connection reset by peer",
		"client disconnected",
		"request canceled",
		"request cancelled",
		"stream closed",
	} {
		if strings.Contains(message, fragment) {
			return true
		}
	}
	return false
}

func writeNotModifiedForTag(w http.ResponseWriter, r *http.Request, tag string) bool {
	if tag == "" {
		return false
	}
	etag := imageETag(tag)
	w.Header().Set("ETag", etag)
	w.Header().Set("Cache-Control", "private, max-age=31536000, immutable")
	for _, candidate := range strings.Split(r.Header.Get("If-None-Match"), ",") {
		if strings.TrimSpace(candidate) == etag || strings.TrimSpace(candidate) == "*" {
			w.WriteHeader(http.StatusNotModified)
			return true
		}
	}
	return false
}

func imageETag(tag string) string {
	// Emby image tags are normally hex, but quote defensively for a valid HTTP entity tag.
	return `"` + strings.NewReplacer(`\`, "", `"`, "").Replace(tag) + `"`
}
