package anime

import (
	"context"
	"errors"
	"fmt"
	"mal/integrations/jikan"
	"mal/internal/domain"
	"mal/internal/observability"
	"mal/internal/server"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
)

const (
	animeSectionTimeout = 12 * time.Second
	watchOrderTimeout   = 15 * time.Second
	audioLookupTimeout  = 8 * time.Second
	episodeCountTimeout = 4 * time.Second
)

func listedEpisodeCount(episodes []domain.EpisodeData) int {
	count := 0
	for _, episode := range episodes {
		if episode.MalID <= 0 || episode.IsRecap {
			continue
		}
		count++
	}
	return count
}

func releasedEpisodeCount(anime domain.Anime, now time.Time) int {
	if !anime.Airing || anime.Aired.From == "" {
		return 0
	}

	firstAired, err := time.Parse(time.RFC3339, anime.Aired.From)
	if err != nil || now.Before(firstAired) {
		return 0
	}

	count := int(now.Sub(firstAired)/(7*24*time.Hour)) + 1
	if anime.Episodes > 0 && count > anime.Episodes {
		return anime.Episodes
	}
	return count
}

func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime, now time.Time) int {
	if h.svc != nil && anime.Airing {
		episodeCtx, cancel := context.WithTimeout(ctx, episodeCountTimeout)
		defer cancel()

		episodes, err := h.svc.GetAllEpisodes(episodeCtx, anime.MalID)
		if err == nil {
			if count := listedEpisodeCount(episodes); count > 0 {
				return count
			}
		} else {
			observability.Warn(
				"anime_episode_count_fetch_failed",
				"anime",
				"",
				map[string]any{
					"anime_id": anime.MalID,
				},
				err,
			)
		}
	}

	return releasedEpisodeCount(anime, now)
}

func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
	hasKnownSub := false
	for _, episode := range episodes {
		if episode.HasDub {
			return "Dub available"
		}
		if episode.HasSub || episode.SubOnly {
			hasKnownSub = true
		}
	}
	if hasKnownSub {
		return "Subtitled only"
	}
	return ""
}

func (h *AnimeHandler) animeAudioAvailability(ctx context.Context, anime domain.Anime) string {
	if h.episodeSvc == nil {
		return ""
	}

	audioCtx, cancel := context.WithTimeout(ctx, audioLookupTimeout)
	defer cancel()

	episodeList, err := h.episodeSvc.GetCanonicalEpisodes(audioCtx, anime, true)
	if err != nil {
		observability.Warn(
			"anime_audio_availability_fetch_failed",
			"anime",
			"",
			map[string]any{
				"anime_id": anime.MalID,
			},
			err,
		)
		return ""
	}
	if episodeList.Source != "AllAnime" {
		return ""
	}

	return animeAudioAvailabilityLabel(episodeList.Episodes)
}

func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil || id <= 0 {
		server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id")
		return
	}

	section := c.Query("section")
	if section != "" && c.GetHeader("HX-Request") == "true" {
		h.handleAnimeDetailsSection(c, id, section)
		return
	}

	anime, err := h.svc.GetAnimeByID(c.Request.Context(), id)
	if err != nil {
		c.Status(http.StatusNotFound)
		return
	}

	h.svc.WarmDetailSections(id)

	user := server.CurrentUser(c)
	status := ""
	var watchlistIDs []int64
	ep := 0
	var cwSeconds float64
	if user != nil {
		entry, err := h.watchlistSvc.GetWatchListEntry(c.Request.Context(), user.ID, int64(id))
		if err == nil {
			status = entry.Status
			watchlistIDs = []int64{entry.AnimeID}
		}

		cwEntry, err := h.watchlistSvc.GetContinueWatchingEntry(c.Request.Context(), user.ID, int64(id))
		if err == nil && cwEntry.CurrentEpisode.Valid {
			ep = int(cwEntry.CurrentEpisode.Int64)
			cwSeconds = cwEntry.CurrentTimeSeconds
		}
	}

	audioAvailability := h.animeAudioAvailability(c.Request.Context(), anime)
	episodesCount := h.animeEpisodeCount(c.Request.Context(), anime, time.Now())

	c.HTML(http.StatusOK, "anime.gohtml", gin.H{
		"Anime":                anime,
		"AudioAvailability":    audioAvailability,
		"CurrentPath":          fmt.Sprintf("/anime/%d", id),
		"User":                 user,
		"Status":               status,
		"WatchlistIDs":         watchlistIDs,
		"ContinueWatchingEp":   ep,
		"ContinueWatchingTime": cwSeconds,
		"EpisodesCount":        episodesCount,
	})
}

func (h *AnimeHandler) handleAnimeDetailsSection(c *gin.Context, id int, section string) {
	sectionCtx, cancel := context.WithTimeout(c.Request.Context(), animeSectionTimeout)
	defer cancel()

	data, tplName, err := h.loadAnimeDetailsSection(sectionCtx, id, section)
	if err != nil {
		if errors.Is(err, context.Canceled) {
			return
		}
		observability.Warn(
			"anime_section_fetch_failed",
			"anime",
			"",
			map[string]any{
				"section":  section,
				"anime_id": id,
			},
			err,
		)
		if section == "recommendations" {
			c.HTML(http.StatusOK, "anime.gohtml", gin.H{
				"_fragment": "anime_recommendations_loading",
				"AnimeID":   id,
			})
			return
		}
		c.Status(http.StatusNoContent)
		return
	}

	c.HTML(http.StatusOK, "anime.gohtml", gin.H{
		"_fragment": tplName,
		"Items":     data,
	})
}

func (h *AnimeHandler) loadAnimeDetailsSection(ctx context.Context, id int, section string) (any, string, error) {
	switch section {
	case "characters":
		data, err := h.svc.GetCharacters(ctx, id)
		return data, "anime_characters", err
	case "recommendations":
		data, err := h.svc.GetRecommendations(ctx, id)
		return data, "anime_recommendations", err
	case "statistics":
		data, err := h.svc.GetStatistics(ctx, id)
		return data, "anime_statistics", err
	case "themes":
		data, err := h.svc.GetThemes(ctx, id)
		return data, "anime_themes", err
	default:
		return nil, "", nil
	}
}

func (h *AnimeHandler) HandleHTMLWatchOrder(c *gin.Context) {
	id, err := strconv.Atoi(c.Query("animeId"))
	if err != nil || id <= 0 {
		server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id")
		return
	}

	userID := server.CurrentUserID(c)
	mode := jikan.NormalizeWatchOrderMode(c.Query("mode"))

	relationsCtx, cancel := context.WithTimeout(c.Request.Context(), watchOrderTimeout)
	defer cancel()

	relations, err := h.svc.GetRelations(relationsCtx, id, mode)
	if err != nil {
		observability.Warn(
			"relations_fetch_failed",
			"anime",
			"",
			map[string]any{
				"anime_id": id,
			},
			err,
		)
		c.HTML(http.StatusOK, "anime.gohtml", gin.H{
			"_fragment": "watch_order_loading",
			"AnimeID":   id,
			"Mode":      string(mode),
		})
		return
	}

	relationAnimeIDs := make([]int64, 0, len(relations))
	for _, relation := range relations {
		if relation.Anime.MalID > 0 {
			relationAnimeIDs = append(relationAnimeIDs, int64(relation.Anime.MalID))
		}
	}
	watchlistMap := h.watchlistMapForIDs(c.Request.Context(), userID, relationAnimeIDs)

	c.HTML(http.StatusOK, "anime.gohtml", gin.H{
		"_fragment":    "watch_order",
		"Relations":    relations,
		"AnimeID":      id,
		"Mode":         string(mode),
		"WatchlistMap": watchlistMap,
	})
}
