//
//  ErrorHandler.swift
//  IceCream
//
//  Created by @randycarney on 12/12/17.
//

import Foundation
import CloudKit

/// This struct helps you handle all the CKErrors and has been updated to the current Apple documentation(12/15/2017):
/// https://developer.apple.com/documentation/cloudkit/ckerror.code

struct ErrorHandler {
    
    static let shared = ErrorHandler()
    
    /// We could classify all the result that CKOperation returns into the following five CKOperationResultTypes
    enum CKOperationResultType {
        case success
        case retry(afterSeconds: Double, message: String, error: Error)
        case chunk
        case recoverableError(reason: CKOperationFailReason, message: String, error: Error)
        case fail(reason: CKOperationFailReason, message: String, error: Error)
    }
    
    /// The reason of CloudKit failure could be classified into following 8 cases
    enum CKOperationFailReason {
        case changeTokenExpired
        case network
        case quotaExceeded
        case partialFailure
        case serverRecordChanged
        case shareRelated
        case unhandledErrorCode
        case unknown
    }
    
    func resultType(with error: Error?) -> CKOperationResultType {
        guard let error = error else { return .success }
        
        guard let e = error as? CKError else {
            return .fail(reason: .unknown, message: "The error returned is not a CKError", error: error)
        }
        
        let message = returnErrorMessage(for: e.code)
        
        switch e.code {
            
        // SHOULD RETRY
        case .serviceUnavailable,
             .requestRateLimited,
             .zoneBusy:
            
            // If there is a retry delay specified in the error, then use that.
            let userInfo = e.userInfo
            if let retry = userInfo[CKErrorRetryAfterKey] as? Double {
                print("ErrorHandler - \(message). Should retry in \(retry) seconds.")
                return .retry(afterSeconds: retry, message: message, error: error)
            } else {
                return .fail(reason: .unknown, message: message, error: error)
            }
            
        // RECOVERABLE ERROR
        case .networkUnavailable,
             .networkFailure:
            print("ErrorHandler.recoverableError: \(message)")
            return .recoverableError(reason: .network, message: message, error: error)
        case .changeTokenExpired:
            print("ErrorHandler.recoverableError: \(message)")
            return .recoverableError(reason: .changeTokenExpired, message: message, error: error)
        case .serverRecordChanged:
            print("ErrorHandler.recoverableError: \(message)")
            return .recoverableError(reason: .serverRecordChanged, message: message, error: error)
        case .partialFailure:
            // Normally it shouldn't happen since if CKOperation `isAtomic` set to true
            if let dictionary = e.userInfo[CKPartialErrorsByItemIDKey] as? NSDictionary {
                print("ErrorHandler.partialFailure for \(dictionary.count) items; CKPartialErrorsByItemIDKey: \(dictionary)")

                if partialFailureShouldRetry(withErrors: dictionary) {
                    print("Retrying the operation as it contains reference errors")
                    return .retry(afterSeconds: 5, message: message, error: error)
                }
            }
            return .recoverableError(reason: .partialFailure, message: message, error: error)
            
        // SHOULD CHUNK IT UP
        case .limitExceeded:
            print("ErrorHandler.Chunk: \(message)")
            return .chunk
            
        // SHARE DATABASE RELATED
        case .alreadyShared,
             .participantMayNeedVerification,
             .tooManyParticipants:
            print("ErrorHandler.Fail: \(message)")
            return .fail(reason: .shareRelated, message: message, error: error)
        
        // quota exceeded is sort of a special case where the user has to take action(like spare more room in iCloud) before retry
        case .quotaExceeded:
            print("ErrorHandler.Fail: \(message)")
            return .fail(reason: .quotaExceeded, message: message, error: error)

        case .referenceViolation:
            print("ErrorHandler.Fail: \(message). Will retry in 5 seconds because likely the parent is currently being uploaded")
            return .retry(afterSeconds: 5, message: message, error: error)
            
        // FAIL IS THE FINAL, WE REALLY CAN'T DO MORE
        default:
            print("ErrorHandler.Fail: \(message)")
            return .fail(reason: .unknown, message: message, error: error)
        }
    }
    
    func retryOperationIfPossible(retryAfter: Double, block: @escaping () -> ()) {
        let delayTime = DispatchTime.now() + retryAfter
        DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
            block()
        })
        
    }

    private func partialFailureShouldRetry(withErrors errors: NSDictionary) -> Bool {
        for error in errors.allValues {
            // If one of the errors is a reference violation, it probably means that the parent is not yet uploaded
            // So, let's retry it
            if let error = error as? CKError, error.code == .referenceViolation {
                return true
            }
        }

        return false
    }
    
    private func returnErrorMessage(for code: CKError.Code) -> String {
        var returnMessage = ""
        
        switch code {
        case .alreadyShared:
            returnMessage = "Already Shared: a record or share cannot be saved because doing so would cause the same hierarchy of records to exist in multiple shares."
        case .assetFileModified:
            returnMessage = "Asset File Modified: the content of the specified asset file was modified while being saved."
        case .assetFileNotFound:
            returnMessage = "Asset File Not Found: the specified asset file is not found."
        case .badContainer:
            returnMessage = "Bad Container: the specified container is unknown or unauthorized."
        case .badDatabase:
            returnMessage = "Bad Database: the operation could not be completed on the given database."
        case .batchRequestFailed:
            returnMessage = "Batch Request Failed: the entire batch was rejected."
        case .changeTokenExpired:
            returnMessage = "Change Token Expired: the previous server change token is too old."
        case .constraintViolation:
            returnMessage = "Constraint Violation: the server rejected the request because of a conflict with a unique field."
        case .incompatibleVersion:
            returnMessage = "Incompatible Version: your app version is older than the oldest version allowed."
        case .internalError:
            returnMessage = "Internal Error: a nonrecoverable error was encountered by CloudKit."
        case .invalidArguments:
            returnMessage = "Invalid Arguments: the specified request contains bad information."
        case .limitExceeded:
            returnMessage = "Limit Exceeded: the request to the server is too large."
        case .managedAccountRestricted:
            returnMessage = "Managed Account Restricted: the request was rejected due to a managed-account restriction."
        case .missingEntitlement:
            returnMessage = "Missing Entitlement: the app is missing a required entitlement."
        case .networkUnavailable:
            returnMessage = "Network Unavailable: the internet connection appears to be offline."
        case .networkFailure:
            returnMessage = "Network Failure: the internet connection appears to be offline."
        case .notAuthenticated:
            returnMessage = "Not Authenticated: to use this app, you must enable iCloud syncing. Go to device Settings, sign in to iCloud, then in the app settings, be sure the iCloud feature is enabled."
        case .operationCancelled:
            returnMessage = "Operation Cancelled: the operation was explicitly canceled."
        case .partialFailure:
            returnMessage = "Partial Failure: some items failed, but the operation succeeded overall."
        case .participantMayNeedVerification:
            returnMessage = "Participant May Need Verification: you are not a member of the share."
        case .permissionFailure:
            returnMessage = "Permission Failure: to use this app, you must enable iCloud syncing. Go to device Settings, sign in to iCloud, then in the app settings, be sure the iCloud feature is enabled."
        case .quotaExceeded:
            returnMessage = "Quota Exceeded: saving would exceed your current iCloud storage quota."
        case .referenceViolation:
            returnMessage = "Reference Violation: the target of a record's parent or share reference was not found."
        case .requestRateLimited:
            returnMessage = "Request Rate Limited: transfers to and from the server are being rate limited at this time."
        case .serverRecordChanged:
            returnMessage = "Server Record Changed: the record was rejected because the version on the server is different."
        case .serverRejectedRequest:
            returnMessage = "Server Rejected Request"
        case .serverResponseLost:
            returnMessage = "Server Response Lost"
        case .serviceUnavailable:
            returnMessage = "Service Unavailable: Please try again."
        case .tooManyParticipants:
            returnMessage = "Too Many Participants: a share cannot be saved because too many participants are attached to the share."
        case .unknownItem:
            returnMessage = "Unknown Item:  the specified record does not exist."
        case .userDeletedZone:
            returnMessage = "User Deleted Zone: the user has deleted this zone from the settings UI."
        case .zoneBusy:
            returnMessage = "Zone Busy: the server is too busy to handle the zone operation."
        case .zoneNotFound:
            returnMessage = "Zone Not Found: the specified record zone does not exist on the server."
        default:
            returnMessage = "Unhandled Error."
        }
        
        return returnMessage + "CKError.Code: \(code.rawValue)"
    }
    
}

extension Array where Element: CKRecord {
    /// Chunk the big group into smaller ones, with the given chunkSize
    /// For example, we have some dogs(You can test it in the playground):
    ///
    /*  var dogs: [Dog] = []
        for i in 0...22 {
        var dog = Dog(age: i, name: "Dog \(i)")
            dogs.append(dog)
        }
        let chunkedDogs = dogs.chunkItUp(by: 5)
    */
    
    func chunkItUp(by chunkSize: Int) -> [[Element]] {
        return stride(from: 0, to: count, by: chunkSize).map({ (startIndex) -> [Element] in
            let endIndex = (startIndex.advanced(by: chunkSize) > count) ? count : (startIndex + chunkSize)
            return Array(self[startIndex..<endIndex])
        })
    }
}

