import CloudKit
import Foundation
import UIKit

class ICloudSyncProvider: SyncProvider {
    // MARK: - Properties

    var type: SyncProviderType { .iCloud }

    var isConfigured: Bool {
        return true
    }

    var isConnected: Bool {
        get { userDefaults.bool(forKey: Keys.isConnected) }
        set { userDefaults.set(newValue, forKey: Keys.isConnected) }
    }

    var lastSyncTime: Date? {
        return userDefaults.object(forKey: Keys.lastSyncTime) as? Date
    }

    private let container = CKContainer.default()
    private lazy var database = container.privateCloudDatabase
    private let zoneID = CKRecordZone.ID(zoneName: "AscentlyZone", ownerName: CKCurrentUserDefaultName)

    private let userDefaults = UserDefaults.standard
    private let logTag = "ICloudSync"

    private enum Keys {
        static let serverChangeToken = "Ascently.ICloud.ServerChangeToken"
        static let lastSyncTime = "Ascently.ICloud.LastSyncTime"
        static let zoneCreated = "Ascently.ICloud.ZoneCreated"
        static let isConnected = "Ascently.ICloud.IsConnected"
    }

    // MARK: - Init

    init() {
        Task {
            try? await checkAccountStatus()
        }
    }

    // MARK: - SyncProvider Protocol

    func testConnection() async throws {
        try await checkAccountStatus()
    }

    func disconnect() {
        isConnected = false
    }

    func sync(dataManager: ClimbingDataManager) async throws {
        if !isConnected {
            try await checkAccountStatus()
            if !isConnected {
                throw SyncError.notConnected
            }
        }

        AppLogger.info("Starting iCloud sync", tag: logTag)

        do {
            try await createZoneIfNeeded()

            do {
                try await pullChanges(dataManager: dataManager)
            } catch {
                let errorString = String(describing: error)
                if errorString.contains("recordName") && errorString.contains("queryable") {
                    AppLogger.warning("Schema initialization detected. Skipping pull to attempt self-healing via push.", tag: logTag)
                } else {
                    throw error
                }
            }

            try await pushChanges(dataManager: dataManager)

            userDefaults.set(Date(), forKey: Keys.lastSyncTime)

            AppLogger.info("iCloud sync completed successfully", tag: logTag)
        } catch {
            AppLogger.error("iCloud sync failed: \(error.localizedDescription)", tag: logTag)
            throw error
        }
    }

    // MARK: - Private Methods

    private func checkAccountStatus() async throws {
        let status: CKAccountStatus
        do {
            status = try await container.accountStatus()
        } catch {
            AppLogger.error("Failed to get iCloud account status: \(error). This often indicates a mismatch between the App Bundle ID and the iCloud Container configuration in the Provisioning Profile.", tag: logTag)
            throw error
        }

        AppLogger.info("iCloud account status: \(status.rawValue)", tag: logTag)

        switch status {
        case .available:
            isConnected = true
        case .noAccount:
            isConnected = false
            throw SyncError.providerError("No iCloud account found. Please sign in to iCloud settings.")
        case .restricted:
            isConnected = false
            throw SyncError.providerError("iCloud access is restricted.")
        case .couldNotDetermine:
            isConnected = false
            throw SyncError.providerError("Could not determine iCloud account status.")
        case .temporarilyUnavailable:
            isConnected = false
            throw SyncError.providerError("iCloud is temporarily unavailable.")
        @unknown default:
            isConnected = false
            throw SyncError.providerError("Unknown iCloud error.")
        }
    }

    private func createZoneIfNeeded() async throws {
        if userDefaults.bool(forKey: Keys.zoneCreated) {
            return
        }

        do {
            let zone = CKRecordZone(zoneID: zoneID)
            try await database.save(zone)
            userDefaults.set(true, forKey: Keys.zoneCreated)
            AppLogger.info("Created custom record zone: \(zoneID.zoneName)", tag: logTag)
        } catch {
            // If zone already exists, that's fine
            if let ckError = error as? CKError {
                if ckError.code == .serverRecordChanged {
                    userDefaults.set(true, forKey: Keys.zoneCreated)
                    return
                }

                if ckError.code == .permissionFailure {
                    AppLogger.error("CloudKit Permission Failure. This usually indicates 'Invalid Bundle ID'. Please Clean Build Folder and ensure Provisioning Profile matches entitlements.", tag: logTag)
                }
            }
            throw error
        }
    }

    // MARK: - Pull (Fetch from CloudKit)

    private func pullChanges(dataManager: ClimbingDataManager) async throws {
        var previousToken: CKServerChangeToken? = nil
        if let tokenData = userDefaults.data(forKey: Keys.serverChangeToken) {
            previousToken = try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)
        }

        var changedRecords: [CKRecord] = []
        var deletedRecordIDs: [CKRecord.ID] = []

        let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
        config.previousServerChangeToken = previousToken

        return try await withCheckedThrowingContinuation { continuation in
            let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zoneID], configurationsByRecordZoneID: [zoneID: config])

            operation.recordWasChangedBlock = { recordID, result in
                switch result {
                case .success(let record):
                    changedRecords.append(record)
                case .failure(let error):
                    AppLogger.error("Failed to fetch record \(recordID): \(error)", tag: self.logTag)
                }
            }

            operation.recordWithIDWasDeletedBlock = { recordID, _ in
                deletedRecordIDs.append(recordID)
            }

            operation.recordZoneChangeTokensUpdatedBlock = { zoneID, token, _ in
                if let token = token {
                    self.saveChangeToken(token)
                }
            }

            operation.fetchRecordZoneChangesResultBlock = { result in
                switch result {
                case .success:
                    Task {
                        await self.applyChanges(changedRecords: changedRecords, deletedRecordIDs: deletedRecordIDs, dataManager: dataManager)
                        continuation.resume()
                    }
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }

            database.add(operation)
        }
    }

    private func saveChangeToken(_ token: CKServerChangeToken) {
        if let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) {
            userDefaults.set(data, forKey: Keys.serverChangeToken)
        }
    }

    private func applyChanges(changedRecords: [CKRecord], deletedRecordIDs: [CKRecord.ID], dataManager: ClimbingDataManager) async {
        guard !changedRecords.isEmpty || !deletedRecordIDs.isEmpty else { return }

        AppLogger.info("Applying CloudKit changes: \(changedRecords.count) updates, \(deletedRecordIDs.count) deletions", tag: logTag)

        await MainActor.run {
            for recordID in deletedRecordIDs {
                let uuidString = recordID.recordName
                if let uuid = UUID(uuidString: uuidString) {
                    if let gym = dataManager.gym(withId: uuid) { dataManager.deleteGym(gym) }
                    else if let problem = dataManager.problem(withId: uuid) { dataManager.deleteProblem(problem) }
                    else if let session = dataManager.session(withId: uuid) { dataManager.deleteSession(session) }
                    else if let attempt = dataManager.attempts.first(where: { $0.id == uuid }) { dataManager.deleteAttempt(attempt) }
                }
            }

            for record in changedRecords {
                guard let uuid = UUID(uuidString: record.recordID.recordName) else { continue }

                switch record.recordType {
                case "Gym":
                    if let gym = mapRecordToGym(record, id: uuid) {
                        if let existing = dataManager.gym(withId: uuid) {
                            if gym.updatedAt >= existing.updatedAt {
                                dataManager.updateGym(gym)
                            }
                        } else {
                            dataManager.addGym(gym)
                        }
                    }
                case "Problem":
                    if let problem = mapRecordToProblem(record, id: uuid) {
                        if let existing = dataManager.problem(withId: uuid) {
                            if problem.updatedAt >= existing.updatedAt {
                                dataManager.updateProblem(problem)
                            }
                        } else {
                            dataManager.addProblem(problem)
                        }
                    }
                case "ClimbSession":
                    if let session = mapRecordToSession(record, id: uuid) {
                        if let existingIndex = dataManager.sessions.firstIndex(where: { $0.id == uuid }) {
                            let existing = dataManager.sessions[existingIndex]
                            if session.updatedAt >= existing.updatedAt {
                                dataManager.sessions[existingIndex] = session
                                dataManager.saveSessions()
                            }
                        } else {
                            dataManager.sessions.append(session)
                            dataManager.saveSessions()
                        }
                    }
                case "Attempt":
                    if let attempt = mapRecordToAttempt(record, id: uuid) {
                        if let existingIndex = dataManager.attempts.firstIndex(where: { $0.id == uuid }) {
                            let existing = dataManager.attempts[existingIndex]
                            if attempt.updatedAt >= existing.updatedAt {
                                dataManager.attempts[existingIndex] = attempt
                                dataManager.saveAttempts()
                            }
                        } else {
                            dataManager.attempts.append(attempt)
                            dataManager.saveAttempts()
                        }
                    }
                default:
                    break
                }
            }
        }
    }

    // MARK: - Push (Upload to CloudKit)

    private func pushChanges(dataManager: ClimbingDataManager) async throws {
        let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date ?? Date.distantPast
        let modifiedGyms = dataManager.gyms.filter { $0.updatedAt > lastSync }
        let modifiedProblems = dataManager.problems.filter { $0.updatedAt > lastSync }
        let modifiedSessions = dataManager.sessions.filter { $0.updatedAt > lastSync }
        let modifiedAttempts = dataManager.attempts.filter { $0.createdAt > lastSync }

        let deletedItems = dataManager.getDeletedItems().filter { item in
            let dateFormatter = ISO8601DateFormatter()
            if let date = dateFormatter.date(from: item.deletedAt) {
                return date > lastSync
            }
            return false
        }

        if modifiedGyms.isEmpty && modifiedProblems.isEmpty && modifiedSessions.isEmpty && modifiedAttempts.isEmpty && deletedItems.isEmpty {
            AppLogger.info("No local changes to push", tag: logTag)
            return
        }

        var recordsToSave: [CKRecord] = []
        var recordIDsToDelete: [CKRecord.ID] = []

        for item in deletedItems {
            recordIDsToDelete.append(CKRecord.ID(recordName: item.id, zoneID: zoneID))
        }

        for gym in modifiedGyms {
            recordsToSave.append(createRecord(from: gym))
        }
        for problem in modifiedProblems {
            recordsToSave.append(createRecord(from: problem))
        }
        for session in modifiedSessions {
            recordsToSave.append(createRecord(from: session))
        }
        for attempt in modifiedAttempts {
            recordsToSave.append(createRecord(from: attempt))
        }

        guard !recordsToSave.isEmpty || !recordIDsToDelete.isEmpty else { return }

        AppLogger.info("Pushing to iCloud: \(recordsToSave.count) saves, \(recordIDsToDelete.count) deletions", tag: logTag)

        let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
        operation.savePolicy = .changedKeys // Merges changes if possible, simpler than handling tags manually
        operation.isAtomic = true // Transactional

        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            operation.modifyRecordsResultBlock = { result in
                switch result {
                case .success:
                    continuation.resume()
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
            database.add(operation)
        }
    }

    // MARK: - Mappers (To CKRecord)

    private func createRecord(from gym: Gym) -> CKRecord {
        let recordID = CKRecord.ID(recordName: gym.id.uuidString, zoneID: zoneID)
        let record = CKRecord(recordType: "Gym", recordID: recordID)

        record["name"] = gym.name
        record["location"] = gym.location
        record["notes"] = gym.notes
        record["createdAt"] = gym.createdAt
        record["updatedAt"] = gym.updatedAt

        record["supportedClimbTypes"] = encode(gym.supportedClimbTypes)
        record["difficultySystems"] = encode(gym.difficultySystems)
        record["customDifficultyGrades"] = encode(gym.customDifficultyGrades)

        return record
    }

    private func createRecord(from problem: Problem) -> CKRecord {
        let recordID = CKRecord.ID(recordName: problem.id.uuidString, zoneID: zoneID)
        let record = CKRecord(recordType: "Problem", recordID: recordID)

        record["gymId"] = problem.gymId.uuidString
        record["name"] = problem.name
        record["description"] = problem.description
        record["climbType"] = problem.climbType.rawValue
        record["difficulty"] = encode(problem.difficulty)
        record["tags"] = encode(problem.tags)
        record["location"] = problem.location
        record["isActive"] = problem.isActive ? 1 : 0
        record["dateSet"] = problem.dateSet
        record["notes"] = problem.notes
        record["createdAt"] = problem.createdAt
        record["updatedAt"] = problem.updatedAt

        var assets: [CKAsset] = []
        for path in problem.imagePaths {
            let fullPath = ImageManager.shared.getFullPath(from: path)
            let fileURL = URL(fileURLWithPath: fullPath)
            if FileManager.default.fileExists(atPath: fullPath) {
                assets.append(CKAsset(fileURL: fileURL))
            }
        }
        if !assets.isEmpty {
            record["images"] = assets
        }

        return record
    }

    private func createRecord(from session: ClimbSession) -> CKRecord {
        let recordID = CKRecord.ID(recordName: session.id.uuidString, zoneID: zoneID)
        let record = CKRecord(recordType: "ClimbSession", recordID: recordID)

        record["gymId"] = session.gymId.uuidString
        record["date"] = session.date
        record["startTime"] = session.startTime
        record["endTime"] = session.endTime
        record["duration"] = session.duration
        record["status"] = session.status.rawValue
        record["notes"] = session.notes
        record["createdAt"] = session.createdAt
        record["updatedAt"] = session.updatedAt

        return record
    }

    private func createRecord(from attempt: Attempt) -> CKRecord {
        let recordID = CKRecord.ID(recordName: attempt.id.uuidString, zoneID: zoneID)
        let record = CKRecord(recordType: "Attempt", recordID: recordID)

        record["sessionId"] = attempt.sessionId.uuidString
        record["problemId"] = attempt.problemId.uuidString
        record["result"] = attempt.result.rawValue
        record["highestHold"] = attempt.highestHold
        record["notes"] = attempt.notes
        record["duration"] = attempt.duration
        record["restTime"] = attempt.restTime
        record["timestamp"] = attempt.timestamp
        record["createdAt"] = attempt.createdAt
        record["updatedAt"] = attempt.updatedAt

        return record
    }

    // MARK: - Mappers (From CKRecord)

    private func mapRecordToGym(_ record: CKRecord, id: UUID) -> Gym? {
        guard let name = record["name"] as? String,
              let supportedClimbTypesData = record["supportedClimbTypes"] as? String,
              let difficultySystemsData = record["difficultySystems"] as? String,
              let createdAt = record["createdAt"] as? Date,
              let updatedAt = record["updatedAt"] as? Date
        else { return nil }

        let location = record["location"] as? String
        let notes = record["notes"] as? String
        let customGradesData = record["customDifficultyGrades"] as? String ?? "[]"

        let supportedClimbTypes: [ClimbType] = decode(supportedClimbTypesData) ?? []
        let difficultySystems: [DifficultySystem] = decode(difficultySystemsData) ?? []
        let customDifficultyGrades: [String] = decode(customGradesData) ?? []

        return Gym.fromImport(
            id: id,
            name: name,
            location: location,
            supportedClimbTypes: supportedClimbTypes,
            difficultySystems: difficultySystems,
            customDifficultyGrades: customDifficultyGrades,
            notes: notes,
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }

    private func mapRecordToProblem(_ record: CKRecord, id: UUID) -> Problem? {
        guard let gymIdString = record["gymId"] as? String,
              let gymId = UUID(uuidString: gymIdString),
              let climbTypeRaw = record["climbType"] as? String,
              let climbType = ClimbType(rawValue: climbTypeRaw),
              let difficultyData = record["difficulty"] as? String,
              let difficulty: DifficultyGrade = decode(difficultyData),
              let createdAt = record["createdAt"] as? Date,
              let updatedAt = record["updatedAt"] as? Date
        else { return nil }

        let name = record["name"] as? String
        let description = record["description"] as? String
        let tagsData = record["tags"] as? String ?? "[]"
        let tags: [String] = decode(tagsData) ?? []
        let location = record["location"] as? String
        let isActive = (record["isActive"] as? Int ?? 1) == 1
        let dateSet = record["dateSet"] as? Date
        let notes = record["notes"] as? String

        var imagePaths: [String] = []
        if let assets = record["images"] as? [CKAsset] {
            for (index, asset) in assets.enumerated() {
                guard let fileURL = asset.fileURL else { continue }
                let filename = ImageNamingUtils.generateImageFilename(problemId: id.uuidString, imageIndex: index)

                if let data = try? Data(contentsOf: fileURL) {
                    _ = try? ImageManager.shared.saveImportedImage(data, filename: filename)
                    imagePaths.append(filename)
                }
            }
        }

        return Problem.fromImport(
            id: id,
            gymId: gymId,
            name: name,
            description: description,
            climbType: climbType,
            difficulty: difficulty,
            tags: tags,
            location: location,
            imagePaths: imagePaths,
            isActive: isActive,
            dateSet: dateSet,
            notes: notes,
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }

    private func mapRecordToSession(_ record: CKRecord, id: UUID) -> ClimbSession? {
        guard let gymIdString = record["gymId"] as? String,
              let gymId = UUID(uuidString: gymIdString),
              let date = record["date"] as? Date,
              let statusRaw = record["status"] as? String,
              let status = SessionStatus(rawValue: statusRaw),
              let createdAt = record["createdAt"] as? Date,
              let updatedAt = record["updatedAt"] as? Date
        else { return nil }

        let startTime = record["startTime"] as? Date
        let endTime = record["endTime"] as? Date
        let duration = record["duration"] as? Int
        let notes = record["notes"] as? String

        return ClimbSession.fromImport(
            id: id,
            gymId: gymId,
            date: date,
            startTime: startTime,
            endTime: endTime,
            duration: duration,
            status: status,
            notes: notes,
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }

    private func mapRecordToAttempt(_ record: CKRecord, id: UUID) -> Attempt? {
        guard let sessionIdString = record["sessionId"] as? String,
              let sessionId = UUID(uuidString: sessionIdString),
              let problemIdString = record["problemId"] as? String,
              let problemId = UUID(uuidString: problemIdString),
              let resultRaw = record["result"] as? String,
              let result = AttemptResult(rawValue: resultRaw),
              let timestamp = record["timestamp"] as? Date,
              let createdAt = record["createdAt"] as? Date,
              let updatedAt = record["updatedAt"] as? Date
        else { return nil }

        let highestHold = record["highestHold"] as? String
        let notes = record["notes"] as? String
        let duration = record["duration"] as? Int
        let restTime = record["restTime"] as? Int

        return Attempt.fromImport(
            id: id,
            sessionId: sessionId,
            problemId: problemId,
            result: result,
            highestHold: highestHold,
            notes: notes,
            duration: duration,
            restTime: restTime,
            timestamp: timestamp,
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }

    // MARK: - Helper Coding

    private func encode<T: Encodable>(_ value: T) -> String {
        guard let data = try? JSONEncoder().encode(value) else { return "" }
        return String(data: data, encoding: .utf8) ?? ""
    }

    private func decode<T: Decodable>(_ json: String) -> T? {
        guard let data = json.data(using: .utf8) else { return nil }
        return try? JSONDecoder().decode(T.self, from: data)
    }
}
