package gobackend

import (
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

const (
	lyricsCacheTTL       = 24 * time.Hour
	durationToleranceSec = 10.0
)

type LRCLibResponse struct {
	ID           int     `json:"id"`
	Name         string  `json:"name"`
	TrackName    string  `json:"trackName"`
	ArtistName   string  `json:"artistName"`
	AlbumName    string  `json:"albumName"`
	Duration     float64 `json:"duration"`
	Instrumental bool    `json:"instrumental"`
	PlainLyrics  string  `json:"plainLyrics"`
	SyncedLyrics string  `json:"syncedLyrics"`
}

func lrclibTrackName(response *LRCLibResponse) string {
	if response == nil {
		return ""
	}
	if trackName := strings.TrimSpace(response.TrackName); trackName != "" {
		return trackName
	}
	return strings.TrimSpace(response.Name)
}

type LyricsLine struct {
	StartTimeMs int64  `json:"startTimeMs"`
	Words       string `json:"words"`
	EndTimeMs   int64  `json:"endTimeMs"`
}

type LyricsResponse struct {
	Lines        []LyricsLine `json:"lines"`
	SyncType     string       `json:"syncType"`
	Instrumental bool         `json:"instrumental"`
	PlainLyrics  string       `json:"plainLyrics"`
	Provider     string       `json:"provider"`
	Source       string       `json:"source"`
}

type LyricsClient struct {
	httpClient *http.Client
}

func NewLyricsClient() *LyricsClient {
	return &LyricsClient{
		httpClient: NewHTTPClientWithTimeout(15 * time.Second),
	}
}

// lrclibGet performs a GET against lrclib.net and decodes the JSON body into
// dst. 404 is reported as a typed lyrics-not-found error.
func (c *LyricsClient) lrclibGet(path string, params url.Values, dst any) error {
	req, err := http.NewRequest("GET", "https://lrclib.net"+path+"?"+params.Encode(), nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("User-Agent", getRandomUserAgent())

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to fetch lyrics: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return lyricsNotFoundErrorf("lyrics not found")
	}
	if resp.StatusCode != 200 {
		return lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
	}

	if err := json.NewDecoder(resp.Body).Decode(dst); err != nil {
		return fmt.Errorf("failed to decode response: %w", err)
	}
	return nil
}

func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsResponse, error) {
	params := url.Values{}
	params.Set("artist_name", artist)
	params.Set("track_name", track)

	var lrcResp LRCLibResponse
	if err := c.lrclibGet("/api/get", params, &lrcResp); err != nil {
		return nil, err
	}
	if !lyricsSearchTitlesMatch(lrclibTrackName(&lrcResp), track, false) ||
		!lyricsSearchArtistsMatch(lrcResp.ArtistName, artist) {
		return nil, lyricsNotFoundErrorf("LRCLIB returned mismatched track metadata")
	}

	return c.parseLRCLibResponse(&lrcResp), nil
}

func (c *LyricsClient) fetchLyricsFromLRCLibSearch(query, trackName, artistName string, durationSec float64) (*LyricsResponse, error) {
	params := url.Values{}
	params.Set("q", query)

	var results []LRCLibResponse
	if err := c.lrclibGet("/api/search", params, &results); err != nil {
		return nil, err
	}

	if len(results) == 0 {
		return nil, lyricsNotFoundErrorf("no lyrics found")
	}

	bestMatch := c.findBestLRCLibSearchMatch(results, query, trackName, artistName, durationSec)
	if bestMatch != nil {
		return c.parseLRCLibResponse(bestMatch), nil
	}

	return nil, lyricsNotFoundErrorf("no matching lyrics found")
}

func lrclibSearchResultMatches(result *LRCLibResponse, query, trackName, artistName string, durationSec float64) bool {
	if result == nil || !lyricsSearchDurationMatches(result.Duration, durationSec) {
		return false
	}

	candidateTrack := lrclibTrackName(result)
	if strings.TrimSpace(trackName) != "" || strings.TrimSpace(artistName) != "" {
		return lyricsSearchTitlesMatch(candidateTrack, trackName, false) &&
			lyricsSearchArtistsMatch(result.ArtistName, artistName)
	}

	normalizedQuery := normalizeLooseArtistName(query)
	normalizedTrack := normalizeLooseArtistName(simplifyTrackName(candidateTrack))
	normalizedArtist := normalizeLooseArtistName(normalizeArtistName(result.ArtistName))
	return normalizedQuery != "" &&
		normalizedTrack != "" &&
		normalizedArtist != "" &&
		containsWordSequence(normalizedQuery, normalizedTrack) &&
		containsWordSequence(normalizedQuery, normalizedArtist)
}

func (c *LyricsClient) findBestLRCLibSearchMatch(results []LRCLibResponse, query, trackName, artistName string, targetDurationSec float64) *LRCLibResponse {
	var bestSynced *LRCLibResponse
	var bestPlain *LRCLibResponse

	for i := range results {
		result := &results[i]
		if !lrclibSearchResultMatches(result, query, trackName, artistName, targetDurationSec) {
			continue
		}
		if result.SyncedLyrics != "" && bestSynced == nil {
			bestSynced = result
		} else if result.PlainLyrics != "" && bestPlain == nil {
			bestPlain = result
		}
	}

	if bestSynced != nil {
		return bestSynced
	}
	return bestPlain
}

func plainLyricsFromTimedLines(lines []LyricsLine) string {
	parts := make([]string, 0, len(lines))
	for _, line := range lines {
		words := strings.TrimSpace(line.Words)
		if words == "" {
			continue
		}
		parts = append(parts, words)
	}
	return strings.Join(parts, "\n")
}

func (c *LyricsClient) durationMatches(lrcDuration, targetDuration float64) bool {
	diff := math.Abs(lrcDuration - targetDuration)
	return diff <= durationToleranceSec
}

func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string, durationSec float64) (*LyricsResponse, error) {
	primaryArtist := normalizeArtistName(artistName)
	fetchOptions := GetLyricsFetchOptions()
	configuredProviderOrder := GetLyricsProviderOrder()

	if isLikelyInstrumentalTrack(trackName) {
		GoLog("[Lyrics] Track marked instrumental by title heuristic, skipping lyrics search: %s - %s\n", artistName, trackName)
		instrumental := &LyricsResponse{
			Instrumental: true,
			Source:       "Heuristic: Instrumental",
		}
		globalLyricsCache.Set(artistName, trackName, durationSec, instrumental)
		return instrumental, nil
	}

	extensionProviders := make(map[string]*extensionProviderWrapper)
	extManager := getExtensionManager()
	if extManager != nil {
		for _, provider := range extManager.GetLyricsProviders() {
			providerName := "extension:" + strings.ToLower(strings.TrimSpace(provider.extension.ID))
			extensionProviders[providerName] = provider
		}
	}

	providerOrder := resolveLyricsProviderOrder(configuredProviderOrder, extensionProviders)
	selectedExtensionCount := 0
	for _, providerName := range providerOrder {
		if strings.HasPrefix(providerName, "extension:") {
			selectedExtensionCount++
		}
	}

	var cachedNonExtension *LyricsResponse
	if cached, found := globalLyricsCache.Get(artistName, trackName, durationSec); found {
		isExtensionCache := strings.HasPrefix(cached.Source, "Extension:")
		cachedProviderSelected := false
		if isExtensionCache {
			cachedProviderName := "extension:" + strings.ToLower(strings.TrimSpace(strings.TrimPrefix(cached.Source, "Extension:")))
			for _, providerName := range providerOrder {
				if providerName == cachedProviderName {
					cachedProviderSelected = true
					break
				}
			}
		}
		if (!isExtensionCache && selectedExtensionCount == 0) || cachedProviderSelected {
			fmt.Printf("[Lyrics] Cache hit for: %s - %s\n", artistName, trackName)
			cachedCopy := *cached
			cachedCopy.Source = cached.Source + " (cached)"
			return &cachedCopy, nil
		}

		if !isExtensionCache {
			// If extension providers are currently selected, don't let stale built-in
			// cache mask them. It remains available as a fallback if they fail.
			cachedNonExtension = cached
			GoLog("[Lyrics] Ignoring cached non-extension lyrics because selected extension providers are available\n")
		} else {
			GoLog("[Lyrics] Ignoring cached lyrics from an unselected extension provider\n")
		}
	}

	isValidResult := func(l *LyricsResponse) bool {
		return lyricsHasUsableText(l)
	}

	simplifiedTrack := simplifyTrackName(trackName)
	request := lyricsProviderSearchRequest{
		spotifyID:       spotifyID,
		trackName:       trackName,
		artistName:      artistName,
		primaryArtist:   primaryArtist,
		simplifiedTrack: simplifiedTrack,
		durationSec:     durationSec,
		fetchOptions:    fetchOptions,
	}

	GoLog("[Lyrics] Searching for: %s - %s (providers: %v)\n", artistName, trackName, providerOrder)

	fetchProvider := func(providerName string, request lyricsProviderSearchRequest) (*LyricsResponse, error, bool) {
		if provider, ok := extensionProviders[providerName]; ok {
			lyrics, err := provider.FetchLyrics(request.trackName, request.artistName, "", request.durationSec)
			return lyrics, err, true
		}
		return c.fetchBuiltInLyricsProvider(providerName, request)
	}

	lyrics, err := fetchLyricsProviders(providerOrder, request, fetchProvider)
	if err == nil && isValidResult(lyrics) {
		globalLyricsCache.Set(artistName, trackName, durationSec, lyrics)
		return lyrics, nil
	}

	if cachedNonExtension != nil {
		cachedCopy := *cachedNonExtension
		cachedCopy.Source = cachedNonExtension.Source + " (cached fallback)"
		GoLog("[Lyrics] Selected extension providers unavailable for this track, using cached built-in lyrics\n")
		return &cachedCopy, nil
	}

	return nil, fmt.Errorf("lyrics not found from any source")
}

func resolveLyricsProviderOrder(
	configuredOrder []string,
	extensionProviders map[string]*extensionProviderWrapper,
) []string {
	providerOrder := make([]string, 0, len(configuredOrder))
	for _, providerName := range configuredOrder {
		if isKnownBuiltInLyricsProvider(providerName) {
			providerOrder = append(providerOrder, providerName)
			continue
		}
		if _, available := extensionProviders[providerName]; available {
			providerOrder = append(providerOrder, providerName)
		}
	}
	return providerOrder
}

func fetchLyricsProviders(
	providerOrder []string,
	request lyricsProviderSearchRequest,
	fetchProvider func(string, lyricsProviderSearchRequest) (*LyricsResponse, error, bool),
) (*LyricsResponse, error) {
	type providerCandidate struct {
		index int
		name  string
	}

	candidates := make([]providerCandidate, 0, len(providerOrder))
	results := make(chan lyricsProviderSearchResult, len(providerOrder))
	sem := make(chan struct{}, lyricsProviderParallelism)
	var wg sync.WaitGroup

	for index, providerName := range providerOrder {
		if skip, remaining, reason := shouldSkipLyricsProvider(providerName); skip {
			GoLog("[Lyrics] Skipping unavailable provider %s for %s: %s\n", providerName, remaining.Round(time.Second), reason)
			continue
		}

		knownProvider := isKnownBuiltInLyricsProvider(providerName) ||
			(strings.HasPrefix(providerName, "extension:") && len(providerName) > len("extension:"))
		if !knownProvider {
			GoLog("[Lyrics] Unknown provider: %s, skipping\n", providerName)
			continue
		}

		candidate := providerCandidate{index: index, name: providerName}
		candidates = append(candidates, candidate)
		wg.Add(1)
		go func() {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			GoLog("[Lyrics] Trying provider: %s\n", candidate.name)
			lyrics, err, ok := fetchProvider(candidate.name, request)
			if !ok {
				results <- lyricsProviderSearchResult{index: candidate.index, providerName: candidate.name, err: fmt.Errorf("unknown provider")}
				return
			}
			if err == nil && lyricsHasUsableText(lyrics) {
				GoLog("[Lyrics] Got lyrics from: %s\n", candidate.name)
				markLyricsProviderAvailable(candidate.name)
			} else if err != nil {
				GoLog("[Lyrics] Provider %s failed: %v\n", candidate.name, err)
				markLyricsProviderUnavailable(candidate.name, err)
			}
			results <- lyricsProviderSearchResult{index: candidate.index, providerName: candidate.name, lyrics: lyrics, err: err}
		}()
	}

	if len(candidates) == 0 {
		return nil, fmt.Errorf("lyrics not found from any source")
	}

	go func() {
		wg.Wait()
		close(results)
	}()

	completed := make(map[int]bool, len(candidates))
	var best *lyricsProviderSearchResult
	var lastErr error
	var graceTimer *time.Timer
	var grace <-chan time.Time

	stopGrace := func() {
		if graceTimer != nil {
			if !graceTimer.Stop() {
				select {
				case <-graceTimer.C:
				default:
				}
			}
			graceTimer = nil
			grace = nil
		}
	}
	defer stopGrace()

	hasPendingEarlier := func(index int) bool {
		for _, candidate := range candidates {
			if candidate.index >= index {
				return false
			}
			if !completed[candidate.index] {
				return true
			}
		}
		return false
	}

	for remaining := len(candidates); remaining > 0; {
		if best != nil && !hasPendingEarlier(best.index) {
			return best.lyrics, nil
		}
		if best != nil && graceTimer == nil {
			graceTimer = time.NewTimer(lyricsProviderPriorityGrace)
			grace = graceTimer.C
		}

		select {
		case result, ok := <-results:
			if !ok {
				remaining = 0
				break
			}
			remaining--
			completed[result.index] = true
			if result.err != nil {
				lastErr = result.err
			}
			if lyricsHasUsableText(result.lyrics) && (best == nil || result.index < best.index) {
				copied := result
				best = &copied
				stopGrace()
			}
		case <-grace:
			if best != nil {
				GoLog("[Lyrics] Returning provider %s after %s priority grace\n", best.providerName, lyricsProviderPriorityGrace)
				return best.lyrics, nil
			}
		}
	}

	if best != nil {
		return best.lyrics, nil
	}
	if lastErr != nil {
		return nil, lastErr
	}
	return nil, fmt.Errorf("lyrics not found from any source")
}

func isKnownBuiltInLyricsProvider(providerName string) bool {
	switch providerName {
	case LyricsProviderLRCLIB,
		LyricsProviderNetease,
		LyricsProviderMusixmatch,
		LyricsProviderAppleMusic,
		LyricsProviderQQMusic,
		LyricsProviderSpotify,
		LyricsProviderDeezer,
		LyricsProviderYouTube,
		LyricsProviderKugou,
		LyricsProviderGenius,
		LyricsProviderLyricsPlus:
		return true
	default:
		return false
	}
}

func (c *LyricsClient) fetchBuiltInLyricsProvider(providerName string, request lyricsProviderSearchRequest) (*LyricsResponse, error, bool) {
	switch providerName {
	case LyricsProviderLRCLIB:
		lyrics, err := c.tryLRCLIB(request.primaryArtist, request.artistName, request.trackName, request.simplifiedTrack, request.durationSec)
		return lyrics, err, true

	case LyricsProviderNetease:
		neteaseClient := NewNeteaseClient()
		lyrics, err := neteaseClient.FetchLyrics(
			request.trackName,
			request.primaryArtist,
			request.durationSec,
			request.fetchOptions.IncludeTranslationNetease,
			request.fetchOptions.IncludeRomanizationNetease,
		)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = neteaseClient.FetchLyrics(
				request.trackName,
				request.artistName,
				request.durationSec,
				request.fetchOptions.IncludeTranslationNetease,
				request.fetchOptions.IncludeRomanizationNetease,
			)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = neteaseClient.FetchLyrics(
				request.simplifiedTrack,
				request.primaryArtist,
				request.durationSec,
				request.fetchOptions.IncludeTranslationNetease,
				request.fetchOptions.IncludeRomanizationNetease,
			)
		}
		return lyrics, err, true

	case LyricsProviderMusixmatch:
		musixmatchClient := NewMusixmatchClient()
		lyrics, err := musixmatchClient.FetchLyrics(
			request.trackName,
			request.primaryArtist,
			request.durationSec,
			request.fetchOptions.MusixmatchLanguage,
		)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = musixmatchClient.FetchLyrics(
				request.trackName,
				request.artistName,
				request.durationSec,
				request.fetchOptions.MusixmatchLanguage,
			)
		}
		return lyrics, err, true

	case LyricsProviderAppleMusic:
		appleClient := NewAppleMusicClient()
		lyrics, err := appleClient.FetchLyrics(request.trackName, request.primaryArtist, request.durationSec, request.fetchOptions.MultiPersonWordByWord, request.fetchOptions.AppleElrcWordSync)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = appleClient.FetchLyrics(request.trackName, request.artistName, request.durationSec, request.fetchOptions.MultiPersonWordByWord, request.fetchOptions.AppleElrcWordSync)
		}
		return lyrics, err, true

	case LyricsProviderQQMusic:
		qqClient := NewQQMusicClient()
		lyrics, err := qqClient.FetchLyrics(request.trackName, request.primaryArtist, request.durationSec, request.fetchOptions.MultiPersonWordByWord)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = qqClient.FetchLyrics(request.trackName, request.artistName, request.durationSec, request.fetchOptions.MultiPersonWordByWord)
		}
		return lyrics, err, true

	case LyricsProviderSpotify:
		spotifyClient := NewSpotifyLyricsClient()
		lyrics, err := spotifyClient.FetchLyrics(request.spotifyID, request.trackName, request.primaryArtist, request.durationSec)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = spotifyClient.FetchLyrics(request.spotifyID, request.trackName, request.artistName, request.durationSec)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = spotifyClient.FetchLyrics("", request.simplifiedTrack, request.primaryArtist, request.durationSec)
		}
		return lyrics, err, true

	case LyricsProviderDeezer:
		deezerClient := NewDeezerLyricsClient()
		lyrics, err := deezerClient.FetchLyrics(request.spotifyID, request.trackName, request.primaryArtist, request.durationSec)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = deezerClient.FetchLyrics(request.spotifyID, request.trackName, request.artistName, request.durationSec)
		}
		return lyrics, err, true

	case LyricsProviderYouTube:
		youtubeClient := NewYouTubeLyricsClient()
		lyrics, err := youtubeClient.FetchLyrics(request.trackName, request.primaryArtist, request.durationSec)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = youtubeClient.FetchLyrics(request.trackName, request.artistName, request.durationSec)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = youtubeClient.FetchLyrics(request.simplifiedTrack, request.primaryArtist, request.durationSec)
		}
		return lyrics, err, true

	case LyricsProviderKugou:
		kugouClient := NewKugouLyricsClient()
		lyrics, err := kugouClient.FetchLyrics(request.trackName, request.primaryArtist, request.durationSec)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = kugouClient.FetchLyrics(request.trackName, request.artistName, request.durationSec)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = kugouClient.FetchLyrics(request.simplifiedTrack, request.primaryArtist, request.durationSec)
		}
		return lyrics, err, true

	case LyricsProviderGenius:
		geniusClient := NewGeniusLyricsClient()
		lyrics, err := geniusClient.FetchLyrics(request.trackName, request.primaryArtist, request.durationSec)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = geniusClient.FetchLyrics(request.trackName, request.artistName, request.durationSec)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = geniusClient.FetchLyrics(request.simplifiedTrack, request.primaryArtist, request.durationSec)
		}
		return lyrics, err, true

	case LyricsProviderLyricsPlus:
		lyricsPlusClient := NewLyricsPlusClient()
		lyrics, err := lyricsPlusClient.FetchLyrics(
			request.trackName,
			request.primaryArtist,
			"",
			request.durationSec,
			request.fetchOptions.MultiPersonWordByWord,
			request.fetchOptions.AppleElrcWordSync,
		)
		if err != nil && !isLyricsProviderUnavailableError(err) && request.primaryArtist != request.artistName {
			lyrics, err = lyricsPlusClient.FetchLyrics(
				request.trackName,
				request.artistName,
				"",
				request.durationSec,
				request.fetchOptions.MultiPersonWordByWord,
				request.fetchOptions.AppleElrcWordSync,
			)
		}
		if err != nil && !isLyricsProviderUnavailableError(err) && request.simplifiedTrack != request.trackName {
			lyrics, err = lyricsPlusClient.FetchLyrics(
				request.simplifiedTrack,
				request.primaryArtist,
				"",
				request.durationSec,
				request.fetchOptions.MultiPersonWordByWord,
				request.fetchOptions.AppleElrcWordSync,
			)
		}
		return lyrics, err, true
	default:
		return nil, fmt.Errorf("unknown provider: %s", providerName), false
	}
}

func (c *LyricsClient) tryLRCLIB(primaryArtist, artistName, trackName, simplifiedTrack string, durationSec float64) (*LyricsResponse, error) {
	var lyrics *LyricsResponse
	var err error

	lyrics, err = c.FetchLyricsWithMetadata(primaryArtist, trackName)
	if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) {
		lyrics.Source = "LRCLIB"
		return lyrics, nil
	}
	if isLyricsProviderUnavailableError(err) {
		return nil, err
	}

	if primaryArtist != artistName {
		lyrics, err = c.FetchLyricsWithMetadata(artistName, trackName)
		if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) {
			lyrics.Source = "LRCLIB"
			return lyrics, nil
		}
		if isLyricsProviderUnavailableError(err) {
			return nil, err
		}
	}

	if simplifiedTrack != trackName {
		lyrics, err = c.FetchLyricsWithMetadata(primaryArtist, simplifiedTrack)
		if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) {
			lyrics.Source = "LRCLIB (simplified)"
			return lyrics, nil
		}
		if isLyricsProviderUnavailableError(err) {
			return nil, err
		}
	}

	query := primaryArtist + " " + trackName
	lyrics, err = c.fetchLyricsFromLRCLibSearch(query, trackName, primaryArtist, durationSec)
	if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) {
		lyrics.Source = "LRCLIB Search"
		return lyrics, nil
	}
	if isLyricsProviderUnavailableError(err) {
		return nil, err
	}

	if simplifiedTrack != trackName {
		query = primaryArtist + " " + simplifiedTrack
		lyrics, err = c.fetchLyricsFromLRCLibSearch(query, simplifiedTrack, primaryArtist, durationSec)
		if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) {
			lyrics.Source = "LRCLIB Search (simplified)"
			return lyrics, nil
		}
		if isLyricsProviderUnavailableError(err) {
			return nil, err
		}
	}

	return nil, lyricsNotFoundErrorf("LRCLIB: no lyrics found")
}

func (c *LyricsClient) parseLRCLibResponse(resp *LRCLibResponse) *LyricsResponse {
	result := &LyricsResponse{
		Instrumental: resp.Instrumental,
		PlainLyrics:  resp.PlainLyrics,
		Provider:     "LRCLIB",
	}

	if resp.SyncedLyrics != "" {
		result.Lines = parseSyncedLyrics(resp.SyncedLyrics)
		result.SyncType = "LINE_SYNCED"
	} else if resp.PlainLyrics != "" {
		result.SyncType = "UNSYNCED"
		lines := strings.Split(resp.PlainLyrics, "\n")
		for _, line := range lines {
			if strings.TrimSpace(line) != "" {
				result.Lines = append(result.Lines, LyricsLine{
					StartTimeMs: 0,
					Words:       line,
					EndTimeMs:   0,
				})
			}
		}
	}

	return result
}
