/*
* Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
*
* 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.
*/

package eu.teraflow.policy;

import eu.teraflow.policy.context.ContextService;
import eu.teraflow.policy.context.model.ConfigActionEnum;
import eu.teraflow.policy.context.model.ConfigRule;
import eu.teraflow.policy.context.model.ConfigRuleCustom;
import eu.teraflow.policy.context.model.ConfigRuleTypeCustom;
import eu.teraflow.policy.context.model.Constraint;
import eu.teraflow.policy.context.model.ConstraintCustom;
import eu.teraflow.policy.context.model.ConstraintTypeCustom;
import eu.teraflow.policy.context.model.ServiceConfig;
import eu.teraflow.policy.device.DeviceService;
import eu.teraflow.policy.model.BooleanOperator;
import eu.teraflow.policy.model.PolicyRule;
import eu.teraflow.policy.model.PolicyRuleAction;
import eu.teraflow.policy.model.PolicyRuleActionConfig;
import eu.teraflow.policy.model.PolicyRuleActionEnum;
import eu.teraflow.policy.model.PolicyRuleBasic;
import eu.teraflow.policy.model.PolicyRuleCondition;
import eu.teraflow.policy.model.PolicyRuleDevice;
import eu.teraflow.policy.model.PolicyRuleService;
import eu.teraflow.policy.model.PolicyRuleState;
import eu.teraflow.policy.model.PolicyRuleStateEnum;
import eu.teraflow.policy.model.PolicyRuleTypeDevice;
import eu.teraflow.policy.model.PolicyRuleTypeService;
import eu.teraflow.policy.monitoring.MonitoringService;
import eu.teraflow.policy.monitoring.model.AlarmDescriptor;
import eu.teraflow.policy.monitoring.model.AlarmResponse;
import eu.teraflow.policy.monitoring.model.AlarmSubscription;
import eu.teraflow.policy.monitoring.model.KpiValueRange;
import eu.teraflow.policy.monitoring.model.MonitorKpiRequest;
import eu.teraflow.policy.service.ServiceService;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.jboss.logging.Logger;

@ApplicationScoped
public class PolicyServiceImpl implements PolicyService {

    private static final Logger LOGGER = Logger.getLogger(PolicyServiceImpl.class);
    private static final String INVALID_MESSAGE = "%s is invalid.";
    private static final String VALID_MESSAGE = "%s is valid.";
    private static final int POLICY_EVALUATION_TIMEOUT = 5;
    private static final int ACCEPTABLE_NUMBER_OF_ALARMS = 3;
    private static final int MONITORING_WINDOW_IN_SECONDS = 5;
    private static final int SAMPLING_RATE_PER_SECOND = 1;

    private static final PolicyRuleState INSERTED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_INSERTED, "Successfully entered to INSERTED state");
    private static final PolicyRuleState VALIDATED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_VALIDATED, "Successfully transitioned to VALIDATED state");
    private static final PolicyRuleState PROVISIONED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_PROVISIONED,
                    "Successfully transitioned from VALIDATED to PROVISIONED state");
    private static final PolicyRuleState ACTIVE_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_ACTIVE,
                    "Successfully transitioned from PROVISIONED to ACTIVE state");
    private static final PolicyRuleState ENFORCED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_ENFORCED,
                    "Successfully transitioned from ACTIVE to ENFORCED state");
    private static final PolicyRuleState INEFFECTIVE_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_INEFFECTIVE,
                    "Transitioned from ENFORCED to INEFFECTIVE state");
    private static final PolicyRuleState EFFECTIVE_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_EFFECTIVE,
                    "Successfully transitioned from ENFORCED to EFFECTIVE state");
    private static final PolicyRuleState UPDATED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_UPDATED, "Successfully entered to UPDATED state");
    private static final PolicyRuleState REMOVED_POLICYRULE_STATE =
            new PolicyRuleState(
                    PolicyRuleStateEnum.POLICY_REMOVED, "Successfully entered to REMOVED state");

    private final ContextService contextService;
    private final MonitoringService monitoringService;
    private final ServiceService serviceService;
    private final DeviceService deviceService;
    private final PolicyRuleConditionValidator policyRuleConditionValidator;
    private final PolicyRuleConditionFieldsGetter policyRuleConditionFieldsGetter;

    private HashMap<String, PolicyRuleAction> policyRuleActionMap = new HashMap<>();
    private ConcurrentHashMap<String, PolicyRuleService> alarmPolicyRuleServiceMap =
            new ConcurrentHashMap<>();
    private ConcurrentHashMap<String, PolicyRuleDevice> alarmPolicyRuleDeviceMap =
            new ConcurrentHashMap<>();

    @Inject
    public PolicyServiceImpl(
            ContextService contextService,
            MonitoringService monitoringService,
            ServiceService serviceService,
            DeviceService deviceService,
            PolicyRuleConditionValidator policyRuleConditionValidator,
            PolicyRuleConditionFieldsGetter policyRuleConditionFieldsGetter) {
        this.contextService = contextService;
        this.monitoringService = monitoringService;
        this.serviceService = serviceService;
        this.deviceService = deviceService;
        this.policyRuleConditionValidator = policyRuleConditionValidator;
        this.policyRuleConditionFieldsGetter = policyRuleConditionFieldsGetter;
    }

    private static String gen() {
        Random r = new Random(System.currentTimeMillis());
        return String.valueOf((1 + r.nextInt(2)) * 10000 + r.nextInt(10000));
    }

    private static double getTimeStamp() {
        long now = Instant.now().getEpochSecond();
        return Long.valueOf(now).doubleValue();
    }

    @Override
    public Uni<PolicyRuleState> addPolicyService(PolicyRuleService policyRuleService) {
        LOGGER.infof("Received %s", policyRuleService);

        if (!policyRuleService.areArgumentsValid()) {
            LOGGER.error(policyRuleService.getExeceptionMessage());
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleService.getExeceptionMessage());

            return Uni.createFrom().item(policyRuleState);
        }

        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();
        if (!policyRuleBasic.areArgumentsValid()) {
            LOGGER.error(policyRuleService.getExeceptionMessage());
            setPolicyRuleServiceToContext(
                    policyRuleService,
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleBasic.getExeceptionMessage()));
            return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
        }

        policyRuleBasic.setPolicyRuleState(INSERTED_POLICYRULE_STATE);
        policyRuleService.setPolicyRuleBasic(policyRuleBasic);
        final var policyRuleTypeService = new PolicyRuleTypeService(policyRuleService);
        final var policyRule = new PolicyRule(policyRuleTypeService);

        contextService
                .setPolicyRule(policyRule)
                .subscribe()
                .with(id -> validateService(policyRuleService));
        return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
    }

    @Override
    public Uni<PolicyRuleState> updatePolicyService(PolicyRuleService policyRuleService) {
        LOGGER.infof("Received %s", policyRuleService);

        if (!policyRuleService.areArgumentsValid()) {
            LOGGER.error(policyRuleService.getExeceptionMessage());
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleService.getExeceptionMessage());

            return Uni.createFrom().item(policyRuleState);
        }

        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();
        if (!policyRuleBasic.areArgumentsValid()) {
            LOGGER.error(policyRuleService.getExeceptionMessage());
            setPolicyRuleServiceToContext(
                    policyRuleService,
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleBasic.getExeceptionMessage()));
            return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
        }

        policyRuleBasic.setPolicyRuleState(UPDATED_POLICYRULE_STATE);
        policyRuleService.setPolicyRuleBasic(policyRuleBasic);
        final var policyRuleTypeService = new PolicyRuleTypeService(policyRuleService);
        final var policyRule = new PolicyRule(policyRuleTypeService);

        contextService
                .setPolicyRule(policyRule)
                .subscribe()
                .with(id -> validateUpdatedPolicyService(policyRuleService));

        return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
    }

    @Override
    public Uni<PolicyRuleState> addPolicyDevice(PolicyRuleDevice policyRuleDevice) {
        LOGGER.infof("Received %s", policyRuleDevice);

        if (!policyRuleDevice.areArgumentsValid()) {
            LOGGER.error(policyRuleDevice.getExeceptionMessage());
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleDevice.getExeceptionMessage());

            return Uni.createFrom().item(policyRuleState);
        }

        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();
        if (!policyRuleBasic.areArgumentsValid()) {
            LOGGER.error(policyRuleDevice.getExeceptionMessage());
            setPolicyRuleDeviceToContext(
                    policyRuleDevice,
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleBasic.getExeceptionMessage()));
            return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
        }

        policyRuleBasic.setPolicyRuleState(INSERTED_POLICYRULE_STATE);
        policyRuleDevice.setPolicyRuleBasic(policyRuleBasic);
        final var policyRuleTypeDevice = new PolicyRuleTypeDevice(policyRuleDevice);
        final var policyRule = new PolicyRule(policyRuleTypeDevice);

        contextService
                .setPolicyRule(policyRule)
                .subscribe()
                .with(id -> validateDevice(policyRuleDevice));
        return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
    }

    @Override
    public Uni<PolicyRuleState> updatePolicyDevice(PolicyRuleDevice policyRuleDevice) {
        LOGGER.infof("Received %s", policyRuleDevice);

        if (!policyRuleDevice.areArgumentsValid()) {
            LOGGER.error(policyRuleDevice.getExeceptionMessage());
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleDevice.getExeceptionMessage());

            return Uni.createFrom().item(policyRuleState);
        }

        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();
        if (!policyRuleBasic.areArgumentsValid()) {
            LOGGER.error(policyRuleDevice.getExeceptionMessage());
            setPolicyRuleDeviceToContext(
                    policyRuleDevice,
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleBasic.getExeceptionMessage()));
            return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
        }

        policyRuleBasic.setPolicyRuleState(UPDATED_POLICYRULE_STATE);
        policyRuleDevice.setPolicyRuleBasic(policyRuleBasic);
        final var policyRuleTypeDevice = new PolicyRuleTypeDevice(policyRuleDevice);
        final var policyRule = new PolicyRule(policyRuleTypeDevice);

        contextService
                .setPolicyRule(policyRule)
                .subscribe()
                .with(id -> validateUpdatedPolicyDevice(policyRuleDevice));

        return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
    }

    @Override
    public Uni<PolicyRuleState> deletePolicy(String policyRuleId) {
        LOGGER.infof("Received %s", policyRuleId);

        PolicyRule policyRule = contextService.getPolicyRule(policyRuleId).await().indefinitely();

        final var policyRuleBasic = policyRule.getPolicyRuleType().getPolicyRuleBasic();
        List<PolicyRuleCondition> policyRuleConditions = policyRuleBasic.getPolicyRuleConditions();

        for (PolicyRuleCondition policy : policyRuleConditions) {
            var empty = monitoringService.deleteKpi(policy.getKpiId());
            empty
                    .subscribe()
                    .with(emptyMessage -> LOGGER.infof("Kpi [%s] has been deleted.\n", policyRuleId));
        }

        var empty = contextService.removePolicyRule(policyRuleId);
        empty
                .subscribe()
                .with(emptyMessage -> LOGGER.infof("Policy [%s] has been removed.\n", policyRuleId));

        setPolicyRuleToContext(policyRule, REMOVED_POLICYRULE_STATE);
        return Uni.createFrom().item(policyRuleBasic.getPolicyRuleState());
    }

    private void monitorKpi(List<AlarmDescriptor> alarmDescriptorList) {

        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            var monitorKpiRequest =
                    new MonitorKpiRequest(
                            alarmDescriptor.getKpiId(), MONITORING_WINDOW_IN_SECONDS, SAMPLING_RATE_PER_SECOND);
            monitoringService
                    .monitorKpi(monitorKpiRequest)
                    .subscribe()
                    .with(
                            emptyMessage ->
                                    LOGGER.infof(
                                            "Kpi [%s] has started to be monitored.\n", alarmDescriptor.getKpiId()));
        }
    }

    private void provisionAlarm(
            PolicyRule policyRule, List<AlarmDescriptor> alarmDescriptorList, Boolean isService) {

        List<AlarmSubscription> alarmSubscriptionList = new ArrayList<>();

        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            monitoringService
                    .setKpiAlarm(alarmDescriptor)
                    .subscribe()
                    .with(alarmId -> alarmSubscriptionList.add(new AlarmSubscription(alarmId, 0, 0)));
        }

        setPolicyRuleToContext(policyRule, PROVISIONED_POLICYRULE_STATE);

        getAlarmResponseStream(policyRule, alarmDescriptorList, alarmSubscriptionList, isService);
    }

    private void getAlarmResponseStream(
            PolicyRule policyRule,
            List<AlarmDescriptor> alarmDescriptorList,
            List<AlarmSubscription> alarmSubscriptionList,
            Boolean isService) {

        List<Multi<AlarmResponse>> alarmResponseStreamList = new ArrayList<>();
        for (AlarmSubscription alarmSubscription : alarmSubscriptionList) {
            alarmResponseStreamList.add(monitoringService.getAlarmResponseStream(alarmSubscription));
        }
        Multi<AlarmResponse> multi = Multi.createBy().merging().streams(alarmResponseStreamList);

        multi
                .select()
                .first()
                .subscribe()
                .with(
                        alarmResponse -> {
                            LOGGER.info(alarmResponse);
                            if (isService) {
                                if (!alarmPolicyRuleServiceMap.containsKey(alarmResponse.getAlarmId())) {
                                    return;
                                }
                                applyActionService(alarmResponse.getAlarmId());
                            } else {
                                if (!alarmPolicyRuleDeviceMap.containsKey(alarmResponse.getAlarmId())) {
                                    return;
                                }
                                applyActionDevice(alarmResponse.getAlarmId());
                            }
                        });

        Long count =
                multi
                        .collect()
                        .with(Collectors.counting())
                        .await()
                        .atMost(Duration.ofMinutes(POLICY_EVALUATION_TIMEOUT));

        if (count > ACCEPTABLE_NUMBER_OF_ALARMS) {
            for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
                monitoringService
                        .deleteAlarm(alarmDescriptor.getAlarmId())
                        .subscribe()
                        .with(
                                emptyMessage ->
                                        LOGGER.infof("Alarm [%s] has been deleted.\n", alarmDescriptor.getAlarmId()));
            }

            setPolicyRuleToContext(policyRule, INEFFECTIVE_POLICYRULE_STATE);

        } else {
            setPolicyRuleToContext(policyRule, EFFECTIVE_POLICYRULE_STATE);

            multi
                    .subscribe()
                    .with(
                            alarmResponse -> {
                                LOGGER.info(alarmResponse);
                                if (isService) {
                                    applyActionService(alarmResponse.getAlarmId());
                                } else {
                                    applyActionDevice(alarmResponse.getAlarmId());
                                }
                            });
        }
    }

    private void applyActionDevice(String alarmId) {
        PolicyRuleDevice policyRuleDevice = alarmPolicyRuleDeviceMap.get(alarmId);

        if (policyRuleActionMap.get(alarmId).getPolicyRuleActionEnum()
                == PolicyRuleActionEnum.POLICY_RULE_ACTION_SET_DEVICE_STATUS) {
            // In case additional PolicyRuleAction for Devices will be added
        }

        setPolicyRuleDeviceToContext(policyRuleDevice, ACTIVE_POLICYRULE_STATE);

        List<String> deviceIds = policyRuleDevice.getDeviceIds();
        List<PolicyRuleActionConfig> actionConfigs =
                policyRuleActionMap.get(alarmId).getPolicyRuleActionConfigs();

        if (deviceIds.size() != actionConfigs.size()) {
            String message =
                    String.format(
                            "The number of action parameters in PolicyRuleDevice with ID: %s, is not aligned with the number of devices.",
                            policyRuleDevice.getPolicyRuleBasic().getPolicyRuleId());
            setPolicyRuleDeviceToContext(
                    policyRuleDevice, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        for (var i = 0; i < deviceIds.size(); i++) {
            activateDevice(deviceIds.get(i), actionConfigs.get(i));
        }

        setPolicyRuleDeviceToContext(policyRuleDevice, ENFORCED_POLICYRULE_STATE);
    }

    private void activateDevice(String deviceId, PolicyRuleActionConfig actionConfig) {

        Boolean toBeEnabled;
        if (actionConfig.getActionKey() == "ENABLED") {
            toBeEnabled = true;
        } else if (actionConfig.getActionKey() == "DISABLED") {
            toBeEnabled = false;
        } else {
            LOGGER.errorf(INVALID_MESSAGE, actionConfig.getActionKey());
            return;
        }

        final var deserializedDeviceUni = contextService.getDevice(deviceId);

        deserializedDeviceUni
                .subscribe()
                .with(
                        device -> {
                            if (toBeEnabled && device.isDisabled()) {
                                device.enableDevice();
                            } else if (!toBeEnabled && device.isEnabled()) {
                                device.disableDevice();
                            } else {
                                LOGGER.errorf(INVALID_MESSAGE, "Device is already in the desired state");
                                return;
                            }

                            deviceService.configureDevice(device);
                        });
    }

    private void addServiceConfigRule(
            PolicyRuleService policyRuleService, PolicyRuleAction policyRuleAction) {

        ConfigActionEnum configActionEnum = ConfigActionEnum.SET;
        List<PolicyRuleActionConfig> actionConfigs = policyRuleAction.getPolicyRuleActionConfigs();
        List<ConfigRule> newConfigRules = new ArrayList<>();

        for (PolicyRuleActionConfig actionConfig : actionConfigs) {
            ConfigRuleCustom configRuleCustom =
                    new ConfigRuleCustom(actionConfig.getActionKey(), actionConfig.getActionValue());
            ConfigRuleTypeCustom configRuleType = new ConfigRuleTypeCustom(configRuleCustom);
            ConfigRule configRule = new ConfigRule(configActionEnum, configRuleType);
            newConfigRules.add(configRule);
        }

        var deserializedServiceUni = contextService.getService(policyRuleService.getServiceId());
        deserializedServiceUni
                .subscribe()
                .with(
                        deserializedService -> {
                            List<ConfigRule> configRules =
                                    deserializedService.getServiceConfig().getConfigRules();
                            configRules.addAll(newConfigRules);
                            deserializedService.setServiceConfig(new ServiceConfig(configRules));
                        });
    }

    private void addServiceConstraint(
            PolicyRuleService policyRuleService, PolicyRuleAction policyRuleAction) {

        List<PolicyRuleActionConfig> actionConfigs = policyRuleAction.getPolicyRuleActionConfigs();
        List<Constraint> constraintList = new ArrayList<>();

        for (PolicyRuleActionConfig actionConfig : actionConfigs) {
            var constraintCustom =
                    new ConstraintCustom(actionConfig.getActionKey(), actionConfig.getActionValue());
            var constraintTypeCustom = new ConstraintTypeCustom(constraintCustom);
            constraintList.add(new Constraint(constraintTypeCustom));
        }

        final var deserializedServiceUni = contextService.getService(policyRuleService.getServiceId());

        deserializedServiceUni
                .subscribe()
                .with(
                        deserializedService -> {
                            deserializedService.appendServiceConstraints(constraintList);
                            serviceService.updateService(deserializedService);
                            setPolicyRuleServiceToContext(policyRuleService, ENFORCED_POLICYRULE_STATE);
                        });
    }

    private void applyActionService(String alarmId) {
        PolicyRuleService policyRuleService = alarmPolicyRuleServiceMap.get(alarmId);
        PolicyRuleAction policyRuleAction = policyRuleActionMap.get(alarmId);

        setPolicyRuleServiceToContext(policyRuleService, ACTIVE_POLICYRULE_STATE);

        switch (policyRuleAction.getPolicyRuleActionEnum()) {
            case POLICY_RULE_ACTION_ADD_SERVICE_CONSTRAINT:
                addServiceConstraint(policyRuleService, policyRuleAction);
            case POLICY_RULE_ACTION_ADD_SERVICE_CONFIGRULE:
                addServiceConfigRule(policyRuleService, policyRuleAction);
            default:
                LOGGER.errorf(INVALID_MESSAGE, policyRuleAction.getPolicyRuleActionEnum());
                return;
        }
    }

    private void validateDevice(PolicyRuleDevice policyRuleDevice) {
        final var deviceIds = policyRuleDevice.getDeviceIds();
        final var policyRuleId = policyRuleDevice.getPolicyRuleBasic().getPolicyRuleId();

        final var invalidDeviceIds = returnInvalidDeviceIds(deviceIds);
        if (!invalidDeviceIds.isEmpty()) {
            String ids = "";
            for (String id : invalidDeviceIds) {
                ids += " ," + id;
            }

            String message =
                    String.format(
                            "The following devices in PolicyRuleDevice with ID: %s are not valid: %s",
                            policyRuleId, ids);
            setPolicyRuleDeviceToContext(
                    policyRuleDevice, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        createAlarmDescriptorsForDevices(policyRuleDevice);
    }

    private void createAlarmDescriptorsForDevices(PolicyRuleDevice policyRuleDevice) {
        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();

        List<AlarmDescriptor> alarmDescriptorList =
                parsePolicyRuleCondition(policyRuleDevice.getPolicyRuleBasic());

        if (alarmDescriptorList.isEmpty()) {
            String message =
                    String.format(
                            "The devices of PolicyRuleDevice with ID: %s are not valid",
                            policyRuleBasic.getPolicyRuleId());
            setPolicyRuleDeviceToContext(
                    policyRuleDevice, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        setPolicyRuleDeviceToContext(policyRuleDevice, VALIDATED_POLICYRULE_STATE);
        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            alarmPolicyRuleDeviceMap.put(alarmDescriptor.getAlarmId(), policyRuleDevice);
        }

        final var policyRuleTypeService = new PolicyRuleTypeDevice(policyRuleDevice);
        final var policyRule = new PolicyRule(policyRuleTypeService);
        monitorKpi(alarmDescriptorList);
        provisionAlarm(policyRule, alarmDescriptorList, false);
        return;
    }

    private void validateUpdatedPolicyService(PolicyRuleService policyRuleService) {

        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();
        final var isUpdatedPolicyRuleValid =
                policyRuleConditionValidator.validateUpdatedPolicyRuleId(policyRuleBasic.getPolicyRuleId());

        isUpdatedPolicyRuleValid
                .subscribe()
                .with(
                        policyRuleBoolean -> {
                            if (Boolean.FALSE.equals(policyRuleBoolean)) {

                                String message =
                                        String.format(
                                                "The PolicyRule with ID: %s was not found. PolicyUpdateService failed.",
                                                policyRuleBasic.getPolicyRuleId());
                                setPolicyRuleServiceToContext(
                                        policyRuleService,
                                        new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
                                return;
                            }

                            validateService(policyRuleService);
                        });
    }

    private void validateUpdatedPolicyDevice(PolicyRuleDevice policyRuleDevice) {

        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();
        final var isUpdatedPolicyRuleValid =
                policyRuleConditionValidator.validateUpdatedPolicyRuleId(policyRuleBasic.getPolicyRuleId());

        isUpdatedPolicyRuleValid
                .subscribe()
                .with(
                        policyRuleBoolean -> {
                            if (Boolean.FALSE.equals(policyRuleBoolean)) {
                                String message =
                                        String.format(
                                                "PolicyRule with ID: %s was not found. PolicyUpdateDevice failed",
                                                policyRuleBasic.getPolicyRuleId());
                                setPolicyRuleDeviceToContext(
                                        policyRuleDevice,
                                        new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
                                return;
                            }
                            validateDevice(policyRuleDevice);
                        });
    }

    private void validateService(PolicyRuleService policyRuleService) {
        final var serviceId = policyRuleService.getServiceId();
        final var deviceIds = policyRuleService.getDeviceIds();
        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();

        Boolean isServiceIdValid =
                policyRuleConditionValidator.validateServiceId(serviceId).await().indefinitely();

        if (!isServiceIdValid) {
            String message =
                    String.format(
                            "Cannot provision/update a PolicyRule with invalid service ID: %s",
                            policyRuleBasic.getPolicyRuleId());
            setPolicyRuleServiceToContext(
                    policyRuleService, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        Boolean isServicesDeviceIdsValid =
                policyRuleConditionValidator
                        .isServicesDeviceIdsValid(serviceId, deviceIds)
                        .await()
                        .indefinitely();

        if (!isServicesDeviceIdsValid) {

            String message =
                    String.format(
                            "Cannot provision/update a PolicyRule with invalid service ID: %s",
                            policyRuleBasic.getPolicyRuleId());
            setPolicyRuleServiceToContext(
                    policyRuleService, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        setPolicyRuleServiceToContext(policyRuleService, VALIDATED_POLICYRULE_STATE);

        createAlarmDescriptorsForService(policyRuleService);
    }

    private void createAlarmDescriptorsForService(PolicyRuleService policyRuleService) {
        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();

        List<AlarmDescriptor> alarmDescriptorList =
                parsePolicyRuleCondition(policyRuleService.getPolicyRuleBasic());

        if (alarmDescriptorList.isEmpty()) {
            String message =
                    String.format(
                            "Invalid PolicyRuleConditions in PolicyRule with ID: %s",
                            policyRuleBasic.getPolicyRuleId());
            setPolicyRuleServiceToContext(
                    policyRuleService, new PolicyRuleState(PolicyRuleStateEnum.POLICY_FAILED, message));
            return;
        }

        setPolicyRuleServiceToContext(policyRuleService, VALIDATED_POLICYRULE_STATE);

        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            alarmPolicyRuleServiceMap.put(alarmDescriptor.getAlarmId(), policyRuleService);
        }

        final var policyRuleTypeService = new PolicyRuleTypeService(policyRuleService);
        final var policyRule = new PolicyRule(policyRuleTypeService);
        provisionAlarm(policyRule, alarmDescriptorList, true);
        return;
    }

    private List<AlarmDescriptor> parsePolicyRuleCondition(PolicyRuleBasic policyRuleBasic) {
        BooleanOperator booleanOperator = policyRuleBasic.getBooleanOperator();
        if (booleanOperator == BooleanOperator.POLICYRULE_CONDITION_BOOLEAN_OR) {
            return parsePolicyRuleConditionOr(policyRuleBasic);
        }
        if (booleanOperator == BooleanOperator.POLICYRULE_CONDITION_BOOLEAN_AND) {
            return Arrays.asList(parsePolicyRuleConditionAnd(policyRuleBasic));
        }
        return List.of();
    }

    private List<AlarmDescriptor> parsePolicyRuleConditionOr(PolicyRuleBasic policyRuleBasic) {

        List<PolicyRuleCondition> policyRuleConditions = policyRuleBasic.getPolicyRuleConditions();
        List<AlarmDescriptor> alarmDescriptorList = new ArrayList<>();

        for (PolicyRuleCondition policyRuleCondition : policyRuleConditions) {
            var kpiValueRange = convertPolicyRuleConditionToKpiValueRange(policyRuleCondition);

            // TODO: Temp fix for AlarmDescriptor object
            AlarmDescriptor alarmDescriptor =
                    new AlarmDescriptor(
                            "alarmId-" + gen(),
                            "alarmDescription",
                            "alarmName-" + gen(),
                            policyRuleCondition.getKpiId(),
                            kpiValueRange,
                            getTimeStamp());

            alarmDescriptorList.add(alarmDescriptor);
        }

        HashMap<String, PolicyRuleAction> policyRuleActionMap = new HashMap<>();
        List<PolicyRuleAction> policyRuleActions = policyRuleBasic.getPolicyRuleActions();

        for (int i = 0; i < policyRuleActions.size(); i++) {
            policyRuleActionMap.put(alarmDescriptorList.get(i).getAlarmId(), policyRuleActions.get(i));
        }

        return alarmDescriptorList;
    }

    private AlarmDescriptor parsePolicyRuleConditionAnd(PolicyRuleBasic policyRuleBasic) {

        // TODO: KpiIds should be the same. Add check.

        List<PolicyRuleCondition> policyRuleConditionList = policyRuleBasic.getPolicyRuleConditions();
        List<String> kpisList = new ArrayList<String>();

        for (PolicyRuleCondition policyRuleCondition : policyRuleConditionList) {
            kpisList.add(policyRuleCondition.getKpiId());
        }

        if (policyRuleConditionList.size() > 1) {
            return createAlarmDescriptorWithRange(policyRuleConditionList);
        }

        return createAlarmDescriptorWithoutRange(policyRuleConditionList.get(0));
    }

    private AlarmDescriptor createAlarmDescriptorWithoutRange(
            PolicyRuleCondition policyRuleCondition) {

        final var kpiId = policyRuleCondition.getKpiId();
        final var kpiValueRange = convertPolicyRuleConditionToKpiValueRange(policyRuleCondition);

        return new AlarmDescriptor(
                "alarmId-" + gen(),
                "alarmDescription",
                "alarmName-" + gen(),
                kpiId,
                kpiValueRange,
                getTimeStamp());
    }

    private AlarmDescriptor createAlarmDescriptorWithRange(
            List<PolicyRuleCondition> policyRuleConditionList) {

        final var kpiId = policyRuleConditionList.get(0).getKpiId();

        HashMap<String, KpiValueRange> KpiValueRangeMap = new HashMap<>();
        for (PolicyRuleCondition policyRuleCondition : policyRuleConditionList) {

            if (!KpiValueRangeMap.containsKey(kpiId)) {
                var kpiValueRange = convertPolicyRuleConditionToKpiValueRange(policyRuleCondition);
                KpiValueRangeMap.put(kpiId, kpiValueRange);
                continue;
            }

            var kpiValueRange = convertPolicyRuleConditionToKpiValueRange(policyRuleCondition);
            // TODO: Handle combineKpiValueRanges exceptions
            var combinedKpiValueRange =
                    combineKpiValueRanges(kpiId, KpiValueRangeMap.get(kpiId), kpiValueRange);
            KpiValueRangeMap.put(kpiId, combinedKpiValueRange);
        }

        return new AlarmDescriptor(
                "alarmId-" + gen(),
                "alarmDescription",
                "alarmName-" + gen(),
                kpiId,
                KpiValueRangeMap.get(kpiId),
                getTimeStamp());
    }

    private KpiValueRange convertPolicyRuleConditionToKpiValueRange(
            PolicyRuleCondition policyRuleCondition) {

        switch (policyRuleCondition.getNumericalOperator()) {
            case POLICY_RULE_CONDITION_NUMERICAL_EQUAL:
                return new KpiValueRange(
                        policyRuleCondition.getKpiValue(), policyRuleCondition.getKpiValue(), true, true, true);
            case POLICY_RULE_CONDITION_NUMERICAL_NOT_EQUAL:
                return new KpiValueRange(
                        policyRuleCondition.getKpiValue(),
                        policyRuleCondition.getKpiValue(),
                        true,
                        false,
                        false);

            case POLICY_RULE_CONDITION_NUMERICAL_GREATER_THAN:
                return new KpiValueRange(policyRuleCondition.getKpiValue(), null, false, false, false);

            case POLICY_RULE_CONDITION_NUMERICAL_GREATER_THAN_EQUAL:
                return new KpiValueRange(policyRuleCondition.getKpiValue(), null, false, true, false);

            case POLICY_RULE_CONDITION_NUMERICAL_LESS_THAN:
                return new KpiValueRange(null, policyRuleCondition.getKpiValue(), false, false, false);

            case POLICY_RULE_CONDITION_NUMERICAL_LESS_THAN_EQUAL:
                return new KpiValueRange(null, policyRuleCondition.getKpiValue(), false, false, true);
            default:
                return null;
        }
    }

    private KpiValueRange combineKpiValueRanges(
            String kpiId, KpiValueRange firstKpiValueRange, KpiValueRange secondKpiValueRange) {
        if (secondKpiValueRange.getInRange() == true) {
            LOGGER.errorf("KpiId: %s, has already range values", kpiId);
            return null;
        }

        if ((firstKpiValueRange.getKpiMinValue() != null)
                && (secondKpiValueRange.getKpiMinValue() != null)) {
            LOGGER.errorf("KpiId: %s, has already min value", kpiId);
            return null;
        }

        if ((firstKpiValueRange.getKpiMaxValue() != null)
                && (secondKpiValueRange.getKpiMinValue() != null)) {
            LOGGER.errorf("KpiId: %s, has already max value", kpiId);
            return null;
        }

        // Objects.nonNull(secondKpiValueRange);

        var kpiMinValue =
                firstKpiValueRange.getKpiMinValue() != null
                        ? firstKpiValueRange.getKpiMinValue()
                        : secondKpiValueRange.getKpiMinValue();
        var kpiMaxValue =
                firstKpiValueRange.getKpiMaxValue() != null
                        ? firstKpiValueRange.getKpiMaxValue()
                        : secondKpiValueRange.getKpiMaxValue();
        boolean includeMinValue =
                firstKpiValueRange.getIncludeMinValue() || secondKpiValueRange.getIncludeMinValue();
        boolean includeMaxValue =
                firstKpiValueRange.getIncludeMaxValue() || secondKpiValueRange.getIncludeMaxValue();

        return new KpiValueRange(kpiMinValue, kpiMaxValue, true, includeMinValue, includeMaxValue);
    }

    private List<String> returnInvalidDeviceIds(List<String> deviceIds) {
        var invalidDeviceIds = new ArrayList<String>();

        if (!deviceIds.isEmpty()) {

            for (String deviceId : deviceIds) {
                final var validatedDeviceId = policyRuleConditionValidator.validateDeviceId(deviceId);

                validatedDeviceId
                        .subscribe()
                        .with(
                                deviceIdBoolean -> {
                                    if (Boolean.FALSE.equals(deviceIdBoolean)) {
                                        invalidDeviceIds.add(deviceId);
                                    }
                                });
            }

        } else {
            LOGGER.warnf("No deviceIds found");
        }

        return invalidDeviceIds;
    }

    private void setPolicyRuleToContext(PolicyRule policyRule, PolicyRuleState policyRuleState) {
        final var policyRuleType = policyRule.getPolicyRuleType();
        final var policyRuleTypeSpecificType = policyRuleType.getPolicyRuleType();

        if (policyRuleTypeSpecificType instanceof PolicyRuleService) {
            setPolicyRuleServiceToContext(
                    (PolicyRuleService) policyRuleTypeSpecificType, policyRuleState);
        }
        if (policyRuleTypeSpecificType instanceof PolicyRuleDevice) {
            setPolicyRuleDeviceToContext((PolicyRuleDevice) policyRuleTypeSpecificType, policyRuleState);
        }
    }

    private void setPolicyRuleServiceToContext(
            PolicyRuleService policyRuleService, PolicyRuleState policyRuleState) {
        LOGGER.infof("Setting Policy Rule state to [%s]", policyRuleState.toString());

        final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();
        policyRuleBasic.setPolicyRuleState(policyRuleState);
        policyRuleService.setPolicyRuleBasic(policyRuleBasic);

        final var policyRuleTypeService = new PolicyRuleTypeService(policyRuleService);
        final var policyRule = new PolicyRule(policyRuleTypeService);
        contextService.setPolicyRule(policyRule);
    }

    private void setPolicyRuleDeviceToContext(
            PolicyRuleDevice policyRuleDevice, PolicyRuleState policyRuleState) {
        LOGGER.infof("Setting Policy Rule state to [%s]", policyRuleState.toString());

        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();
        policyRuleBasic.setPolicyRuleState(policyRuleState);
        policyRuleDevice.setPolicyRuleBasic(policyRuleBasic);

        final var policyRuleTypeService = new PolicyRuleTypeDevice(policyRuleDevice);
        final var policyRule = new PolicyRule(policyRuleTypeService);
        contextService.setPolicyRule(policyRule);
    }
}
