//
//  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()

    private let container: CKContainer
    private let publicDatabase: CKDatabase

    private init() {
        self.container = CKContainer(identifier: "iCloud.com.sportstime.app")
        self.publicDatabase = container.publicCloudDatabase
    }

    // 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()
        return status == .available
    }

    func checkAvailabilityWithError() async throws {
        let status = await checkAccountStatus()
        switch status {
        case .available:
            return
        case .noAccount:
            throw CloudKitError.notSignedIn
        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
    /// - Parameter lastSync: If nil, fetches all stadiums. If provided, fetches only stadiums modified since that date.
    func fetchStadiumsForSync(since lastSync: Date?) async throws -> [SyncStadium] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate)

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

        return results.compactMap { result -> SyncStadium? in
            guard case .success(let record) = result.1 else { return nil }
            let ckStadium = CKStadium(record: record)
            guard let stadium = ckStadium.stadium,
                  let canonicalId = ckStadium.canonicalId
            else { return nil }
            return SyncStadium(stadium: stadium, canonicalId: canonicalId)
        }
    }

    /// Fetch teams for sync operations
    /// - Parameter lastSync: If nil, fetches all teams. If provided, fetches only teams modified since that date.
    func fetchTeamsForSync(since lastSync: Date?) async throws -> [SyncTeam] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.team, predicate: predicate)

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

        return results.compactMap { result -> SyncTeam? in
            guard case .success(let record) = result.1 else { return nil }
            let ckTeam = CKTeam(record: record)
            guard let team = ckTeam.team,
                  let canonicalId = ckTeam.canonicalId,
                  let stadiumCanonicalId = ckTeam.stadiumCanonicalId
            else { return nil }
            return SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId)
        }
    }

    /// Fetch games for sync operations
    /// - Parameter lastSync: If nil, fetches all games. If provided, fetches only games modified since that date.
    func fetchGamesForSync(since lastSync: Date?) async throws -> [SyncGame] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }
        let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)

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

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

            guard let canonicalId = ckGame.canonicalId,
                  let homeTeamCanonicalId = ckGame.homeTeamCanonicalId,
                  let awayTeamCanonicalId = ckGame.awayTeamCanonicalId,
                  let stadiumCanonicalId = ckGame.stadiumCanonicalId
            else { return nil }

            guard let game = ckGame.game(
                homeTeamId: homeTeamCanonicalId,
                awayTeamId: awayTeamCanonicalId,
                stadiumId: stadiumCanonicalId
            ) else { return nil }

            return SyncGame(
                game: game,
                canonicalId: canonicalId,
                homeTeamCanonicalId: homeTeamCanonicalId,
                awayTeamCanonicalId: awayTeamCanonicalId,
                stadiumCanonicalId: stadiumCanonicalId
            )
        }.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
    func fetchLeagueStructureChanges(since lastSync: Date?) async throws -> [LeagueStructureModel] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.leagueStructure, predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: CKLeagueStructure.lastModifiedKey, 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()
        }
    }

    /// Fetch team alias records modified after the given date
    func fetchTeamAliasChanges(since lastSync: Date?) async throws -> [TeamAlias] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.teamAlias, predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: CKTeamAlias.lastModifiedKey, 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 CKTeamAlias(record: record).toModel()
        }
    }

    /// Fetch stadium alias records modified after the given date
    func fetchStadiumAliasChanges(since lastSync: Date?) async throws -> [StadiumAlias] {
        let predicate: NSPredicate
        if let lastSync = lastSync {
            predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
        } else {
            predicate = NSPredicate(value: true)
        }

        let query = CKQuery(recordType: CKRecordType.stadiumAlias, predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: CKStadiumAlias.lastModifiedKey, 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 CKStadiumAlias(record: record).toModel()
        }
    }

    // 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: "game-updates",
            options: [.firesOnRecordCreation, .firesOnRecordUpdate]
        )

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

        try await publicDatabase.save(subscription)
    }

    func subscribeToLeagueStructureUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.leagueStructure,
            predicate: NSPredicate(value: true),
            subscriptionID: "league-structure-updates",
            options: [.firesOnRecordCreation, .firesOnRecordUpdate]
        )

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

        try await publicDatabase.save(subscription)
    }

    func subscribeToTeamAliasUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.teamAlias,
            predicate: NSPredicate(value: true),
            subscriptionID: "team-alias-updates",
            options: [.firesOnRecordCreation, .firesOnRecordUpdate]
        )

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

        try await publicDatabase.save(subscription)
    }

    func subscribeToStadiumAliasUpdates() async throws {
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.stadiumAlias,
            predicate: NSPredicate(value: true),
            subscriptionID: "stadium-alias-updates",
            options: [.firesOnRecordCreation, .firesOnRecordUpdate]
        )

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

        try await publicDatabase.save(subscription)
    }

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