// 
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

@available(iOS 13.0, *)
extension MXRestClient {
    /// Errors thrown by the async extensions to `MXRestClient.`
    enum ClientError: Error {
        /// An unexpected response was received.
        case invalidResponse
        /// The error that occurred was missing from the closure.
        case unknownError
        /// An error occurred whilst decoding the received JSON.
        case decodingError
    }
    
    /// An async version of `wellKnow(_:failure:)`.
    func wellKnown() async throws -> MXWellKnown {
        try await getResponse { success, failure in
            wellKnow(success, failure: failure)
        }
    }
    
    // MARK: - Login
    
    /// An async version of `getLoginSession(completion:)`.
    func getLoginSession() async throws -> MXAuthenticationSession {
        try await getResponse(getLoginSession)
    }
    
    /// An async version of `login(parameters:completion:)`, that takes a value that conforms to `LoginParameters`.
    func login(parameters: LoginParameters) async throws -> MXCredentials {
        let dictionary = try parameters.dictionary()
        return try await login(parameters: dictionary)
    }
    
    /// An async version of `login(parameters:completion:)`.
    func login(parameters: [String: Any]) async throws -> MXCredentials {
        let jsonDictionary = try await getResponse { completion in
            login(parameters: parameters, completion: completion)
        }
        guard let loginResponse = MXLoginResponse(fromJSON: jsonDictionary) else { throw ClientError.decodingError }
        return MXCredentials(loginResponse: loginResponse, andDefaultCredentials: credentials)
    }
    
    /// An async version of generateLoginToken(completion:)
    func generateLoginToken() async throws -> MXLoginToken {
        try await getResponse(generateLoginToken)
    }
    
    // MARK: - Registration
    
    /// An async version of `getRegisterSession(completion:)`.
    func getRegisterSession() async throws -> MXAuthenticationSession {
        try await getResponse(getRegisterSession)
    }
    
    /// An async version of `isUsernameAvailable(_:completion:)`.
    func isUsernameAvailable(_ username: String) async throws -> Bool {
        let availability = try await getResponse { completion in
            isUsernameAvailable(username, completion: completion)
        }
        return availability.available
    }
    
    /// An async version of `register(parameters:completion:)`, that takes a `RegistrationParameters` value instead of a dictionary.
    func register(parameters: RegistrationParameters) async throws -> MXLoginResponse {
        let dictionary = try parameters.dictionary()
        return try await register(parameters: dictionary)
    }
    
    /// An async version of `register(parameters:completion:)`.
    func register(parameters: [String: Any]) async throws -> MXLoginResponse {
        let jsonDictionary = try await getResponse { completion in
            register(parameters: parameters, completion: completion)
        }
        guard let loginResponse = MXLoginResponse(fromJSON: jsonDictionary) else { throw ClientError.decodingError }
        return loginResponse
    }
    
    /// An async version of both `requestToken(forEmail:isDuringRegistration:clientSecret:sendAttempt:nextLink:success:failure:)` and
    /// `requestToken(forPhoneNumber:isDuringRegistration:countryCode:clientSecret:sendAttempt:nextLink:success:failure:)` depending
    /// on the kind of third party ID is supplied to the `threePID` parameter.
    func requestTokenDuringRegistration(for threePID: RegisterThreePID, clientSecret: String, sendAttempt: UInt) async throws -> RegistrationThreePIDTokenResponse {
        switch threePID {
        case .email(let email):
            let sessionID: String = try await getResponse { success, failure in
                requestToken(forEmail: email,
                             isDuringRegistration: true,
                             clientSecret: clientSecret,
                             sendAttempt: sendAttempt,
                             nextLink: nil,
                             success: success,
                             failure: failure)
            }
            
            return RegistrationThreePIDTokenResponse(sessionID: sessionID)
        case .msisdn(let msisdn, let countryCode):
            let (sessionID, msisdn, submitURL): (String?, String?, String?) = try await getResponse { success, failure in
                requestToken(forPhoneNumber: msisdn,
                             isDuringRegistration: true,
                             countryCode: countryCode,
                             clientSecret: clientSecret,
                             sendAttempt: sendAttempt,
                             nextLink: nil,
                             success: success,
                             failure: failure)
            }
            guard let sessionID = sessionID else { throw ClientError.invalidResponse }
            return RegistrationThreePIDTokenResponse(sessionID: sessionID, submitURL: submitURL, msisdn: msisdn)
        }
    }
    
    // MARK: - Reset Password
    
    /// An async version of `forgetPassword(forEmail:clientSecret:sendAttempt:success:failure:)`.
    /// - Returns: The session ID to be included when calling `resetPassword(parameters:)`.
    func forgetPassword(for email: String, clientSecret: String, sendAttempt: UInt) async throws -> String {
        try await getResponse { success, failure in
            forgetPassword(forEmail: email,
                           clientSecret: clientSecret,
                           sendAttempt: sendAttempt,
                           success: success,
                           failure: failure)
        }
    }
    
    /// An async version of `resetPassword(parameters:completion:)`, that takes a `CheckResetPasswordParameters` value instead of a dictionary.
    func resetPassword(parameters: CheckResetPasswordParameters) async throws {
        let dictionary = try parameters.dictionary()
        try await resetPassword(parameters: dictionary)
    }
    
    /// An async version of `resetPassword(parameters:completion:)`.
    func resetPassword(parameters: [String: Any]) async throws {
        try await getResponse { completion in
            resetPassword(parameters: parameters, completion: completion)
        }
    }

    // MARK: - Change Password

    /// An async version of `changePassword(from:to:logoutDevices:completion:)`.
    func changePassword(from oldPassword: String, to newPassword: String, logoutDevices: Bool) async throws {
        try await getResponse { completion in
            changePassword(from: oldPassword, to: newPassword, logoutDevices: logoutDevices, completion: completion)
        }
    }

    // MARK: - Versions

    /// An async version of `supportedMatrixVersions(completion:)`.
    func supportedMatrixVersions() async throws -> MXMatrixVersions {
        try await getResponse({ completion in
            supportedMatrixVersions(completion: completion)
        })
    }
    
    // MARK: - Private
    
    @MainActor
    private func getResponse<T>(_ callback: (@escaping (MXResponse<T>) -> Void) -> MXHTTPOperation) async throws -> T {
        try await withCheckedThrowingContinuation { continuation in
            _ = callback { response in
                guard let value = response.value else {
                    continuation.resume(with: .failure(response.error ?? ClientError.unknownError))
                    return
                }
                
                continuation.resume(with: .success(value))
            }
        }
    }
    
    @MainActor
    private func getResponse<T>(_ callback: (@escaping (T?) -> Void, @escaping (Error?) -> Void) -> MXHTTPOperation) async throws -> T {
        try await withCheckedThrowingContinuation { continuation in
            _ = callback { response in
                guard let response = response else {
                    continuation.resume(with: .failure(ClientError.invalidResponse))
                    return
                }
                
                continuation.resume(with: .success(response))
            } _: { error in
                continuation.resume(with: .failure(error ?? ClientError.unknownError))
            }
        }
    }
    
    @MainActor
    private func getResponse<T, U, V>(_ callback: (@escaping (T?, U?, V?) -> Void, @escaping (Error?) -> Void) -> MXHTTPOperation) async throws -> (T?, U?, V?) {
        try await withCheckedThrowingContinuation { continuation in
            _ = callback { arg1, arg2, arg3  in
                continuation.resume(with: .success((arg1, arg2, arg3)))
            } _: { error in
                continuation.resume(with: .failure(error ?? ClientError.unknownError))
            }
        }
    }
}
