import Foundation

/// Fetches lyrics from LRCLIB (https://lrclib.net) — a free, open-source, community-driven lyrics API.
/// No API key required. Returns both synced (LRC timestamped) and plain lyrics.
actor LRCLIBService {

    static let shared = LRCLIBService()

    private let baseURL = "https://lrclib.net/api"
    private let session: URLSession

    /// Cached results keyed by "artist - title".
    private var cache: [String: LyricsResult] = [:]

    init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 10
        config.httpAdditionalHeaders = ["User-Agent": "MixBoard/1.0 (https://github.com/mixboard)"]
        session = URLSession(configuration: config)
    }

    // MARK: - Public API

    /// Search for lyrics by artist and track title.
    /// Optionally provide album name and duration (in seconds) for better matching.
    func fetchLyrics(
        artist: String,
        title: String,
        album: String? = nil,
        duration: TimeInterval? = nil
    ) async throws -> LyricsResult {
        // Check cache
        let cacheKey = "\(artist.lowercased()) - \(title.lowercased())"
        if let cached = cache[cacheKey] {
            return cached
        }

        // Build the request URL
        // First try the exact "get" endpoint (best match with duration)
        if let result = try? await fetchExact(artist: artist, title: title, album: album, duration: duration) {
            cache[cacheKey] = result
            return result
        }

        // Fallback to search endpoint
        let result = try await fetchSearch(artist: artist, title: title)
        cache[cacheKey] = result
        return result
    }

    /// Clear the lyrics cache.
    func clearCache() {
        cache.removeAll()
    }

    // MARK: - Private

    /// Use the /api/get endpoint (exact match with optional duration).
    private func fetchExact(
        artist: String,
        title: String,
        album: String?,
        duration: TimeInterval?
    ) async throws -> LyricsResult {
        var components = URLComponents(string: "\(baseURL)/get")!
        var queryItems = [
            URLQueryItem(name: "artist_name", value: artist),
            URLQueryItem(name: "track_name", value: title)
        ]
        if let album, !album.isEmpty {
            queryItems.append(URLQueryItem(name: "album_name", value: album))
        }
        if let duration, duration > 0 {
            queryItems.append(URLQueryItem(name: "duration", value: String(Int(duration))))
        }
        components.queryItems = queryItems

        guard let url = components.url else {
            throw LyricsError.invalidURL
        }

        let (data, response) = try await session.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw LyricsError.networkError
        }

        if httpResponse.statusCode == 404 {
            throw LyricsError.notFound
        }

        guard httpResponse.statusCode == 200 else {
            throw LyricsError.httpError(httpResponse.statusCode)
        }

        let decoded = try JSONDecoder().decode(LRCLIBResponse.self, from: data)
        return LyricsResult(from: decoded)
    }

    /// Use the /api/search endpoint (fuzzy search, returns array).
    private func fetchSearch(artist: String, title: String) async throws -> LyricsResult {
        var components = URLComponents(string: "\(baseURL)/search")!
        components.queryItems = [
            URLQueryItem(name: "q", value: "\(artist) \(title)")
        ]

        guard let url = components.url else {
            throw LyricsError.invalidURL
        }

        let (data, response) = try await session.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw LyricsError.networkError
        }

        let results = try JSONDecoder().decode([LRCLIBResponse].self, from: data)

        guard let best = results.first else {
            throw LyricsError.notFound
        }

        return LyricsResult(from: best)
    }
}

// MARK: - Models

/// Response from the LRCLIB API.
private struct LRCLIBResponse: Decodable {
    let id: Int
    let trackName: String?
    let artistName: String?
    let albumName: String?
    let duration: Double?
    let instrumental: Bool?
    let plainLyrics: String?
    let syncedLyrics: String?
}

/// Parsed lyrics result.
struct LyricsResult {
    let trackName: String
    let artistName: String
    let albumName: String
    let isInstrumental: Bool
    let plainLyrics: String?
    let syncedLyrics: String?

    /// Whether synced (timestamped) lyrics are available.
    var hasSyncedLyrics: Bool { syncedLyrics != nil && !(syncedLyrics?.isEmpty ?? true) }

    /// Whether any lyrics are available.
    var hasLyrics: Bool { hasSyncedLyrics || (plainLyrics != nil && !(plainLyrics?.isEmpty ?? true)) }

    fileprivate init(from response: LRCLIBResponse) {
        trackName = response.trackName ?? ""
        artistName = response.artistName ?? ""
        albumName = response.albumName ?? ""
        isInstrumental = response.instrumental ?? false
        plainLyrics = response.plainLyrics
        syncedLyrics = response.syncedLyrics
    }

    init(
        trackName: String = "",
        artistName: String = "",
        albumName: String = "",
        isInstrumental: Bool = false,
        plainLyrics: String? = nil,
        syncedLyrics: String? = nil
    ) {
        self.trackName = trackName
        self.artistName = artistName
        self.albumName = albumName
        self.isInstrumental = isInstrumental
        self.plainLyrics = plainLyrics
        self.syncedLyrics = syncedLyrics
    }
}

// MARK: - Errors

enum LyricsError: LocalizedError {
    case invalidURL
    case notFound
    case networkError
    case httpError(Int)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid lyrics search URL"
        case .notFound: return "No lyrics found"
        case .networkError: return "Network error fetching lyrics"
        case .httpError(let code): return "HTTP error \(code)"
        }
    }
}
