package jikan

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/url"
	"strconv"
	"time"
)

type ScheduleResult struct {
	Animes      []Anime
	HasNextPage bool // whether more pages available
}

// GetSeasonsNow returns currently airing anime for the current season.
func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, error) {
	return c.getSeasonList(ctx, page, "now")
}

// GetSeasonsUpcoming returns anime scheduled to air in upcoming seasons.
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
	return c.getSeasonList(ctx, page, "upcoming")
}

func (c *Client) getSeasonList(ctx context.Context, page int, season string) (TopAnimeResult, error) {
	if page < 1 {
		page = 1
	}
	cacheKey := fmt.Sprintf("seasons_%s:%d", season, page)

	var result TopAnimeResponse
	params := url.Values{}
	params.Set("page", strconv.Itoa(page))
	reqURL := buildRequestURL(c.baseURL, fmt.Sprintf("/seasons/%s", season), params)

	err := c.getWithCache(ctx, cacheKey, shortCacheTTL, reqURL, &result)
	if err != nil {
		return TopAnimeResult{}, err
	}

	return TopAnimeResult{
		Animes:      result.Data,
		HasNextPage: result.Pagination.HasNextPage,
	}, nil
}

// seedRandomPool seeds the in-memory pool of random anime
func (c *Client) seedRandomPool(ctx context.Context) {
	if !c.markRandomPoolInitialized() {
		return
	}

	c.loadCachedRandomPool(ctx)

	// Fetch a solid baseline in the background, then start refreshing.
	go c.seedRandomPoolBaseline()
}

func (c *Client) markRandomPoolInitialized() bool {
	c.poolMu.Lock()
	defer c.poolMu.Unlock()

	if c.poolInitialized {
		return false
	}

	c.poolInitialized = true
	return true
}

func (c *Client) loadCachedRandomPool(ctx context.Context) {
	cachedJSONs, err := c.db.GetAllCachedAnime(ctx)
	if err != nil || len(cachedJSONs) == 0 {
		return
	}

	loadedAnimes := decodeCachedAnime(cachedJSONs)
	if len(loadedAnimes) == 0 {
		return
	}

	c.poolMu.Lock()
	c.randomPool = append(c.randomPool, loadedAnimes...)
	c.poolMu.Unlock()
}

func decodeCachedAnime(cachedJSONs []string) []Anime {
	loadedAnimes := make([]Anime, 0, len(cachedJSONs))
	for _, dataStr := range cachedJSONs {
		var anime Anime
		if err := json.Unmarshal([]byte(dataStr), &anime); err != nil || anime.MalID == 0 {
			continue
		}

		loadedAnimes = append(loadedAnimes, anime)
	}

	return loadedAnimes
}

func (c *Client) seedRandomPoolBaseline() {
	bgCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	fetchedAnimes := c.fetchBaselineAnime(bgCtx)
	if len(fetchedAnimes) > 0 {
		c.appendUniqueRandomPool(fetchedAnimes)
	}

	// Start background refresher once seeding completes
	c.startPoolRefresher()
}

func (c *Client) fetchBaselineAnime(ctx context.Context) []Anime {
	topPageOne := c.fetchTopAnimePage(ctx, 1)
	topPageTwo := c.fetchTopAnimePage(ctx, 2)
	currentSeason := c.fetchCurrentSeasonAnime(ctx)

	fetchedAnimes := make([]Anime, 0, len(topPageOne)+len(topPageTwo)+len(currentSeason))
	fetchedAnimes = append(fetchedAnimes, topPageOne...)
	fetchedAnimes = append(fetchedAnimes, topPageTwo...)
	fetchedAnimes = append(fetchedAnimes, currentSeason...)
	return fetchedAnimes
}

func (c *Client) fetchTopAnimePage(ctx context.Context, page int) []Anime {
	top, err := c.GetTopAnime(ctx, page)
	if err != nil {
		return nil
	}

	return top.Animes
}

func (c *Client) fetchCurrentSeasonAnime(ctx context.Context) []Anime {
	now, err := c.GetSeasonsNow(ctx, 1)
	if err != nil {
		return nil
	}

	return now.Animes
}

func (c *Client) appendUniqueRandomPool(animes []Anime) {
	c.poolMu.Lock()
	defer c.poolMu.Unlock()

	seen := make(map[int]bool, len(c.randomPool)+len(animes))
	for _, anime := range c.randomPool {
		seen[anime.MalID] = true
	}

	for _, anime := range animes {
		if seen[anime.MalID] {
			continue
		}

		c.randomPool = append(c.randomPool, anime)
		seen[anime.MalID] = true
	}
}

// startPoolRefresher runs in the background to slowly mix in true random anime
func (c *Client) startPoolRefresher() {
	ticker := time.NewTicker(30 * time.Second)
	ctx := context.Background()

	for range ticker.C {
		var result struct {
			Data Anime `json:"data"`
		}
		reqURL := fmt.Sprintf("%s/random/anime", c.baseURL)
		err := c.fetchWithRetry(ctx, reqURL, &result)
		if err != nil {
			continue
		}

		if result.Data.MalID == 0 {
			continue
		}

		c.poolMu.Lock()
		if len(c.randomPool) >= 1000 {
			idx := rand.Intn(len(c.randomPool))
			c.randomPool[idx] = result.Data
		} else {
			duplicate := false
			for _, a := range c.randomPool {
				if a.MalID == result.Data.MalID {
					duplicate = true
					break
				}
			}
			if !duplicate {
				c.randomPool = append(c.randomPool, result.Data)
			}
		}
		c.poolMu.Unlock()
	}
}

// GetRandomAnime returns a random anime from the database.
func (c *Client) GetRandomAnime(ctx context.Context) (Anime, error) {
	c.poolMu.Lock()
	initialized := c.poolInitialized
	c.poolMu.Unlock()

	if !initialized {
		c.seedRandomPool(ctx)
	}

	c.poolMu.RLock()
	defer c.poolMu.RUnlock()

	if len(c.randomPool) == 0 {
		var result struct {
			Data Anime `json:"data"`
		}

		reqURL := fmt.Sprintf("%s/random/anime", c.baseURL)
		err := c.fetchWithRetry(ctx, reqURL, &result)
		if err != nil {
			return Anime{}, err
		}
		if result.Data.MalID == 0 {
			return Anime{}, fmt.Errorf("jikan: empty response for random/anime")
		}

		return result.Data, nil
	}

	idx := rand.Intn(len(c.randomPool))
	return c.randomPool[idx], nil
}
