package handlers

import (
	"errors"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/tdvorak/seen/backend/internal/domain"
	"github.com/tdvorak/seen/backend/internal/services/auth"
	"github.com/tdvorak/seen/backend/internal/services/catalog"
	"github.com/tdvorak/seen/backend/pkg/httpx"
)

type CatalogHandler struct {
	service     *catalog.Service
	authService *auth.Service
}

func NewCatalogHandler(service *catalog.Service, authService *auth.Service) *CatalogHandler {
	return &CatalogHandler{service: service, authService: authService}
}

type watchLaterAddRequest struct {
	MediaID int `json:"mediaId"`
}

type progressUpdateRequest struct {
	MediaID         int `json:"mediaId"`
	SeasonNumber    int `json:"seasonNumber"`
	EpisodeNumber   int `json:"episodeNumber"`
	ProgressPercent int `json:"progressPercent"`
}

func (h *CatalogHandler) Dashboard(c *gin.Context) {
	httpx.JSON(c, http.StatusOK, h.service.Dashboard())
}

func (h *CatalogHandler) ContinueWatching(c *gin.Context) {
	user, ok := h.resolveUser(c)
	if !ok {
		return
	}

	items, err := h.service.ContinueWatching(user.ID)
	if err != nil {
		switch {
		case errors.Is(err, catalog.ErrInvalidInput):
			httpx.JSONError(c, http.StatusBadRequest, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to load continue watching")
		}
		return
	}

	httpx.JSON(c, http.StatusOK, items)
}

func (h *CatalogHandler) Discover(c *gin.Context) {
	page := parseInt(c.Query("page"), 1)
	pageSize := parseInt(c.Query("pageSize"), 6)

	httpx.JSON(c, http.StatusOK, h.service.Discover(catalog.DiscoverParams{
		Page:      page,
		PageSize:  pageSize,
		Query:     c.Query("query"),
		Genre:     c.Query("genre"),
		MediaType: c.Query("mediaType"),
	}))
}

func (h *CatalogHandler) Games(c *gin.Context) {
	page := parseInt(c.Query("page"), 1)
	pageSize := parseInt(c.Query("pageSize"), 6)

	httpx.JSON(c, http.StatusOK, h.service.Discover(catalog.DiscoverParams{
		Page:      page,
		PageSize:  pageSize,
		Query:     c.Query("query"),
		Genre:     c.Query("genre"),
		MediaType: string(catalog.MediaTypeGame),
	}))
}

func (h *CatalogHandler) Search(c *gin.Context) {
	httpx.JSON(c, http.StatusOK, h.service.Search(catalog.SearchParams{
		Query:     c.Query("query"),
		Genre:     c.Query("genre"),
		MediaType: c.Query("mediaType"),
	}))
}

func (h *CatalogHandler) WatchLater(c *gin.Context) {
	user, ok := h.resolveUser(c)
	if !ok {
		return
	}

	items, err := h.service.WatchLater(user.ID)
	if err != nil {
		switch {
		case errors.Is(err, catalog.ErrInvalidInput):
			httpx.JSONError(c, http.StatusBadRequest, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to load watch later")
		}
		return
	}

	httpx.JSON(c, http.StatusOK, items)
}

func (h *CatalogHandler) AddWatchLater(c *gin.Context) {
	user, ok := h.resolveUser(c)
	if !ok {
		return
	}

	var request watchLaterAddRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		httpx.JSONError(c, http.StatusBadRequest, "invalid request body")
		return
	}

	items, err := h.service.AddWatchLater(user.ID, request.MediaID)
	if err != nil {
		switch {
		case errors.Is(err, catalog.ErrInvalidInput):
			httpx.JSONError(c, http.StatusBadRequest, err.Error())
		case errors.Is(err, catalog.ErrMediaNotFound):
			httpx.JSONError(c, http.StatusNotFound, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to add watch later item")
		}
		return
	}

	httpx.JSON(c, http.StatusOK, items)
}

func (h *CatalogHandler) RemoveWatchLater(c *gin.Context) {
	user, ok := h.resolveUser(c)
	if !ok {
		return
	}

	mediaID := parseInt(c.Param("mediaId"), 0)
	items, err := h.service.RemoveWatchLater(user.ID, mediaID)
	if err != nil {
		switch {
		case errors.Is(err, catalog.ErrInvalidInput):
			httpx.JSONError(c, http.StatusBadRequest, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to remove watch later item")
		}
		return
	}

	httpx.JSON(c, http.StatusOK, items)
}

func (h *CatalogHandler) UpdateProgress(c *gin.Context) {
	user, ok := h.resolveUser(c)
	if !ok {
		return
	}

	var request progressUpdateRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		httpx.JSONError(c, http.StatusBadRequest, "invalid request body")
		return
	}

	items, err := h.service.UpdateProgress(user.ID, catalog.ProgressUpdateInput{
		MediaID:         request.MediaID,
		SeasonNumber:    request.SeasonNumber,
		EpisodeNumber:   request.EpisodeNumber,
		ProgressPercent: request.ProgressPercent,
	})
	if err != nil {
		switch {
		case errors.Is(err, catalog.ErrInvalidInput):
			httpx.JSONError(c, http.StatusBadRequest, err.Error())
		case errors.Is(err, catalog.ErrMediaNotFound):
			httpx.JSONError(c, http.StatusNotFound, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to update progress")
		}
		return
	}

	httpx.JSON(c, http.StatusOK, items)
}

func (h *CatalogHandler) resolveUser(c *gin.Context) (*domain.User, bool) {
	accessToken, ok := bearerToken(c.GetHeader("Authorization"))
	if !ok {
		httpx.JSONError(c, http.StatusUnauthorized, "missing bearer token")
		return nil, false
	}

	user, err := h.authService.UserFromAccessToken(c.Request.Context(), accessToken)
	if err != nil {
		switch {
		case errors.Is(err, auth.ErrInvalidToken):
			httpx.JSONError(c, http.StatusUnauthorized, err.Error())
		default:
			httpx.JSONError(c, http.StatusInternalServerError, "failed to resolve user")
		}
		return nil, false
	}

	return user, true
}

func parseInt(raw string, fallback int) int {
	if raw == "" {
		return fallback
	}

	parsed, err := strconv.Atoi(raw)
	if err != nil {
		return fallback
	}

	return parsed
}
