//
//  CloudKitService.swift
//  SportsTime
//

import Foundation
import CloudKit

// MARK: - CloudKit Errors

enum CloudKitError: Error, LocalizedError {
    case notSignedIn
    case networkUnavailable
    case serverError(String)
    case quotaExceeded
    case permissionDenied
    case recordNotFound
    case unknown(Error)

    var errorDescription: String? {
        switch self {
        case .notSignedIn:
            return "Please sign in to iCloud in Settings to sync data."
        case .networkUnavailable:
            return "Unable to connect to the server. Check your internet connection."
        case .serverError(let message):
            return "Server error: \(message)"
        case .quotaExceeded:
            return "iCloud storage quota exceeded."
        case .permissionDenied:
            return "Permission denied. Check your iCloud settings."
        case .recordNotFound:
            return "Data not found."
        case .unknown(let error):
            return "An unexpected error occurred: \(error.localizedDescription)"
        }
    }

    static func from(_ error: Error) -> CloudKitError {
        if let ckError = error as? CKError {
            switch ckError.code {
            case .notAuthenticated:
                return .notSignedIn
            case .networkUnavailable, .networkFailure:
                return .networkUnavailable
            case .serverResponseLost:
                return .serverError("Connection lost")
            case .quotaExceeded:
                return .quotaExceeded
            case .permissionFailure:
                return .permissionDenied
            case .unknownItem:
                return .recordNotFound
            default:
                return .serverError(ckError.localizedDescription)
            }
        }
        return .unknown(error)
    }
}

actor CloudKitService {
    static let shared = CloudKitService()

    nonisolated static let gameUpdatesSubscriptionID = "game-updates"
    nonisolated static let teamUpdatesSubscriptionID = "team-updates"
    nonisolated static let stadiumUpdatesSubscriptionID = "stadium-updates"
    nonisolated static let leagueStructureUpdatesSubscriptionID = "league-structure-updates"
    nonisolated static let teamAliasUpdatesSubscriptionID = "team-alias-updates"
    nonisolated static let stadiumAliasUpdatesSubscriptionID = "stadium-alias-updates"
    nonisolated static let sportUpdatesSubscriptionID = "sport-updates"
    nonisolated static let canonicalSubscriptionIDs: Set<String> = [
        gameUpdatesSubscriptionID,
        teamUpdatesSubscriptionID,
        stadiumUpdatesSubscriptionID,
        leagueStructureUpdatesSubscriptionID,
        teamAliasUpdatesSubscriptionID,
        stadiumAliasUpdatesSubscriptionID,
        sportUpdatesSubscriptionID
    ]

    nonisolated static func recordType(forSubscriptionID subscriptionID: String) -> String? {
        switch subscriptionID {
        case gameUpdatesSubscriptionID:
            return "Game"
        case teamUpdatesSubscriptionID:
            return "Team"
        case stadiumUpdatesSubscriptionID:
            return "Stadium"
        case leagueStructureUpdatesSubscriptionID:
            return "LeagueStructure"
        case teamAliasUpdatesSubscriptionID:
            return "TeamAlias"
        case stadiumAliasUpdatesSubscriptionID:
            return "StadiumAlias"
        case sportUpdatesSubscriptionID:
            return "Sport"
        default:
            return nil
        }
    }

    private let container: CKContainer
    private let publicDatabase: CKDatabase

    /// Maximum records per CloudKit query (400 is the default limit)
    private let recordsPerPage = 400
    /// Re-fetch a small overlap window to avoid missing updates around sync boundaries.
    private let deltaOverlapSeconds: TimeInterval = 120

    private init() {
        self.container = CloudKitContainerConfig.makeContainer()
        self.publicDatabase = container.publicCloudDatabase
    }

    // MARK: - Pagination Helper

    /// Fetches all records matching a query using cursor-based pagination.
    /// Checks cancellation token between pages to allow graceful interruption.
    private func fetchAllRecords(
        matching query: CKQuery,
        cancellationToken: SyncCancellationToken?
    ) async throws -> [CKRecord] {
        var allRecords: [CKRecord] = []
        var cursor: CKQueryOperation.Cursor?
        var partialFailureCount = 0
        var firstPartialFailure: Error?

        // First page
        let (firstResults, firstCursor) = try await publicDatabase.records(
            matching: query,
            resultsLimit: recordsPerPage
        )

        for result in firstResults {
            switch result.1 {
            case .success(let record):
                allRecords.append(record)
            case .failure(let error):
                partialFailureCount += 1
                if firstPartialFailure == nil {
                    firstPartialFailure = error
                }
            }
        }
        cursor = firstCursor

        // Continue fetching pages while cursor exists
        while let currentCursor = cursor {
            // Check cancellation between pages
            if cancellationToken?.isCancelled == true {
                throw CancellationError()
            }

            let (results, nextCursor) = try await publicDatabase.records(
                continuingMatchFrom: currentCursor,
                resultsLimit: recordsPerPage
            )

            for result in results {
                switch result.1 {
                case .success(let record):
                    allRecords.append(record)
                case .failure(let error):
                    partialFailureCount += 1
                    if firstPartialFailure == nil {
                        firstPartialFailure = error
                    }
                }
            }
            cursor = nextCursor
        }

        if partialFailureCount > 0 {
            SyncLogger.shared.log("⚠️ [CK] \(query.recordType) query had \(partialFailureCount) per-record failures")
            if allRecords.isEmpty, let firstPartialFailure {
                throw firstPartialFailure
            }
        }

        return allRecords
    }

    /// Normalizes a stored sync timestamp into a safe CloudKit delta start.
    /// - Returns: `nil` when a full sync should be used.
    private func effectiveDeltaStartDate(_ lastSync: Date?) -> Date? {
        guard let lastSync else { return nil }

        let now = Date()
        if lastSync > now.addingTimeInterval(60) {
            SyncLogger.shared.log("⚠️ [CK] Last sync timestamp is in the future; falling back to full sync")
            return nil
        }

        return lastSync.addingTimeInterval(-deltaOverlapSeconds)
    }

    /// Fails fast when CloudKit returned records but none were parseable.
    private func throwIfAllDropped(recordType: String, totalRecords: Int, parsedRecords: Int) throws {
        guard totalRecords > 0, parsedRecords == 0 else { return }
        let message = "All \(recordType) records were unparseable (\(totalRecords) fetched)"
        SyncLogger.shared.log("❌ [CK] \(message)")
        throw CloudKitError.serverError(message)
    }

    // MARK: - Sync Types (include canonical IDs from CloudKit)

    struct SyncStadium {
        let stadium: Stadium
        let canonicalId: String
    }

    struct SyncTeam {
        let team: Team
        let canonicalId: String
        let stadiumCanonicalId: String?
    }

    struct SyncGame {
        let game: Game
        let canonicalId: String
        let homeTeamCanonicalId: String
        let awayTeamCanonicalId: String
        let stadiumCanonicalId: String?
    }

    // MARK: - Availability Check

    func isAvailable() async -> Bool {
        let status = await checkAccountStatus()
        switch status {
        case .available, .noAccount, .couldNotDetermine:
            // Public DB reads should still be attempted without an iCloud account.
            return true
        case .restricted, .temporarilyUnavailable:
            return false
        @unknown default:
            return false
        }
    }

    func checkAvailabilityWithError() async throws {
        let status = await checkAccountStatus()
        switch status {
        case .available, .noAccount:
            return
        case .restricted:
            throw CloudKitError.permissionDenied
        case .couldNotDetermine:
            throw CloudKitError.networkUnavailable
        case .temporarilyUnavailable:
            throw CloudKitError.networkUnavailable
        @unknown default:
            throw CloudKitError.networkUnavailable
        }
    }

    // MARK: - Fetch Operations

    func fetchTeams(for sport: Sport) async throws -> [Team] {
        let predicate = NSPredicate(format: "sport == %@", sport.rawValue)
        let query = CKQuery(recordType: CKRecordType.team, predicate: predicate)

        let (results, _) = try await publicDatabase.records(matching: query)

        return results.compactMap { result in
            guard case .success(let record) = result.1 else { return nil }
            return CKTeam(record: record).team
        }
    }

    func fetchStadiums() async throws -> [Stadium] {
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate)

        let (results, _) = try await publicDatabase.records(matching: query)

        return results.compactMap { result in
            guard case .success(let record) = result.1 else { return nil }
            return CKStadium(record: record).stadium
        }
    }

    func fetchGames(
        sports: Set<Sport>,
        startDate: Date,
        endDate: Date
    ) async throws -> [Game] {
        var allGames: [Game] = []

        for sport in sports {
            let predicate = NSPredicate(
                format: "sport == %@ AND dateTime >= %@ AND dateTime <= %@",
                sport.rawValue,
                startDate as NSDate,
                endDate as NSDate
            )
            let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)

            let (results, _) = try await publicDatabase.records(matching: query)

            let games = results.compactMap { result -> Game? in
                guard case .success(let record) = result.1 else { return nil }
                let ckGame = CKGame(record: record)

                guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
                      let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference
                else { return nil }

                let homeId = homeRef.recordID.recordName
                let awayId = awayRef.recordID.recordName

                // Stadium ref is optional - use placeholder if not present
                let stadiumId: String
                if let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference {
                    stadiumId = stadiumRef.recordID.recordName
                } else {
                    stadiumId = "stadium_placeholder_\(UUID().uuidString)" // Placeholder - will be resolved via team lookup
                }

                return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
            }

            allGames.append(contentsOf: games)
        }

        return allGames.sorted { $0.dateTime < $1.dateTime }
    }

    func fetchGame(by id: String) async throws -> Game? {
        let predicate = NSPredicate(format: "gameId == %@", id)
        let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)

        let (results, _) = try await publicDatabase.records(matching: query)

        guard let result = results.first,
              case .success(let record) = result.1 else { return nil }

        let ckGame = CKGame(record: record)

        guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
              let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference,
              let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference
        else { return nil }

        let homeId = homeRef.recordID.recordName
        let awayId = awayRef.recordID.recordName
        let stadiumId = stadiumRef.recordID.recordName

        return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
    }

    // MARK: - Sync Fetch Methods (return canonical IDs directly from CloudKit)

    /// Fetch stadiums for sync operations
    /// - Parameters:
    ///   - lastSync: If nil, fetches all stadiums. If provided, fetches only stadiums modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchStadiumsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncStadium] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching stadiums modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL stadiums (full sync)")
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate)

        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) stadium records from CloudKit")

        var validStadiums: [SyncStadium] = []
        var skipped = 0
        for record in records {
            let ckStadium = CKStadium(record: record)
            guard let stadium = ckStadium.stadium,
                  let canonicalId = ckStadium.canonicalId
            else {
                skipped += 1
                continue
            }
            validStadiums.append(SyncStadium(stadium: stadium, canonicalId: canonicalId))
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) stadium records due to missing required fields")
        }
        try throwIfAllDropped(recordType: CKRecordType.stadium, totalRecords: records.count, parsedRecords: validStadiums.count)

        return validStadiums
    }

    /// Fetch teams for sync operations
    /// - Parameters:
    ///   - lastSync: If nil, fetches all teams. If provided, fetches only teams modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchTeamsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncTeam] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching teams modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL teams (full sync)")
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.team, predicate: predicate)

        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) team records from CloudKit")

        var validTeams: [SyncTeam] = []
        var skipped = 0
        var missingStadiumRef = 0
        for record in records {
            let ckTeam = CKTeam(record: record)
            guard let team = ckTeam.team,
                  let canonicalId = ckTeam.canonicalId
            else {
                skipped += 1
                continue
            }
            let stadiumCanonicalId = ckTeam.stadiumCanonicalId
            if stadiumCanonicalId == nil {
                missingStadiumRef += 1
            }
            validTeams.append(SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId))
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) team records due to missing required fields")
        }
        if missingStadiumRef > 0 {
            log.log("⚠️ [CK] \(missingStadiumRef) team records are missing stadium refs; merge will preserve local stadiums when possible")
        }
        try throwIfAllDropped(recordType: CKRecordType.team, totalRecords: records.count, parsedRecords: validTeams.count)

        return validTeams
    }

    /// Fetch games for sync operations
    /// - Parameters:
    ///   - lastSync: If nil, fetches all games. If provided, fetches only games modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchGamesForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncGame] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching games modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL games (full sync)")
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)

        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) game records from CloudKit")

        var validGames: [SyncGame] = []
        var skippedMissingIds = 0
        var skippedInvalidGame = 0
        var missingStadiumRef = 0

        for record in records {
            let ckGame = CKGame(record: record)

            guard let canonicalId = ckGame.canonicalId,
                  let homeTeamCanonicalId = ckGame.homeTeamCanonicalId,
                  let awayTeamCanonicalId = ckGame.awayTeamCanonicalId
            else {
                skippedMissingIds += 1
                continue
            }

            let stadiumCanonicalId = ckGame.stadiumCanonicalId
            if stadiumCanonicalId == nil {
                missingStadiumRef += 1
            }

            guard let game = ckGame.game(
                homeTeamId: homeTeamCanonicalId,
                awayTeamId: awayTeamCanonicalId,
                stadiumId: stadiumCanonicalId ?? "stadium_placeholder_\(canonicalId)"
            ) else {
                skippedInvalidGame += 1
                continue
            }

            validGames.append(SyncGame(
                game: game,
                canonicalId: canonicalId,
                homeTeamCanonicalId: homeTeamCanonicalId,
                awayTeamCanonicalId: awayTeamCanonicalId,
                stadiumCanonicalId: stadiumCanonicalId
            ))
        }

        log.log("☁️ [CK] Parsed \(validGames.count) valid games (skipped: \(skippedMissingIds) missing IDs, \(skippedInvalidGame) invalid)")
        if missingStadiumRef > 0 {
            log.log("⚠️ [CK] \(missingStadiumRef) games are missing stadium refs; merge will derive stadiums from team mappings when possible")
        }
        try throwIfAllDropped(recordType: CKRecordType.game, totalRecords: records.count, parsedRecords: validGames.count)

        // Log sport breakdown
        var bySport: [String: Int] = [:]
        for g in validGames {
            bySport[g.game.sport.rawValue, default: 0] += 1
        }
        for (sport, count) in bySport.sorted(by: { $0.key < $1.key }) {
            log.log("☁️ [CK]   \(sport): \(count) games")
        }

        return validGames.sorted { $0.game.dateTime < $1.game.dateTime }
    }

    // MARK: - League Structure & Team Aliases

    func fetchLeagueStructure(for sport: Sport? = nil) async throws -> [LeagueStructureModel] {
        let predicate: NSPredicate
        if let sport = sport {
            predicate = NSPredicate(format: "sport == %@", sport.rawValue)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.leagueStructure, predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: CKLeagueStructure.displayOrderKey, ascending: true)]

        let (results, _) = try await publicDatabase.records(matching: query)

        return results.compactMap { result in
            guard case .success(let record) = result.1 else { return nil }
            return CKLeagueStructure(record: record).toModel()
        }
    }

    func fetchTeamAliases(for teamCanonicalId: String? = nil) async throws -> [TeamAlias] {
        let predicate: NSPredicate
        if let teamId = teamCanonicalId {
            predicate = NSPredicate(format: "teamCanonicalId == %@", teamId)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.teamAlias, predicate: predicate)

        let (results, _) = try await publicDatabase.records(matching: query)

        return results.compactMap { result in
            guard case .success(let record) = result.1 else { return nil }
            return CKTeamAlias(record: record).toModel()
        }
    }

    func fetchStadiumAliases(for stadiumCanonicalId: String? = nil) async throws -> [StadiumAlias] {
        let predicate: NSPredicate
        if let stadiumId = stadiumCanonicalId {
            predicate = NSPredicate(format: "stadiumCanonicalId == %@", stadiumId)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.stadiumAlias, predicate: predicate)

        let (results, _) = try await publicDatabase.records(matching: query)

        return results.compactMap { result in
            guard case .success(let record) = result.1 else { return nil }
            return CKStadiumAlias(record: record).toModel()
        }
    }

    // MARK: - Delta Sync (Date-Based for Public Database)

    /// Fetch league structure records modified after the given date
    /// - Parameters:
    ///   - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchLeagueStructureChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [LeagueStructureModel] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching league structures modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL league structures (full sync)")
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.leagueStructure, predicate: predicate)
        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) league structure records from CloudKit")

        var parsed: [LeagueStructureModel] = []
        var skipped = 0
        for record in records {
            if let model = CKLeagueStructure(record: record).toModel() {
                parsed.append(model)
            } else {
                skipped += 1
            }
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) league structure records due to missing required fields")
        }
        try throwIfAllDropped(recordType: CKRecordType.leagueStructure, totalRecords: records.count, parsedRecords: parsed.count)

        return parsed.sorted { lhs, rhs in
            lhs.lastModified < rhs.lastModified
        }
    }

    /// Fetch team alias records modified after the given date
    /// - Parameters:
    ///   - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchTeamAliasChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [TeamAlias] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching team aliases modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL team aliases (full sync)")
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.teamAlias, predicate: predicate)
        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) team alias records from CloudKit")

        var parsed: [TeamAlias] = []
        var skipped = 0
        for record in records {
            if let model = CKTeamAlias(record: record).toModel() {
                parsed.append(model)
            } else {
                skipped += 1
            }
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) team alias records due to missing required fields")
        }
        try throwIfAllDropped(recordType: CKRecordType.teamAlias, totalRecords: records.count, parsedRecords: parsed.count)

        return parsed.sorted { lhs, rhs in
            lhs.lastModified < rhs.lastModified
        }
    }

    /// Fetch stadium alias records modified after the given date
    /// - Parameters:
    ///   - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchStadiumAliasChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [StadiumAlias] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching stadium aliases modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL stadium aliases (full sync)")
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.stadiumAlias, predicate: predicate)
        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) stadium alias records from CloudKit")

        var parsed: [StadiumAlias] = []
        var skipped = 0
        for record in records {
            if let model = CKStadiumAlias(record: record).toModel() {
                parsed.append(model)
            } else {
                skipped += 1
            }
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) stadium alias records due to missing required fields")
        }
        try throwIfAllDropped(recordType: CKRecordType.stadiumAlias, totalRecords: records.count, parsedRecords: parsed.count)

        return parsed.sorted { lhs, rhs in
            lhs.lastModified < rhs.lastModified
        }
    }

    // MARK: - Sport Sync

    /// Fetch sports for sync operations
    /// - Parameters:
    ///   - lastSync: If nil, fetches all sports. If provided, fetches only sports modified since that date.
    ///   - cancellationToken: Optional token to check for cancellation between pages
    func fetchSportsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [CanonicalSport] {
        let log = SyncLogger.shared
        let predicate: NSPredicate
        if let deltaStart = effectiveDeltaStartDate(lastSync) {
            log.log("☁️ [CK] Fetching sports modified since \(deltaStart.formatted()) (overlap window applied)")
            predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
        } else {
            log.log("☁️ [CK] Fetching ALL sports (full sync)")
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.sport, predicate: predicate)

        let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
        log.log("☁️ [CK] Received \(records.count) sport records from CloudKit")

        var parsed: [CanonicalSport] = []
        var skipped = 0
        for record in records {
            if let sport = CKSport(record: record).toCanonical() {
                parsed.append(sport)
            } else {
                skipped += 1
            }
        }

        if skipped > 0 {
            log.log("⚠️ [CK] Skipped \(skipped) sport records due to missing required fields")
        }
        try throwIfAllDropped(recordType: CKRecordType.sport, totalRecords: records.count, parsedRecords: parsed.count)

        return parsed
    }

    // MARK: - Sync Status

    func checkAccountStatus() async -> CKAccountStatus {
        do {
            return try await container.accountStatus()
        } catch {
            return .couldNotDetermine
        }
    }

    // MARK: - Subscriptions

    func subscribeToScheduleUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.game,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.gameUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToTeamUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.team,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.teamUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToStadiumUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.stadium,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.stadiumUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToLeagueStructureUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.leagueStructure,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.leagueStructureUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToTeamAliasUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.teamAlias,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.teamAliasUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToStadiumAliasUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.stadiumAlias,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.stadiumAliasUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    func subscribeToSportUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.sport,
            predicate: NSPredicate(value: true),
            subscriptionID: Self.sportUpdatesSubscriptionID,
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await saveSubscriptionIfNeeded(subscription)
    }

    /// Subscribe to all canonical data updates
    func subscribeToAllUpdates() async throws {
        try await subscribeToScheduleUpdates()
        try await subscribeToTeamUpdates()
        try await subscribeToStadiumUpdates()
        try await subscribeToLeagueStructureUpdates()
        try await subscribeToTeamAliasUpdates()
        try await subscribeToStadiumAliasUpdates()
        try await subscribeToSportUpdates()
    }

    private func saveSubscriptionIfNeeded(_ subscription: CKQuerySubscription) async throws {
        do {
            try await publicDatabase.save(subscription)
        } catch let error as CKError where error.code == .serverRejectedRequest {
            // Existing subscriptions can be rejected as duplicates in some environments.
            // Confirm it exists before treating this as non-fatal.
            do {
                _ = try await publicDatabase.subscription(for: subscription.subscriptionID)
                SyncLogger.shared.log("ℹ️ [CK] Subscription already exists: \(subscription.subscriptionID)")
            } catch {
                throw error
            }
        }
    }
}
