//
//  FormValidator.swift
//  MVMCoreUI
//
//  Created by Suresh, Kamlesh on 3/26/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import UIKit
import MVMCore


@objcMembers public class FormValidator: NSObject {
    
    public enum ValidationError: Error {
        case other(error: String)
    }
    
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    public static var defaultGroupName: String = "default"
    public var formRules: [FormGroupRule]?
    public weak var delegate: FormHolderProtocol?
    public var fields: [String: FormFieldProtocol] = [:]
    public var groupWatchers: [FormGroupWatcherFieldProtocol] = []
    public var radioButtonsModelByGroup: [String: RadioButtonSelectionHelper] = [:]

    //--------------------------------------------------
    // MARK: - Initializer
    //--------------------------------------------------
    
    public init(_ formRules: [FormGroupRule]?) {
        self.formRules = formRules
    }

    //--------------------------------------------------
    // MARK: - Methods
    //--------------------------------------------------
    
    /// Adds the form field to the validator.
    public func add(_ field: FormFieldProtocol) {
        if let fieldKey = field.fieldKey {
            fields[fieldKey] = field
        }
    }
    
    /// Adds the form action to the validator.
    public func add(_ action: FormGroupWatcherFieldProtocol) {
        groupWatchers.append(action)
    }
    
    /// Removes the form field to the validator.
    public func remove(_ field: FormFieldProtocol) {
        if let fieldKey = field.fieldKey {
            fields.removeValue(forKey: fieldKey)
        }
    }
    
    /// Determines the type of item and adds it.
    private func insert(_ item: FormItemProtocol) {
        if let item = item as? FormFieldProtocol {
            add(item)
        } else if let item = item as? FormGroupWatcherFieldProtocol {
            add(item)
        }
    }
    
    /// Convenience function. Gets the form validator from the holder and sets up the item with it.
    public static func setupValidation(for item: FormItemProtocol, delegate: FormHolderProtocol?) {
        if let validator = delegate?.formValidator {
            validator.delegate = delegate
            validator.insert(item)
            
            // TODO: Temporary hacks, rewrite architecture to support this.
            _ = validator.validate()
        }
    }

    /// Convenience function. Gets the form validator from the holder and asks it to validate.
    public static func validate(delegate: FormHolderProtocol?) -> Bool? {
        delegate?.formValidator?.validate()
    }

    /// Validates all rule groups. Returns if valid
    public func validate() -> Bool {
        var valid = true
        guard let formRules = formRules else { return valid }
        
        for group in formRules {
            
            var groupValid = false
            do {
                groupValid = try validateGroup(group)
            } catch {
                if let err = MVMCoreErrorObject.createErrorObject(for: error, location: "FormValidator"){
                    MVMCoreLoggingHandler.shared()?.addError(toLog: err)
                    fatalError(err.description)
                }
            }
            valid = valid && groupValid
        }
        
        return valid
    }
    
    /// Validates a FormGroupRule
    /// - Parameters:
    ///   - group: FormGroupRule being validated
    ///   - counter: keeps track of how many times causes another group validation
    /// - Returns: validity for the FormGroupRule.rules
    public func validateGroup(_ group: FormGroupRule, counter: Int = 0) throws -> Bool {
        let tuple = group.validate(fields)
        
        group.rules.forEach { rule in
            for formKey in rule.fields {
                guard let formField = fields[formKey] as? FormRuleWatcherFieldProtocol,
                      let fieldValidity = tuple.fieldValidity[formKey] else { continue }
                formField.setValidity(fieldValidity, rule: rule)
            }
        }
        
        // Notify the group watchers of validity.
        for watcher in groupWatchers.filter({$0.groupName == group.groupName}) {
            watcher.setValidity(tuple.valid)
        }
        
        var ruleChange = false
        
        //loop the effects
        group.effects?.forEach({ effect in
            //get the fieldKey for the effect
            if let effected = fields[effect.fieldKey] {
                //get the validity
                let effectTuple = effect.validate(fields)

                //set the effect with the validation
                effect.setEffect(validity: effectTuple.valid, field: effected, form: self, group: group)
                
                //update the group form rules
                if let ruleIds = effect.activatedRuleIds {
                    let didChange = self.updateRules(for: group, with: effectTuple.valid, for: effect.fieldKey, and: ruleIds)
                    if(didChange) {
                        ruleChange = didChange
                    }
                }
            }
        })
        
        if ruleChange {
            if counter > 3 {
                throw ValidationError.other(error: "Effect caused validation loop error")
            } else {
                return try self.validateGroup(group, counter: counter + 1)
            }
        } else {
            return tuple.valid
        }
        
    }
    
    /// Updates the Fields in which a specific rule within a FormGroupRule validates against.
    /// - Parameters:
    ///   - group: FormGroupRule to update
    ///   - validity: If you the fieldKey should be added or removed from a rule
    ///   - fieldKey: FieldKey that will be added or removed from a rule
    ///   - ruleIds: Array of ruleIds to add or remove the fieldKey into the rule.fields property
    public func updateRules(for group: FormGroupRule, with validity: Bool, for fieldKey: String, and ruleIds: [String]) -> Bool{
        //update the group rules based on the validation of this rule to show/hide
        var ruleChange = false
        ruleIds.forEach { ruleId in
            //only change a rule found
            if var mutableRule = group.rules.first(where: { rule in return rule.ruleId == ruleId}) {
                //make a copy of the fields
                var mutableFields = mutableRule.fields
                //if the validity is true
                if validity {
                    //we need to ensure this fieldKey is included in the found rule
                    //therefore we mush append the fieldKey to the fields array
                    if !mutableRule.fields.contains(where: { field in return field == fieldKey }){
                        mutableFields.append(fieldKey)
                        mutableRule.fields = mutableFields
                        //a rule was changed, therefore we need
                        //to re-run the validation on the group
                        ruleChange = true
                    }
                } else {
                    //the validity is false, therefore we must ensure
                    //that the fieldKey does NOT exist in the found
                    //rule fields
                    if let fieldKeyIndex = mutableFields.firstIndex(of: fieldKey) {
                        mutableFields.remove(at: fieldKeyIndex)
                        mutableRule.fields = mutableFields
                        //a rule was changed, therefore we need
                        //to re-run the validation on the group
                        ruleChange = true
                    }
                }
            }
        }
        
        //if any rules updated, run the validateGroup again
        return ruleChange
    }
}

// MARK: Form params
// TODO: Temporary hacks, rewrite architecture to support this.
public extension FormValidator {
    
    func addFormParams(requestParameters: MVMCoreRequestParameters, model: (MoleculeModelProtocol & FormItemProtocol)?)  {
        let groupName = model?.groupName ?? getGroupName(forPageType: requestParameters.pageType) ?? FormValidator.defaultGroupName
        let formParams = getFormParams(forGroup: groupName)
        requestParameters.add(formParams)
    }

    @objc func getFormParams(forGroup groupName: String) -> [String: Any] {
        
        var extraParam: [String: Any] = [:]
        for (fieldKey, field) in fields {
            if let formFieldValue = field.formFieldValue(),
               groupName == field.groupName {
                extraParam[fieldKey] = formFieldValue
            }
        }
        
        return extraParam
    }
}

// TODO: Temporary hacks, rewrite architecture to support this.
public extension FormValidator {

    func getGroupName(forPageType pageType: String?) -> String? {

        for actionItem in groupWatchers {
            if let buttonModel = actionItem as? ButtonModel,
                pageType == (buttonModel.action as? ActionOpenPageProtocol)?.pageType {
                return buttonModel.groupName
            }
        }

        return nil
    }
}

