package api

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/ponzischeme89/memby/server/internal/radarr"
	"github.com/ponzischeme89/memby/server/internal/recommend"
)

const radarrCalendarCachePrefix = "radarr:calendar:v3:"
const radarrScheduleDays = 5
const radarrTheatricalDelayDays = 30

type radarrRelease struct {
	at        time.Time
	estimated bool
}

type radarrScheduleItem struct {
	ID                    string            `json:"Id"`
	Name                  string            `json:"Name"`
	Type                  string            `json:"Type"`
	Overview              string            `json:"Overview,omitempty"`
	ProductionYear        int               `json:"ProductionYear,omitempty"`
	RunTimeTicks          int64             `json:"RunTimeTicks,omitempty"`
	Genres                []string          `json:"Genres"`
	ImageTags             map[string]string `json:"ImageTags"`
	BackdropImageTags     []string          `json:"BackdropImageTags"`
	MembySource           string            `json:"MembySource"`
	MembyAirsAt           string            `json:"MembyAirsAt"`
	MembyAirDayLabel      string            `json:"MembyAirDayLabel"`
	MembyAirLabel         string            `json:"MembyAirLabel"`
	MembyAvailability     string            `json:"MembyAvailability"`
	MembyAvailabilityText string            `json:"MembyAvailabilityText"`
	// Radarr's lifecycle for the title — announced, in cinemas, released — which is a
	// different question from whether the household's copy has downloaded yet.
	MembyLifecycle     string `json:"MembyLifecycle,omitempty"`
	MembyLifecycleText string `json:"MembyLifecycleText,omitempty"`
	MembyPlayable      bool   `json:"MembyPlayable"`
}

func (s *Server) radarrUpcomingMoviesRow(ctx context.Context) (*recommend.Row, error) {
	if s.radarr == nil {
		return nil, nil
	}
	location := s.cfg.RadarrLocation
	if location == nil {
		location = time.Local
	}
	now := time.Now().In(location)
	dayStart := localDayStart(now, location)
	cacheKey := radarrCalendarCachePrefix + dayStart.Format("2006-01-02")
	if row := s.cachedRadarrRow(ctx, cacheKey); row != nil {
		return row, nil
	}

	s.radarrMu.Lock()
	defer s.radarrMu.Unlock()
	if row := s.cachedRadarrRow(ctx, cacheKey); row != nil {
		return row, nil
	}

	// A cinema release can stand in for an unknown digital date at cinema + 30 days,
	// so include the preceding 30 days in the Radarr query. buildRadarrRow applies the
	// actual five-day effective-release window after the response arrives.
	movies, err := s.radarr.Calendar(
		ctx,
		dayStart.AddDate(0, 0, -radarrTheatricalDelayDays),
		dayStart.AddDate(0, 0, radarrScheduleDays),
	)
	if err != nil {
		return nil, err
	}
	row, err := buildRadarrRow(movies, now, location)
	if err != nil {
		return nil, err
	}
	if body, marshalErr := json.Marshal(row); marshalErr == nil {
		if cacheErr := s.cache.Set(ctx, cacheKey, body, s.cfg.RadarrTTL); cacheErr != nil {
			s.loggerFor(ctx).Warn("radarr calendar cache write failed", "error", cacheErr)
		}
	}
	return row, nil
}

func (s *Server) cachedRadarrRow(ctx context.Context, key string) *recommend.Row {
	raw, err := s.cache.Get(ctx, key)
	if err != nil {
		return nil
	}
	var row recommend.Row
	if json.Unmarshal(raw, &row) != nil {
		return nil
	}
	return &row
}

func buildRadarrRow(movies []radarr.Movie, now time.Time, location *time.Location) (*recommend.Row, error) {
	sort.SliceStable(movies, func(i, j int) bool {
		left, leftOK := effectiveRadarrRelease(movies[i])
		right, rightOK := effectiveRadarrRelease(movies[j])
		if !leftOK {
			return false
		}
		if !rightOK {
			return true
		}
		return left.at.Before(right.at)
	})

	items := make([]json.RawMessage, 0, len(movies))
	dayStart := localDayStart(now, location)
	windowEnd := dayStart.AddDate(0, 0, radarrScheduleDays)
	for _, movie := range movies {
		release, ok := effectiveRadarrRelease(movie)
		if !ok {
			continue
		}
		localRelease := release.at.In(location)
		if localRelease.Before(dayStart) || !localRelease.Before(windowEnd) {
			continue
		}
		raw, err := json.Marshal(toRadarrScheduleItem(movie, release, now, location))
		if err != nil {
			return nil, err
		}
		items = append(items, raw)
	}
	return &recommend.Row{
		ID:    "radarr-upcoming-movies",
		Title: "Upcoming Movie releases",
		Kind:  "movie-schedule",
		Items: items,
	}, nil
}

// effectiveRadarrRelease prefers Radarr's actual digital date. Cinema + 30 days is
// only a fallback when Radarr has no digital date at all. In particular, an old movie
// with an old digital date cannot appear because of a newer cinema/re-release date.
func effectiveRadarrRelease(movie radarr.Movie) (radarrRelease, bool) {
	if movie.DigitalRelease != nil {
		return radarrRelease{at: *movie.DigitalRelease}, true
	}
	if movie.InCinemas != nil {
		return radarrRelease{
			at:        movie.InCinemas.AddDate(0, 0, radarrTheatricalDelayDays),
			estimated: true,
		}, true
	}
	return radarrRelease{}, false
}

func toRadarrScheduleItem(movie radarr.Movie, release radarrRelease, now time.Time, location *time.Location) radarrScheduleItem {
	availabilityText := "Upcoming digital release"
	if release.estimated {
		availabilityText = "Estimated digital release"
	}
	item := radarrScheduleItem{
		ID:                    fmt.Sprintf("radarr:%d", movie.ID),
		Name:                  movie.Title,
		Type:                  "MembyRadarrMovie",
		Overview:              movie.Overview,
		ProductionYear:        movie.Year,
		RunTimeTicks:          int64(movie.Runtime) * 600_000_000,
		Genres:                nonNilStrings(movie.Genres),
		ImageTags:             map[string]string{},
		BackdropImageTags:     []string{},
		MembySource:           "radarr",
		MembyAvailability:     "upcoming",
		MembyAvailabilityText: availabilityText,
		MembyPlayable:         false,
	}
	lifecycle := movieLifecycleTag(movie.Status)
	item.MembyLifecycle, item.MembyLifecycleText = lifecycle.Status, lifecycle.Label
	if hasRadarrCover(movie.Images, "poster") {
		item.ImageTags["Primary"] = "radarr"
	}
	if hasRadarrCover(movie.Images, "fanart") {
		item.BackdropImageTags = []string{"radarr"}
	}
	localRelease := release.at.In(location)
	item.MembyAirsAt = localRelease.Format(time.RFC3339)
	item.MembyAirDayLabel = scheduleAirDayLabel(localRelease, now, location)
	item.MembyAirLabel = digitalReleaseLabel(localRelease, now, location, release.estimated)
	switch {
	case movie.HasFile:
		item.MembyAvailability = "available"
		item.MembyAvailabilityText = "Downloaded"
		if movie.MovieFile != nil && movie.MovieFile.DateAdded != nil {
			item.MembyAvailabilityText = "Added at " +
				movie.MovieFile.DateAdded.In(location).Format("3:04 PM")
		}
	case !movie.Monitored:
		item.MembyAvailability = "unmonitored"
		item.MembyAvailabilityText = "Not monitored"
	case release.at.Before(now):
		item.MembyAvailability = "awaiting"
		item.MembyAvailabilityText = "Awaiting download"
	}
	return item
}

func digitalReleaseLabel(release, now time.Time, location *time.Location, estimated bool) string {
	release = release.In(location)
	now = now.In(location)
	today := localDayStart(now, location)
	releaseDay := localDayStart(release, location)
	prefix := "Digital release "
	if estimated {
		prefix = "Estimated digital release "
	}
	switch {
	case releaseDay.Equal(today):
		return prefix + "today"
	case releaseDay.Equal(today.AddDate(0, 0, 1)):
		return prefix + "tomorrow"
	default:
		return prefix + release.Format("Monday")
	}
}

func hasRadarrCover(images []radarr.Image, coverType string) bool {
	for _, image := range images {
		if strings.EqualFold(image.CoverType, coverType) {
			return true
		}
	}
	return false
}
