package gobackend

import (
	"errors"
	"fmt"
	"math"
	"regexp"
	"strings"
	"sync"
	"time"
)

const (
	lyricsProviderUnavailableCooldown = 10 * time.Minute
	lyricsProviderParallelism         = 3
	lyricsProviderPriorityGrace       = 5000 * time.Millisecond
)

const (
	LyricsProviderLRCLIB     = "lrclib"
	LyricsProviderNetease    = "netease"
	LyricsProviderMusixmatch = "musixmatch"
	LyricsProviderAppleMusic = "apple_music"
	LyricsProviderQQMusic    = "qqmusic"
	LyricsProviderSpotify    = "spotify"
	LyricsProviderDeezer     = "deezer"
	LyricsProviderYouTube    = "youtube"
	LyricsProviderKugou      = "kugou"
	LyricsProviderGenius     = "genius"
	LyricsProviderLyricsPlus = "lyricsplus"
)

var DefaultLyricsProviders = []string{
	LyricsProviderLRCLIB,
	LyricsProviderAppleMusic,
}

var (
	lyricsProvidersMu sync.RWMutex
	lyricsProviders   []string // ordered list of enabled providers
	appVersionMu      sync.RWMutex
	appVersion        string
)

type lyricsProviderHealthEntry struct {
	unavailableUntil time.Time
	reason           string
}

type lyricsProviderSearchRequest struct {
	spotifyID       string
	trackName       string
	artistName      string
	primaryArtist   string
	simplifiedTrack string
	durationSec     float64
	fetchOptions    LyricsFetchOptions
}

type lyricsProviderSearchResult struct {
	index        int
	providerName string
	lyrics       *LyricsResponse
	err          error
}

var (
	lyricsProviderHealthMu sync.RWMutex
	lyricsProviderHealth   = make(map[string]lyricsProviderHealthEntry)
)

func SetAppVersion(version string) {
	normalized := strings.TrimSpace(version)

	appVersionMu.Lock()
	defer appVersionMu.Unlock()
	appVersion = normalized
}

func GetAppVersion() string {
	appVersionMu.RLock()
	defer appVersionMu.RUnlock()
	return appVersion
}

func appUserAgent() string {
	version := GetAppVersion()

	if version == "" {
		return "SpotiFLAC-Mobile"
	}

	return "SpotiFLAC-Mobile/" + version
}

type LyricsFetchOptions struct {
	IncludeTranslationNetease  bool   `json:"include_translation_netease"`
	IncludeRomanizationNetease bool   `json:"include_romanization_netease"`
	MultiPersonWordByWord      bool   `json:"multi_person_word_by_word"`
	AppleElrcWordSync          bool   `json:"apple_elrc_word_sync"`
	MusixmatchLanguage         string `json:"musixmatch_language,omitempty"`
}

var defaultLyricsFetchOptions = LyricsFetchOptions{
	IncludeTranslationNetease:  false,
	IncludeRomanizationNetease: false,
	MultiPersonWordByWord:      true,
	AppleElrcWordSync:          false,
	MusixmatchLanguage:         "",
}

var instrumentalTrackPattern = regexp.MustCompile(`(?i)(?:^|[\s\[(\-])(?:instrumental|inst\.?)(?:[\s\])]|$)`)

var (
	lyricsFetchOptionsMu sync.RWMutex
	lyricsFetchOptions   = defaultLyricsFetchOptions
)

func SetLyricsProviderOrder(providers []string) {
	lyricsProvidersMu.Lock()

	if len(providers) == 0 {
		changed := len(lyricsProviders) != 0
		lyricsProviders = nil
		lyricsProvidersMu.Unlock()
		clearLyricsProviderHealth()
		if changed {
			globalLyricsCache.ClearAll()
		}
		return
	}

	validNames := map[string]bool{
		LyricsProviderLRCLIB:     true,
		LyricsProviderNetease:    true,
		LyricsProviderMusixmatch: true,
		LyricsProviderAppleMusic: true,
		LyricsProviderQQMusic:    true,
		LyricsProviderSpotify:    true,
		LyricsProviderDeezer:     true,
		LyricsProviderYouTube:    true,
		LyricsProviderKugou:      true,
		LyricsProviderGenius:     true,
		LyricsProviderLyricsPlus: true,
	}

	valid := make([]string, 0, len(providers))
	seen := make(map[string]struct{}, len(providers))
	for _, p := range providers {
		normalized := strings.ToLower(strings.TrimSpace(p))
		isExtension := strings.HasPrefix(normalized, "extension:") &&
			strings.TrimSpace(strings.TrimPrefix(normalized, "extension:")) != ""
		if !validNames[normalized] && !isExtension {
			continue
		}
		if _, exists := seen[normalized]; exists {
			continue
		}
		seen[normalized] = struct{}{}
		valid = append(valid, normalized)
	}

	changed := !equalLyricsProviderOrders(lyricsProviders, valid)
	lyricsProviders = valid
	lyricsProvidersMu.Unlock()
	clearLyricsProviderHealth()
	if changed {
		globalLyricsCache.ClearAll()
	}
	GoLog("[Lyrics] Provider order set to: %v\n", valid)
}

func equalLyricsProviderOrders(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func clearLyricsProviderHealth() {
	lyricsProviderHealthMu.Lock()
	defer lyricsProviderHealthMu.Unlock()
	lyricsProviderHealth = make(map[string]lyricsProviderHealthEntry)
}

func lyricsProviderHealthKey(providerName string) string {
	return strings.ToLower(strings.TrimSpace(providerName))
}

func shouldSkipLyricsProvider(providerName string) (bool, time.Duration, string) {
	key := lyricsProviderHealthKey(providerName)
	if key == "" {
		return false, 0, ""
	}

	now := time.Now()
	lyricsProviderHealthMu.RLock()
	entry, ok := lyricsProviderHealth[key]
	lyricsProviderHealthMu.RUnlock()
	if !ok {
		return false, 0, ""
	}
	if !now.Before(entry.unavailableUntil) {
		lyricsProviderHealthMu.Lock()
		if current, exists := lyricsProviderHealth[key]; exists && !now.Before(current.unavailableUntil) {
			delete(lyricsProviderHealth, key)
		}
		lyricsProviderHealthMu.Unlock()
		return false, 0, ""
	}
	return true, time.Until(entry.unavailableUntil), entry.reason
}

func markLyricsProviderAvailable(providerName string) {
	key := lyricsProviderHealthKey(providerName)
	if key == "" {
		return
	}
	lyricsProviderHealthMu.Lock()
	delete(lyricsProviderHealth, key)
	lyricsProviderHealthMu.Unlock()
}

func markLyricsProviderUnavailable(providerName string, err error) {
	if err == nil || !isLyricsProviderUnavailableError(err) {
		return
	}
	key := lyricsProviderHealthKey(providerName)
	if key == "" {
		return
	}
	reason := strings.TrimSpace(err.Error())
	if len(reason) > 160 {
		reason = reason[:160]
	}
	unavailableUntil := time.Now().Add(lyricsProviderUnavailableCooldown)

	lyricsProviderHealthMu.Lock()
	lyricsProviderHealth[key] = lyricsProviderHealthEntry{
		unavailableUntil: unavailableUntil,
		reason:           reason,
	}
	lyricsProviderHealthMu.Unlock()
	GoLog("[Lyrics] Provider %s marked unavailable for %s: %s\n", providerName, lyricsProviderUnavailableCooldown, reason)
}

// isLyricsProviderUnavailableError reports whether err is a provider/API-level
// failure that should temporarily disable a lyrics source. Providers classify
// their failures with the typed errors in lyrics_errors.go at the point of
// origin; transport failures are handled by isConnectivityFailure.
func isLyricsProviderUnavailableError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, errLyricsNotFound) {
		return false
	}
	if errors.Is(err, errLyricsServiceUnavailable) {
		return true
	}
	return isConnectivityFailure(err)
}

func GetLyricsProviderOrder() []string {
	lyricsProvidersMu.RLock()
	defer lyricsProvidersMu.RUnlock()

	if len(lyricsProviders) == 0 {
		result := make([]string, len(DefaultLyricsProviders))
		copy(result, DefaultLyricsProviders)
		return result
	}

	result := make([]string, len(lyricsProviders))
	copy(result, lyricsProviders)
	return result
}

func GetAvailableLyricsProviders() []map[string]any {
	return []map[string]any{
		{"id": LyricsProviderLRCLIB, "name": "LRCLIB", "has_proxy_dependency": false, "description": "Open-source synced lyrics database"},
		{"id": LyricsProviderNetease, "name": "Netease", "has_proxy_dependency": true, "description": "NetEase Cloud Music lyrics"},
		{"id": LyricsProviderMusixmatch, "name": "Musixmatch", "has_proxy_dependency": true, "description": "Musixmatch lyrics"},
		{"id": LyricsProviderAppleMusic, "name": "Apple Music", "has_proxy_dependency": true, "description": "Apple Music synced lyrics"},
		{"id": LyricsProviderQQMusic, "name": "QQ Music", "has_proxy_dependency": true, "description": "QQ Music lyrics"},
		{"id": LyricsProviderSpotify, "name": "Spotify", "has_proxy_dependency": true, "description": "Spotify synced lyrics"},
		{"id": LyricsProviderDeezer, "name": "Deezer", "has_proxy_dependency": true, "description": "Deezer lyrics"},
		{"id": LyricsProviderYouTube, "name": "YouTube", "has_proxy_dependency": true, "description": "YouTube lyrics"},
		{"id": LyricsProviderKugou, "name": "Kugou", "has_proxy_dependency": true, "description": "Kugou lyrics"},
		{"id": LyricsProviderGenius, "name": "Genius", "has_proxy_dependency": true, "description": "Genius lyrics"},
		{"id": LyricsProviderLyricsPlus, "name": "LyricsPlus", "has_proxy_dependency": true, "description": "Word-by-word karaoke lyrics (Apple/Musixmatch/Spotify/QQ)"},
	}
}

func normalizeLyricsFetchOptions(opts LyricsFetchOptions) LyricsFetchOptions {
	opts.MusixmatchLanguage = strings.ToLower(strings.TrimSpace(opts.MusixmatchLanguage))
	opts.MusixmatchLanguage = regexp.MustCompile(`[^a-z0-9\-_]`).ReplaceAllString(opts.MusixmatchLanguage, "")
	if len(opts.MusixmatchLanguage) > 16 {
		opts.MusixmatchLanguage = opts.MusixmatchLanguage[:16]
	}
	return opts
}

func SetLyricsFetchOptions(opts LyricsFetchOptions) {
	normalized := normalizeLyricsFetchOptions(opts)

	lyricsFetchOptionsMu.Lock()
	defer lyricsFetchOptionsMu.Unlock()
	changed := lyricsFetchOptions != normalized
	lyricsFetchOptions = normalized

	if changed {
		globalLyricsCache.ClearAll()
	}

	GoLog("[Lyrics] Fetch options set: translation=%v romanization=%v multi_person=%v apple_elrc=%v musixmatch_lang=%q\n",
		normalized.IncludeTranslationNetease,
		normalized.IncludeRomanizationNetease,
		normalized.MultiPersonWordByWord,
		normalized.AppleElrcWordSync,
		normalized.MusixmatchLanguage,
	)
}

func GetLyricsFetchOptions() LyricsFetchOptions {
	lyricsFetchOptionsMu.RLock()
	defer lyricsFetchOptionsMu.RUnlock()
	return lyricsFetchOptions
}

type lyricsCacheEntry struct {
	response  *LyricsResponse
	expiresAt time.Time
}

type lyricsCache struct {
	mu    sync.RWMutex
	cache map[string]*lyricsCacheEntry
}

var globalLyricsCache = &lyricsCache{
	cache: make(map[string]*lyricsCacheEntry),
}

func (c *lyricsCache) generateKey(artist, track string, durationSec float64) string {
	normalizedArtist := strings.ToLower(strings.TrimSpace(artist))
	normalizedTrack := strings.ToLower(strings.TrimSpace(track))
	roundedDuration := math.Round(durationSec/10) * 10
	return fmt.Sprintf("%s|%s|%.0f", normalizedArtist, normalizedTrack, roundedDuration)
}

func (c *lyricsCache) Get(artist, track string, durationSec float64) (*LyricsResponse, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	key := c.generateKey(artist, track, durationSec)
	entry, exists := c.cache[key]
	if !exists {
		return nil, false
	}

	if time.Now().After(entry.expiresAt) {
		return nil, false
	}

	return entry.response, true
}

const lyricsCacheMaxEntries = 500

func (c *lyricsCache) Set(artist, track string, durationSec float64, response *LyricsResponse) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Bound the cache: without eviction a long session accumulates every
	// looked-up track's full lyrics forever.
	if len(c.cache) >= lyricsCacheMaxEntries {
		now := time.Now()
		for key, entry := range c.cache {
			if now.After(entry.expiresAt) {
				delete(c.cache, key)
			}
		}
		for len(c.cache) >= lyricsCacheMaxEntries {
			var oldestKey string
			var oldestAt time.Time
			for key, entry := range c.cache {
				if oldestKey == "" || entry.expiresAt.Before(oldestAt) {
					oldestKey = key
					oldestAt = entry.expiresAt
				}
			}
			delete(c.cache, oldestKey)
		}
	}

	key := c.generateKey(artist, track, durationSec)
	c.cache[key] = &lyricsCacheEntry{
		response:  response,
		expiresAt: time.Now().Add(lyricsCacheTTL),
	}
}

func (c *lyricsCache) CleanExpired() int {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	cleaned := 0
	for key, entry := range c.cache {
		if now.After(entry.expiresAt) {
			delete(c.cache, key)
			cleaned++
		}
	}
	return cleaned
}

func (c *lyricsCache) Size() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.cache)
}

func (c *lyricsCache) ClearAll() int {
	c.mu.Lock()
	defer c.mu.Unlock()

	cleared := len(c.cache)
	c.cache = make(map[string]*lyricsCacheEntry)
	return cleared
}
