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

/// The result from a registration screen's coordinator
enum AuthenticationRegistrationStageResult {
    /// The screen completed with the associated registration result.
    case completed(RegistrationResult)
    /// The user would like to cancel the registration.
    case cancel
}

/// The result from a response of a registration flow step.
enum RegistrationResult {
    /// Registration has completed, creating an `MXSession` for the account.
    case success(MXSession)
    /// The request was successful but there are pending steps to complete.
    case flowResponse(FlowResult)
}

/// The state of an authentication flow after a step has been completed.
struct FlowResult {
    /// The stages in the flow that are yet to be completed.
    let missingStages: [Stage]
    /// The stages in the flow that have been completed.
    let completedStages: [Stage]
    
    /// A stage in the authentication flow.
    enum Stage {
        /// The stage with the type `m.login.email.identity`.
        case email(isMandatory: Bool)
        
        /// The stage with the type `m.login.msisdn`.
        case msisdn(isMandatory: Bool)
        
        /// The stage with the type `m.login.terms`.
        case terms(isMandatory: Bool, terms: MXLoginTerms?)
        
        /// The stage with the type `m.login.recaptcha`.
        case reCaptcha(isMandatory: Bool, siteKey: String)
        
        /// The stage with the type `m.login.dummy`.
        ///
        /// This stage can be mandatory if there is no other stages. In this case the account cannot
        /// be created by just sending a username and a password, the dummy stage has to be completed.
        case dummy(isMandatory: Bool)
        
        /// A stage of an unknown type.
        case other(isMandatory: Bool, type: String, params: [AnyHashable: Any])
        
        /// Whether the stage is mandatory.
        var isMandatory: Bool {
            switch self {
            case .reCaptcha(let isMandatory, _):
                return isMandatory
            case .email(let isMandatory):
                return isMandatory
            case .msisdn(let isMandatory):
                return isMandatory
            case .dummy(let isMandatory):
                return isMandatory
            case .terms(let isMandatory, _):
                return isMandatory
            case .other(let isMandatory, _, _):
                return isMandatory
            }
        }
        
        /// Whether the stage is the dummy stage.
        var isDummy: Bool {
            guard case .dummy = self else { return false }
            return true
        }
    }
    
    /// Determines the next stage to be completed in the flow, following the order Email → Terms → ReCaptcha.
    var nextUncompletedStageOrdered: Stage? {
        if let emailStage = missingStages.first(where: { if case .email = $0 { return true } else { return false } }) {
            return emailStage
        }
        if let termsStage = missingStages.first(where: { if case .terms = $0 { return true } else { return false } }) {
            return termsStage
        }
        if let reCaptchaStage = missingStages.first(where: { if case .reCaptcha = $0 { return true } else { return false } }) {
            return reCaptchaStage
        }
        
        return nextUncompletedStage
    }
    
    /// Determines the next stage to be completed in the flow honouring the server's ordering.
    /// This ordering is slightly broken when the are multiple flows as mandatory stages are
    /// shown first and then optional ones afterwards.
    var nextUncompletedStage: Stage? {
        if let mandatoryStage = missingStages.filter(\.isMandatory).first {
            return mandatoryStage
        }
        return missingStages.first
    }
    
    /// Whether fallback registration should be used due to unsupported stages.
    var needsFallback: Bool {
        missingStages.filter(\.isMandatory).contains { stage in
            if case .other = stage { return true } else { return false }
        }
    }
}

extension MXAuthenticationSession {
    /// The flows from the session mapped as a `FlowResult` value.
    var flowResult: FlowResult {
        let allFlowTypes = OrderedSet(flows.flatMap { $0.stages ?? [] })
        var missingStages = [FlowResult.Stage]()
        var completedStages = [FlowResult.Stage]()
        
        allFlowTypes.forEach { flow in
            let isMandatory = flows.allSatisfy { $0.stages.contains(flow) }
            
            let stage: FlowResult.Stage
            switch flow {
            case kMXLoginFlowTypeRecaptcha:
                let parameters = params[flow] as? [AnyHashable: Any]
                let publicKey = parameters?["public_key"] as? String
                stage = .reCaptcha(isMandatory: isMandatory, siteKey: publicKey ?? "")
            case kMXLoginFlowTypeDummy:
                stage = .dummy(isMandatory: isMandatory)
            case kMXLoginFlowTypeTerms:
                let parameters = params[flow] as? [AnyHashable: Any]
                let terms = MXLoginTerms(fromJSON: parameters)
                stage = .terms(isMandatory: isMandatory, terms: terms)
            case kMXLoginFlowTypeMSISDN:
                stage = .msisdn(isMandatory: isMandatory)
            case kMXLoginFlowTypeEmailIdentity:
                stage = .email(isMandatory: isMandatory)
            default:
                let parameters = params[flow] as? [AnyHashable: Any]
                stage = .other(isMandatory: isMandatory, type: flow, params: parameters ?? [:])
            }
            
            if let completed = completed, completed.contains(flow) {
                completedStages.append(stage)
            } else {
                missingStages.append(stage)
            }
        }
        
        return FlowResult(missingStages: missingStages, completedStages: completedStages)
    }
}

// MARK: - Equatable

extension FlowResult.Stage: Equatable {
    // The [AnyHashable: Any] dictionary breaks automatic conformance, so add manually (but ignore this value).
    static func == (lhs: FlowResult.Stage, rhs: FlowResult.Stage) -> Bool {
        switch (lhs, rhs) {
        case (.email(let lhsMandatory), .email(let rhsMandatory)):
            return lhsMandatory == rhsMandatory
        case (.msisdn(let lhsMandatory), .msisdn(let rhsMandatory)):
            return lhsMandatory == rhsMandatory
        case (.terms(let lhsMandatory, let lhsTerms), .terms(let rhsMandatory, let rhsTerms)):
            // TODO: Add comprehensive Equatable conformance on MXLoginTerms
            return lhsMandatory == rhsMandatory && lhsTerms?.policies == rhsTerms?.policies
        case (.reCaptcha(let lhsMandatory, let lhsSiteKey), .reCaptcha(let rhsMandatory, let rhsSiteKey)):
            return lhsMandatory == rhsMandatory && lhsSiteKey == rhsSiteKey
        case (.dummy(let lhsMandatory), .dummy(let rhsMandatory)):
            return lhsMandatory == rhsMandatory
        case (.other(let lhsMandatory, let lhsType, _), .other(let rhsMandatory, let rhsType, _)):
            return lhsMandatory == rhsMandatory && lhsType == rhsType
        default:
            return false
        }
    }
}
