import APIClient
import Combine
import ComponentLibrary
import ComposableArchitecture
import Foundation
import Utilities
/*
 Simple lyrics cache that keeps lyrics data for up to 500 hooks in memory.
 - Hooks are evicted in LRU order.
 - Provides cache storage and retrieval for lyrics data.
 - Tracks in-progress requests to prevent duplicates.
 */
final actor SimpleLyricsCache: Sendable {
    private var lyricsData: [String: LyricsDataV2] = [:]
    private var accessOrder: [String] = [] // LRU tracking
    private let maxCacheSize = 500

    // Keep track of hooks currently being fetched to prevent duplicates
    private var inProgressFetchRequests: Set<String> = []

    func getLyrics(for hookId: String) -> LyricsDataV2? {
        // Update access order for LRU
        if lyricsData[hookId] != nil {
            updateAccessOrder(for: hookId)
        }
        return lyricsData[hookId]
    }

    func setLyrics(_ lyrics: LyricsDataV2, for hookId: String) {
        lyricsData[hookId] = lyrics
        updateAccessOrder(for: hookId)
        evictIfNeeded()
    }

    func getLyricsMap(for hookIds: [String]) -> [String: LyricsDataV2] {
        var result: [String: LyricsDataV2] = [:]
        for hookId in hookIds {
            if let lyrics = lyricsData[hookId] {
                result[hookId] = lyrics
                updateAccessOrder(for: hookId)
            }
        }
        return result
    }

    private func getHooksNeedingLyrics(from hooks: [Hook]) -> [String] {
        let needingLyrics = hooks.compactMap { hook -> String? in
            let hasLyrics = lyricsData[hook.id] != nil
            let inProgress = inProgressFetchRequests.contains(hook.id)
            guard hook.showLyrics && !hasLyrics && !inProgress else {
                return nil
            }
            return hook.id
        }
        return needingLyrics
    }

    func removeHook(_ hookId: String) {
        lyricsData.removeValue(forKey: hookId)
        accessOrder.removeAll { $0 == hookId }
        inProgressFetchRequests.remove(hookId)
    }

    public func fetchForHooksIfNeeded(_ hooks: [Hook]) async {
        let hookIdsNeedingLyrics = getHooksNeedingLyrics(from: hooks)
        guard !hookIdsNeedingLyrics.isEmpty else { return }

        // Mark as in progress to prevent duplicate requests
        for hookId in hookIdsNeedingLyrics {
            inProgressFetchRequests.insert(hookId)
        }

        // Clean up in-progress requests when done
        defer {
            for hookId in hookIdsNeedingLyrics {
                inProgressFetchRequests.remove(hookId)
            }
        }

        do {
            @Dependency(\.apiClientV2) var api
            let lyricsMap = try await api.fetchHookLyrics(hookIdsNeedingLyrics)

            for (hookId, alignedLyrics) in lyricsMap {
                let lyricsDataV2 = alignedLyrics.toLyricsDataV2
                setLyrics(lyricsDataV2, for: hookId)
            }
        } catch {
            log.telemetry.error(error, message: "Failed to fetch lyrics for \(hookIdsNeedingLyrics.count) hooks: \(error).")
        }
    }

    private func updateAccessOrder(for hookId: String) {
        // Remove from current position
        accessOrder.removeAll { $0 == hookId }
        // Add to end (most recently used)
        accessOrder.append(hookId)
    }

    private func evictIfNeeded() {
        while lyricsData.count > maxCacheSize {
            guard let leastRecentlyUsed = accessOrder.first else { break }
            lyricsData.removeValue(forKey: leastRecentlyUsed)
            accessOrder.removeFirst()
        }
    }
}

// MARK: - AlignedLyrics to LyricsDataV2 Conversion

private extension AlignedLyrics {
    var toLyricsDataV2: LyricsDataV2 {
        let lyricsLines = alignedLyrics.mappedToLyricsLines
        return LyricsDataV2(lines: lyricsLines)
    }
}

private extension [AlignedLyricLine] {
    var mappedToLyricsLines: [LyricsLine] {
        guard !self.isEmpty else { return [] }

        return self.map { lyricLine in
            let wordTokens = lyricLine.words.map { word in
                WordToken(
                    text: word.text,
                    startTime: word.startS,
                    endTime: word.endS
                )
            }
            return LyricsLine(
                text: lyricLine.text,
                startTime: lyricLine.startS,
                endTime: lyricLine.endS,
                section: lyricLine.section,
                words: wordTokens
            )
        }
    }
}
