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

@objc protocol DeactivateAccountServiceDelegate: AnyObject {
    /// The service encountered an error.
    /// - Parameter error: The error that occurred.
    func deactivateAccountServiceDidEncounterError(_ error: Error)
    
    /// The service successfully completed the account deactivation.
    func deactivateAccountServiceDidCompleteDeactivation()
}

/// The kind of authentication needed to deactivate an account.
@objc enum DeactivateAccountAuthentication: Int {
    /// The deactivation endpoint is already authenticated. This is unlikely to be the case.
    case authenticated
    /// The deactivation endpoint requires a password for authentication.
    case requiresPassword
    /// The deactivation endpoint requires fallback for authentication.
    case requiresFallback
}

/// An error that occurred in the `DeactivateAccountService`
enum DeactivateAccountServiceError: Error {
    /// The next stage in the flow wasn't found.
    case missingStage
    /// The URL needed to present fallback authentication wasn't found.
    case missingFallbackURL
}

/// A service that helps handle interactive authentication when deactivating an account.
@objcMembers class DeactivateAccountService: NSObject {
    private let session: MXSession
    private let uiaService: UserInteractiveAuthenticationService
    private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST", params: [:])
    
    /// The authentication session's ID if interactive authentication has begun, otherwise `nil`.
    private var sessionID: String?
    
    weak var delegate: DeactivateAccountServiceDelegate?
    
    /// Creates a new service for the supplied session.
    /// - Parameter session: The session with the account to be deactivated.
    init(session: MXSession) {
        self.session = session
        self.uiaService = UserInteractiveAuthenticationService(session: session)
    }
    
    /// Checks the authentication required for deactivation.
    /// - Parameters:
    ///   - success: A closure called containing information about the kind of authentication required (and a fallback URL if needed).
    ///   - failure: A closure called then the check failed.
    func checkAuthentication(success: @escaping (DeactivateAccountAuthentication, URL?) -> Void,
                             failure: @escaping (Error) -> Void) {
        uiaService.authenticatedEndpointStatus(for: request) { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let status):
                switch status {
                case .authenticationNotNeeded:
                    success(.authenticated, nil)
                case .authenticationNeeded(let session):
                    do {
                        let (authentication, fallbackURL) = try self.handleAuthenticationSession(session)
                        success(authentication, fallbackURL)
                    } catch {
                        failure(error)
                    }
                }
            case .failure(let error):
                failure(error)
            }
        }
    }
    
    /// Extracts the authentication information out of an `MXAuthenticationSession`.
    /// - Parameter authenticationSession: The authentication session to be used.
    /// - Returns: A tuple containing the required authentication method along with a URL for fallback if necessary.
    private func handleAuthenticationSession(_ authenticationSession: MXAuthenticationSession) throws -> (DeactivateAccountAuthentication, URL?) {
        guard let nextStage = uiaService.firstUncompletedFlowIdentifier(in: authenticationSession) else {
            MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine the next stage.")
            throw DeactivateAccountServiceError.missingStage
        }
        
        switch nextStage {
        case kMXLoginFlowTypePassword:
            sessionID = authenticationSession.session
            return (.requiresPassword, nil)
        default:
            guard let fallbackURL = uiaService.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) else {
                MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine fallback URL.")
                throw DeactivateAccountServiceError.missingFallbackURL
            }
            sessionID = authenticationSession.session
            return (.requiresFallback, fallbackURL)
        }
        
    }
    
    /// Deactivates the account with the supplied password.
    /// - Parameters:
    ///   - password: The password for the account.
    ///   - eraseAccount: Whether or not to erase all of the data from the account too.
    func deactivate(with password: String, eraseAccount: Bool) {
        do {
            let parameters = try DeactivateAccountPasswordParameters(user: session.myUserId, password: password).dictionary()
            deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
        } catch {
            self.delegate?.deactivateAccountServiceDidEncounterError(error)
        }
    }
    
    /// Deactivates the account when authentication has already been completed.
    /// - Parameter eraseAccount: Whether or not to erase all of the data from the account too.
    func deactivate(eraseAccount: Bool) {
        do {
            let parameters = try DeactivateAccountDummyParameters(user: session.myUserId, session: sessionID ?? "").dictionary()
            deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
        } catch {
            self.delegate?.deactivateAccountServiceDidEncounterError(error)
        }
    }
    
    
    /// Deactivated the account using the specified parameters.
    /// - Parameters:
    ///   - parameters: The parameters for account deactivation.
    ///   - eraseAccount: Whether or not to erase all of the data from the account too.
    private func deactivateAccount(parameters: [String: Any], eraseAccount: Bool) {
        session.deactivateAccount(withAuthParameters: parameters, eraseAccount: eraseAccount) { [weak self] response in
            switch response {
            case .success:
                self?.delegate?.deactivateAccountServiceDidCompleteDeactivation()
            case .failure(let error):
                self?.delegate?.deactivateAccountServiceDidEncounterError(error)
            }
        }
    }
}
