import Foundation
import Security

// MARK: - Cached Regexes for TMDB Title Normalization
private enum TMDBTitleRegex {
    static let yearInParens: NSRegularExpression? = try? NSRegularExpression(pattern: #"\s*\(\d{4}\)\s*"#)
    static let yearWithDash: NSRegularExpression? = try? NSRegularExpression(pattern: #"\s*-\s*\d{4}\s*$"#)
    static let standaloneYear: NSRegularExpression? = try? NSRegularExpression(pattern: #"\s+\d{4}\s*$"#)
    static let fourKPrefix: NSRegularExpression? = try? NSRegularExpression(pattern: #"^4K:\s*"#, options: .caseInsensitive)
}

// MARK: - Thread-safe Progress Counter
private actor ProgressCounter {
    private var count = 0

    func increment() -> Int {
        count += 1
        return count
    }
}

actor TMDBService {
    nonisolated(unsafe) static let shared = TMDBService()

    private let networkService = NetworkService.shared
    private let storageService = StorageService.shared

    // Memory cache
    private var cachedMovies: [TMDBMovie]?
    private var cacheTimestamp: Date?

    private init() {}

    // MARK: - API Key Management

    func saveAPIKey(_ key: String) throws {
        guard let data = key.data(using: .utf8) else {
            throw NetworkError.encodingError
        }

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: Constants.Storage.keychainService,
            kSecAttrAccount as String: Constants.Storage.tmdbAPIKeyKey,
            kSecValueData as String: data
        ]

        SecItemDelete(query as CFDictionary)

        let status = SecItemAdd(query as CFDictionary, nil)

        guard status == errSecSuccess else {
            throw NSError(
                domain: "TMDBService",
                code: Int(status),
                userInfo: [NSLocalizedDescriptionKey: "Failed to save TMDB API key to Keychain"]
            )
        }
    }

    func getAPIKey() throws -> String {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: Constants.Storage.keychainService,
            kSecAttrAccount as String: Constants.Storage.tmdbAPIKeyKey,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess,
              let data = result as? Data,
              let key = String(data: data, encoding: .utf8) else {
            throw NetworkError.tmdbAPIKeyMissing
        }

        return key
    }

    func hasAPIKey() -> Bool {
        do {
            _ = try getAPIKey()
            return true
        } catch {
            return false
        }
    }

    // MARK: - Persistent Release Date Cache

    /// Cache key format: "normalizedTitle|year" -> "YYYY-MM-DD"
    private var releaseDateCache: [String: String] = [:]
    private var cacheLoaded = false

    /// Load the persistent release date cache from storage
    private func loadReleaseDateCache() async {
        guard !cacheLoaded else { return }
        do {
            if let cached: [String: String] = try await storageService.loadPreference(forKey: Constants.Storage.releaseDateCacheKey) {
                releaseDateCache = cached
                print("🎬 Loaded \(cached.count) release dates from persistent cache")
            }
        } catch {
            print("🎬 Failed to load release date cache: \(error)")
        }
        cacheLoaded = true
    }

    /// Save the release date cache to persistent storage
    private func saveReleaseDateCache() async {
        do {
            try await storageService.savePreference(releaseDateCache, forKey: Constants.Storage.releaseDateCacheKey)
        } catch {
            print("🎬 Failed to save release date cache: \(error)")
        }
    }

    /// Get cached release date for a movie
    func getCachedReleaseDate(for normalizedTitle: String, year: Int?) -> String? {
        let key = year != nil ? "\(normalizedTitle)|\(year!)" : normalizedTitle
        return releaseDateCache[key]
    }

    /// Store release date in cache
    private func cacheReleaseDate(_ date: String, for normalizedTitle: String, year: Int?) {
        let key = year != nil ? "\(normalizedTitle)|\(year!)" : normalizedTitle
        releaseDateCache[key] = date
    }

    // MARK: - Search for Individual Movies

    /// Search TMDB for a specific movie by title and year
    private func searchMovie(title: String, year: Int?) async throws -> TMDBMovie? {
        let apiKey = try getAPIKey()

        guard var urlComponents = URLComponents(string: Constants.TMDB.baseURL + Constants.TMDB.searchPath) else {
            throw NetworkError.invalidURL
        }

        var queryItems = [
            URLQueryItem(name: "api_key", value: apiKey),
            URLQueryItem(name: "query", value: title),
            URLQueryItem(name: "language", value: "en-US"),
            URLQueryItem(name: "page", value: "1")
        ]

        if let year = year {
            queryItems.append(URLQueryItem(name: "year", value: String(year)))
        }

        urlComponents.queryItems = queryItems

        guard let url = urlComponents.url else {
            throw NetworkError.invalidURL
        }

        let response: TMDBMovieListResponse = try await networkService.request(url: url)

        // Return the best match (first result, or match by year)
        if let exactMatch = response.results.first(where: { movie in
            guard let releaseDate = movie.releaseDate, releaseDate.count >= 4 else { return false }
            let movieYear = Int(String(releaseDate.prefix(4)))
            return movieYear == year
        }) {
            return exactMatch
        }

        return response.results.first
    }

    /// Fetch release dates for movies not already in cache (parallel fetching)
    /// Returns a dictionary of VODItem.id -> release date
    func fetchReleaseDatesForMovies(_ movies: [VODItem], progressCallback: ((Int, Int) -> Void)? = nil) async -> [String: String] {
        await loadReleaseDateCache()

        var results: [String: String] = [:]
        var moviesToFetch: [(VODItem, String, Int?)] = []  // (item, normalizedTitle, year)

        // Check which movies need fetching
        for movie in movies {
            let normalized = normalizeTitle(movie.name)
            let year = movie.releaseYear

            if let cachedDate = getCachedReleaseDate(for: normalized, year: year) {
                // Already have this one cached
                results[movie.id] = cachedDate
            } else {
                moviesToFetch.append((movie, normalized, year))
            }
        }

        print("🎬 Release dates: \(results.count) cached, \(moviesToFetch.count) to fetch")

        if moviesToFetch.isEmpty {
            return results
        }

        // Verify API key exists
        guard (try? getAPIKey()) != nil else {
            print("🎬 No API key, cannot fetch release dates")
            return results
        }

        let total = moviesToFetch.count
        let maxConcurrency = 10  // Safe concurrency level for TMDB (limit is ~50/sec)

        // Thread-safe counter for progress
        let progressCounter = ProgressCounter()

        // Process in batches with controlled concurrency
        let batches = stride(from: 0, to: moviesToFetch.count, by: maxConcurrency).map {
            Array(moviesToFetch[Swift.min($0, moviesToFetch.count)..<Swift.min($0 + maxConcurrency, moviesToFetch.count)])
        }

        print("🎬 Fetching \(total) movies in \(batches.count) batches (max \(maxConcurrency) concurrent)")

        for (batchIndex, batch) in batches.enumerated() {
            // Fetch batch in parallel
            let batchResults = await withTaskGroup(of: (String, String, String?, Int?)?.self) { group in
                for (movie, normalized, year) in batch {
                    group.addTask {
                        await self.fetchSingleMovieDate(movie: movie, normalized: normalized, year: year)
                    }
                }

                var batchResults: [(movieId: String, normalized: String, releaseDate: String?, year: Int?)] = []
                for await result in group {
                    if let result = result {
                        batchResults.append((result.0, result.1, result.2, result.3))
                    }

                    // Update progress
                    let current = await progressCounter.increment()
                    progressCallback?(current, total)
                }
                return batchResults
            }

            // Process batch results (update cache and results)
            for (movieId, normalized, releaseDate, year) in batchResults {
                if let date = releaseDate {
                    results[movieId] = date
                    cacheReleaseDate(date, for: normalized, year: year)
                }
            }

            // Small delay between batches to be respectful of rate limits
            if batchIndex < batches.count - 1 {
                try? await Task.sleep(nanoseconds: 200_000_000)  // 200ms between batches
            }
        }

        // Save updated cache
        await saveReleaseDateCache()
        print("🎬 Fetched release dates, cache now has \(releaseDateCache.count) entries")

        return results
    }

    /// Fetch release date for a single movie (used in parallel fetching)
    private func fetchSingleMovieDate(movie: VODItem, normalized: String, year: Int?, retryCount: Int = 0) async -> (String, String, String?, Int?)? {
        do {
            // Extract clean title for search (remove year suffix) using cached regexes
            var searchTitle = movie.name
            let nameRange = NSRange(searchTitle.startIndex..., in: searchTitle)
            if let regex = TMDBTitleRegex.yearWithDash {
                searchTitle = regex.stringByReplacingMatches(in: searchTitle, range: nameRange, withTemplate: "")
            }
            let updatedRange = NSRange(searchTitle.startIndex..., in: searchTitle)
            if let regex = TMDBTitleRegex.fourKPrefix {
                searchTitle = regex.stringByReplacingMatches(in: searchTitle, range: updatedRange, withTemplate: "")
            }
            searchTitle = searchTitle.trimmingCharacters(in: .whitespaces)

            if let tmdbMovie = try await searchMovie(title: searchTitle, year: year),
               let releaseDate = tmdbMovie.releaseDate {
                return (movie.id, normalized, releaseDate, year)
            }
            return (movie.id, normalized, nil, year)
        } catch let error as NetworkError {
            // Handle rate limiting (429) with retry
            if case .rateLimited = error, retryCount < 3 {
                print("🎬 Rate limited for '\(movie.name)', retrying in \(retryCount + 1) seconds...")
                try? await Task.sleep(nanoseconds: UInt64((retryCount + 1) * 1_000_000_000))
                return await fetchSingleMovieDate(movie: movie, normalized: normalized, year: year, retryCount: retryCount + 1)
            }
            print("🎬 Failed to fetch date for '\(movie.name)': \(error)")
            return (movie.id, normalized, nil, year)
        } catch {
            print("🎬 Failed to fetch date for '\(movie.name)': \(error)")
            return (movie.id, normalized, nil, year)
        }
    }

    // MARK: - Fetch Recent Releases

    func fetchRecentReleases(forceRefresh: Bool = false) async throws -> [TMDBMovie] {
        print("🎬 fetchRecentReleases called (forceRefresh: \(forceRefresh))")

        // Check memory cache
        if !forceRefresh, let cached = cachedMovies, let timestamp = cacheTimestamp {
            if Date().timeIntervalSince(timestamp) < Constants.TMDB.cacheTTL {
                print("🎬 Using memory cache (\(cached.count) movies)")
                return cached
            }
        }

        // Check disk cache
        if !forceRefresh, let diskCache = await loadFromDiskCache() {
            if !diskCache.isStale {
                print("🎬 Using disk cache (\(diskCache.movies.count) movies)")
                cachedMovies = diskCache.movies
                cacheTimestamp = diskCache.cachedAt
                return diskCache.movies
            } else {
                print("🎬 Disk cache is stale, fetching fresh data")
            }
        } else {
            print("🎬 No disk cache found, fetching fresh data")
        }

        // Fetch fresh data from TMDB API
        print("🎬 Calling TMDB API...")
        let movies = try await fetchFromAPI()

        // Update caches
        cachedMovies = movies
        cacheTimestamp = Date()
        await saveToDiskCache(TMDBCache(movies: movies, cachedAt: Date()))

        return movies
    }

    private func fetchFromAPI() async throws -> [TMDBMovie] {
        let apiKey = try getAPIKey()

        // Calculate date range: last 90 days for recent digital releases
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let today = Date()
        let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: today) ?? today

        var allMovies: [TMDBMovie] = []
        let maxPages = 10  // Fetch up to 10 pages (200 movies)

        for page in 1...maxPages {
            guard var urlComponents = URLComponents(string: Constants.TMDB.baseURL + Constants.TMDB.discoverPath) else {
                throw NetworkError.invalidURL
            }

            urlComponents.queryItems = [
                URLQueryItem(name: "api_key", value: apiKey),
                URLQueryItem(name: "language", value: "en-US"),
                URLQueryItem(name: "region", value: "US"),
                URLQueryItem(name: "sort_by", value: "release_date.desc"),
                URLQueryItem(name: "with_release_type", value: Constants.TMDB.digitalReleaseType),
                URLQueryItem(name: "release_date.gte", value: dateFormatter.string(from: ninetyDaysAgo)),
                URLQueryItem(name: "release_date.lte", value: dateFormatter.string(from: today)),
                URLQueryItem(name: "page", value: String(page))
            ]

            guard let url = urlComponents.url else {
                throw NetworkError.invalidURL
            }

            if page == 1 {
                print("🎬 TMDB API URL: \(url.absoluteString)")
            }

            do {
                let response: TMDBMovieListResponse = try await networkService.request(url: url)

                if page == 1 {
                    print("🎬 TMDB API returned \(response.results.count) movies on page 1, total available: \(response.totalResults)")
                }

                allMovies.append(contentsOf: response.results)

                // Stop if we've fetched all pages
                if page >= response.totalPages {
                    break
                }
            } catch let error as NetworkError {
                print("🎬 TMDB API NetworkError on page \(page): \(error)")
                throw error
            } catch {
                print("🎬 TMDB API Error on page \(page): \(error)")
                throw NetworkError.tmdbRequestFailed(error.localizedDescription)
            }
        }

        print("🎬 TMDB API fetched \(allMovies.count) total movies across pages")
        if !allMovies.isEmpty {
            print("🎬 Sample TMDB titles: \(allMovies.prefix(5).map { "\($0.title) (\($0.releaseDate ?? "no date"))" })")
        }

        return allMovies
    }

    // MARK: - Disk Cache

    private func loadFromDiskCache() async -> TMDBCache? {
        do {
            return try await storageService.loadPreference(forKey: Constants.Storage.tmdbCacheKey)
        } catch {
            return nil
        }
    }

    private func saveToDiskCache(_ cache: TMDBCache) async {
        do {
            try await storageService.savePreference(cache, forKey: Constants.Storage.tmdbCacheKey)
        } catch {
            // Silent failure for cache save
        }
    }

    func clearCache() async {
        cachedMovies = nil
        cacheTimestamp = nil
        await storageService.removePreference(forKey: Constants.Storage.tmdbCacheKey)
    }

    // MARK: - Title Matching

    /// Returns matched VOD items and a mapping of VODItem.id to TMDB release date
    func matchVODItems(_ vodItems: [VODItem], against tmdbMovies: [TMDBMovie]) -> (items: [VODItem], releaseDates: [String: String]) {
        // Create lookup from normalized title to TMDB movie (with year)
        var tmdbLookup: [String: TMDBMovie] = [:]
        for movie in tmdbMovies {
            let normalized = normalizeTitle(movie.title)
            // Extract year from TMDB release date (format: "YYYY-MM-DD")
            let year: Int? = movie.releaseDate.flatMap { dateStr in
                guard dateStr.count >= 4 else { return nil }
                return Int(String(dateStr.prefix(4)))
            }
            let key = year != nil ? "\(normalized)|\(year!)" : normalized
            tmdbLookup[key] = movie
            // Also store without year for fallback matching
            if tmdbLookup[normalized] == nil {
                tmdbLookup[normalized] = movie
            }
        }

        print("🎬 TMDB lookup keys (sample): \(Array(tmdbLookup.keys.prefix(10)))")

        var matchedItems: [VODItem] = []
        var matchedIds: Set<String> = []  // Prevent duplicates
        var releaseDates: [String: String] = [:]

        // Sample some VOD normalized names for debugging
        let sampleVODNames = vodItems.prefix(5).map { "\($0.name) -> \(normalizeTitle($0.name)) (year: \($0.releaseYear ?? 0))" }
        print("🎬 Sample VOD items: \(sampleVODNames)")

        for vodItem in vodItems {
            // Skip if already matched (prevent duplicates)
            guard !matchedIds.contains(vodItem.id) else { continue }

            let normalizedName = normalizeTitle(vodItem.name)
            let vodYear = vodItem.releaseYear

            // Try to match with year first (more precise)
            var tmdbMovie: TMDBMovie? = nil
            if let year = vodYear {
                // Try exact year match
                tmdbMovie = tmdbLookup["\(normalizedName)|\(year)"]
                // Try year +/- 1 (release dates can differ by region)
                if tmdbMovie == nil {
                    tmdbMovie = tmdbLookup["\(normalizedName)|\(year + 1)"]
                }
                if tmdbMovie == nil {
                    tmdbMovie = tmdbLookup["\(normalizedName)|\(year - 1)"]
                }
            }

            // Only use title-only match if VOD has no year
            if tmdbMovie == nil && vodYear == nil {
                tmdbMovie = tmdbLookup[normalizedName]
            }

            if let movie = tmdbMovie {
                print("🎬 MATCH: '\(vodItem.name)' matched '\(movie.title)' (\(movie.releaseDate ?? "no date"))")
                matchedItems.append(vodItem)
                matchedIds.insert(vodItem.id)
                if let releaseDate = movie.releaseDate {
                    releaseDates[vodItem.id] = releaseDate
                }
            }
        }

        return (matchedItems, releaseDates)
    }

    private func normalizeTitle(_ title: String) -> String {
        var normalized = title.lowercased()

        // Remove common articles at the beginning
        for article in ["the ", "a ", "an "] {
            if normalized.hasPrefix(article) {
                normalized = String(normalized.dropFirst(article.count))
            }
        }

        // Remove year in parentheses, e.g., "(2024)" using cached regex
        if let regex = TMDBTitleRegex.yearInParens {
            let range = NSRange(normalized.startIndex..., in: normalized)
            normalized = regex.stringByReplacingMatches(in: normalized, range: range, withTemplate: " ")
        }

        // Remove year with dash, e.g., "- 2024" or "- 2025" using cached regex
        if let regex = TMDBTitleRegex.yearWithDash {
            let range = NSRange(normalized.startIndex..., in: normalized)
            normalized = regex.stringByReplacingMatches(in: normalized, range: range, withTemplate: "")
        }

        // Remove standalone year at end, e.g., "Movie 2024" using cached regex
        if let regex = TMDBTitleRegex.standaloneYear {
            let range = NSRange(normalized.startIndex..., in: normalized)
            normalized = regex.stringByReplacingMatches(in: normalized, range: range, withTemplate: "")
        }

        // Remove quality indicators
        let qualityIndicators = ["4k", "uhd", "hd", "1080p", "720p", "480p", "bluray", "blu-ray", "webrip", "web-dl", "hdrip", "dvdrip", "brrip"]
        for indicator in qualityIndicators {
            normalized = normalized.replacingOccurrences(of: indicator, with: "")
        }

        // Remove punctuation except spaces
        normalized = normalized.components(separatedBy: CharacterSet.alphanumerics.union(.whitespaces).inverted).joined()

        // Collapse multiple spaces and trim
        normalized = normalized.components(separatedBy: .whitespaces)
            .filter { !$0.isEmpty }
            .joined(separator: " ")
            .trimmingCharacters(in: .whitespaces)

        return normalized
    }

    // MARK: - Combined Fetch and Match

    func getRecentReleases(from vodItems: [VODItem], forceRefresh: Bool = false) async throws -> (items: [VODItem], releaseDates: [String: String]) {
        print("🎬 TMDBService: Fetching recent releases...")
        let tmdbMovies = try await fetchRecentReleases(forceRefresh: forceRefresh)
        print("🎬 TMDBService: Got \(tmdbMovies.count) movies from TMDB")

        if !tmdbMovies.isEmpty {
            print("🎬 TMDBService: Sample TMDB titles: \(tmdbMovies.prefix(5).map { $0.title })")
        }

        let (matchedItems, releaseDates) = matchVODItems(vodItems, against: tmdbMovies)
        print("🎬 TMDBService: Matched \(matchedItems.count) items from \(vodItems.count) VOD items")

        // Limit to max total count
        let limitedItems = Array(matchedItems.prefix(Constants.TMDB.maxTotalCount))

        // Filter release dates to only include items we're returning
        let limitedIds = Set(limitedItems.map { $0.id })
        let filteredReleaseDates = releaseDates.filter { limitedIds.contains($0.key) }

        return (limitedItems, filteredReleaseDates)
    }

    // MARK: - TV Show Methods

    // Memory cache for trending TV shows
    private var cachedTVShows: [TMDBTVShow]?
    private var tvCacheTimestamp: Date?

    // Persistent cache for TV show air dates
    private var tvAirDateCache: [String: String] = [:]  // showName -> lastAirDate
    private var tvAirDateCacheLoaded = false

    /// Load the persistent TV air date cache from storage
    private func loadTVAirDateCache() async {
        guard !tvAirDateCacheLoaded else { return }
        do {
            if let cached: [String: String] = try await storageService.loadPreference(forKey: Constants.Storage.tvShowAirDatesKey) {
                tvAirDateCache = cached
                print("📺 Loaded \(cached.count) TV air dates from persistent cache")
            }
        } catch {
            print("📺 Failed to load TV air date cache: \(error)")
        }
        tvAirDateCacheLoaded = true
    }

    /// Save the TV air date cache to persistent storage
    private func saveTVAirDateCache() async {
        do {
            try await storageService.savePreference(tvAirDateCache, forKey: Constants.Storage.tvShowAirDatesKey)
        } catch {
            print("📺 Failed to save TV air date cache: \(error)")
        }
    }

    /// Fetch trending TV shows from TMDB
    func fetchTrendingTVShows(forceRefresh: Bool = false) async throws -> [TMDBTVShow] {
        print("📺 fetchTrendingTVShows called (forceRefresh: \(forceRefresh))")

        // Check memory cache
        if !forceRefresh, let cached = cachedTVShows, let timestamp = tvCacheTimestamp {
            if Date().timeIntervalSince(timestamp) < Constants.TMDB.cacheTTL {
                print("📺 Using memory cache (\(cached.count) shows)")
                return cached
            }
        }

        // Check disk cache
        if !forceRefresh, let diskCache = await loadTVFromDiskCache() {
            if !diskCache.isStale {
                print("📺 Using disk cache (\(diskCache.shows.count) shows)")
                cachedTVShows = diskCache.shows
                tvCacheTimestamp = diskCache.cachedAt
                return diskCache.shows
            } else {
                print("📺 Disk cache is stale, fetching fresh data")
            }
        } else {
            print("📺 No disk cache found, fetching fresh data")
        }

        // Fetch fresh data from TMDB API
        print("📺 Calling TMDB API for trending TV...")
        let shows = try await fetchTrendingFromAPI()

        // Update caches
        cachedTVShows = shows
        tvCacheTimestamp = Date()
        await saveTVToDiskCache(TMDBTVCache(shows: shows, cachedAt: Date()))

        return shows
    }

    private func fetchTrendingFromAPI() async throws -> [TMDBTVShow] {
        let apiKey = try getAPIKey()

        guard var urlComponents = URLComponents(string: Constants.TMDB.baseURL + Constants.TMDB.tvTrendingPath) else {
            throw NetworkError.invalidURL
        }

        urlComponents.queryItems = [
            URLQueryItem(name: "api_key", value: apiKey),
            URLQueryItem(name: "language", value: "en-US")
        ]

        guard let url = urlComponents.url else {
            throw NetworkError.invalidURL
        }

        print("📺 TMDB TV Trending URL: \(url.absoluteString)")

        let response: TMDBTVListResponse = try await networkService.request(url: url)
        print("📺 TMDB API returned \(response.results.count) trending TV shows")

        return response.results
    }

    // Debug counter for limiting output
    private var debugPrintCount = 0

    /// Fetch TV show details by ID to get last_air_date
    func fetchTVShowDetails(id: Int) async throws -> TMDBTVShow {
        let apiKey = try getAPIKey()

        guard var urlComponents = URLComponents(string: Constants.TMDB.baseURL + Constants.TMDB.tvDetailsPath + "/\(id)") else {
            throw NetworkError.invalidURL
        }

        urlComponents.queryItems = [
            URLQueryItem(name: "api_key", value: apiKey),
            URLQueryItem(name: "language", value: "en-US")
        ]

        guard let url = urlComponents.url else {
            throw NetworkError.invalidURL
        }

        // Debug: Fetch raw data first to see what we're getting
        let (data, _) = try await URLSession.shared.data(from: url)
        if let jsonString = String(data: data, encoding: .utf8) {
            // Print first 3 responses for debugging
            if debugPrintCount < 3 {
                debugPrintCount += 1
                print("📺 DEBUG: TV details response for ID \(id):")
                print("📺 DEBUG: \(jsonString.prefix(800))")
            }
        }

        let decoder = JSONDecoder()
        let response = try decoder.decode(TMDBTVShow.self, from: data)
        return response
    }

    /// Search for a TV show by title
    func searchTVShow(title: String) async throws -> TMDBTVShow? {
        let apiKey = try getAPIKey()

        guard var urlComponents = URLComponents(string: Constants.TMDB.baseURL + Constants.TMDB.tvSearchPath) else {
            throw NetworkError.invalidURL
        }

        urlComponents.queryItems = [
            URLQueryItem(name: "api_key", value: apiKey),
            URLQueryItem(name: "query", value: title),
            URLQueryItem(name: "language", value: "en-US"),
            URLQueryItem(name: "page", value: "1")
        ]

        guard let url = urlComponents.url else {
            throw NetworkError.invalidURL
        }

        let response: TMDBTVListResponse = try await networkService.request(url: url)
        return response.results.first
    }

    /// Fetch air dates for shows (batch with caching)
    func fetchAirDatesForShows(_ shows: [TVShow]) async -> [String: String] {
        await loadTVAirDateCache()

        var results: [String: String] = [:]
        var showsToFetch: [(TVShow, String)] = []  // (show, normalizedName)

        // Check which shows need fetching
        for show in shows {
            let normalized = normalizeShowTitle(show.name)

            if let cachedDate = tvAirDateCache[normalized] {
                results[show.name] = cachedDate
            } else {
                showsToFetch.append((show, normalized))
            }
        }

        print("📺 Air dates: \(results.count) cached, \(showsToFetch.count) to fetch")

        if showsToFetch.isEmpty {
            return results
        }

        // Verify API key exists
        guard (try? getAPIKey()) != nil else {
            print("📺 No API key, cannot fetch air dates")
            return results
        }

        let maxConcurrency = 10
        let batches = stride(from: 0, to: showsToFetch.count, by: maxConcurrency).map {
            Array(showsToFetch[Swift.min($0, showsToFetch.count)..<Swift.min($0 + maxConcurrency, showsToFetch.count)])
        }

        print("📺 Fetching \(showsToFetch.count) shows in \(batches.count) batches")

        for (batchIndex, batch) in batches.enumerated() {
            let batchResults = await withTaskGroup(of: (String, String, String?)?.self) { group in
                for (show, normalized) in batch {
                    group.addTask {
                        await self.fetchSingleShowAirDate(show: show, normalized: normalized)
                    }
                }

                var batchResults: [(showName: String, normalized: String, airDate: String?)] = []
                for await result in group {
                    if let result = result {
                        batchResults.append((result.0, result.1, result.2))
                    }
                }
                return batchResults
            }

            // Process batch results
            for (showName, normalized, airDate) in batchResults {
                if let date = airDate {
                    results[showName] = date
                    tvAirDateCache[normalized] = date
                }
            }

            // Small delay between batches
            if batchIndex < batches.count - 1 {
                try? await Task.sleep(nanoseconds: 200_000_000)  // 200ms
            }
        }

        // Save updated cache
        await saveTVAirDateCache()
        print("📺 Fetched air dates, cache now has \(tvAirDateCache.count) entries")

        return results
    }

    /// Fetch air date for a single show
    private func fetchSingleShowAirDate(show: TVShow, normalized: String) async -> (String, String, String?)? {
        do {
            if let tmdbShow = try await searchTVShow(title: show.name) {
                // Fetch details to get last_air_date
                let details = try await fetchTVShowDetails(id: tmdbShow.id)
                if let airDate = details.lastAirDate {
                    print("📺 ✓ '\(show.name)' -> last_air_date: \(airDate)")
                } else {
                    print("📺 ✗ '\(show.name)' -> no last_air_date in response")
                }
                return (show.name, normalized, details.lastAirDate)
            } else {
                print("📺 ✗ '\(show.name)' -> not found on TMDB")
            }
            return (show.name, normalized, nil)
        } catch {
            print("📺 ✗ '\(show.name)': \(error)")
            return (show.name, normalized, nil)
        }
    }

    /// Normalize show title for matching
    private func normalizeShowTitle(_ title: String) -> String {
        var normalized = title.lowercased()

        // Remove common articles at the beginning
        for article in ["the ", "a ", "an "] {
            if normalized.hasPrefix(article) {
                normalized = String(normalized.dropFirst(article.count))
            }
        }

        // Remove punctuation except spaces
        normalized = normalized.components(separatedBy: CharacterSet.alphanumerics.union(.whitespaces).inverted).joined()

        // Collapse multiple spaces and trim
        normalized = normalized.components(separatedBy: .whitespaces)
            .filter { !$0.isEmpty }
            .joined(separator: " ")
            .trimmingCharacters(in: .whitespaces)

        return normalized
    }

    // MARK: - TV Disk Cache

    private func loadTVFromDiskCache() async -> TMDBTVCache? {
        do {
            return try await storageService.loadPreference(forKey: Constants.Storage.tmdbTVTrendingCacheKey)
        } catch {
            return nil
        }
    }

    private func saveTVToDiskCache(_ cache: TMDBTVCache) async {
        do {
            try await storageService.savePreference(cache, forKey: Constants.Storage.tmdbTVTrendingCacheKey)
        } catch {
            // Silent failure for cache save
        }
    }
}
