/*
* Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
*
* 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 org.etsi.tfs.policy.policy;

import static org.etsi.tfs.policy.common.ApplicationProperties.*;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.groups.UniJoin;
import io.smallrye.mutiny.subscription.Cancellable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
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.function.Function;
import org.etsi.tfs.policy.context.ContextService;
import org.etsi.tfs.policy.context.model.ConfigActionEnum;
import org.etsi.tfs.policy.context.model.ConfigRule;
import org.etsi.tfs.policy.context.model.ConfigRuleCustom;
import org.etsi.tfs.policy.context.model.ConfigRuleTypeCustom;
import org.etsi.tfs.policy.context.model.Constraint;
import org.etsi.tfs.policy.context.model.ConstraintCustom;
import org.etsi.tfs.policy.context.model.ConstraintTypeCustom;
import org.etsi.tfs.policy.context.model.ServiceConfig;
import org.etsi.tfs.policy.context.model.ServiceId;
import org.etsi.tfs.policy.device.DeviceService;
import org.etsi.tfs.policy.exception.ExternalServiceFailureException;
import org.etsi.tfs.policy.monitoring.MonitoringService;
import org.etsi.tfs.policy.monitoring.model.AlarmDescriptor;
import org.etsi.tfs.policy.monitoring.model.AlarmResponse;
import org.etsi.tfs.policy.monitoring.model.AlarmSubscription;
import org.etsi.tfs.policy.monitoring.model.KpiValueRange;
import org.etsi.tfs.policy.policy.model.BooleanOperator;
import org.etsi.tfs.policy.policy.model.PolicyRule;
import org.etsi.tfs.policy.policy.model.PolicyRuleAction;
import org.etsi.tfs.policy.policy.model.PolicyRuleActionConfig;
import org.etsi.tfs.policy.policy.model.PolicyRuleActionEnum;
import org.etsi.tfs.policy.policy.model.PolicyRuleBasic;
import org.etsi.tfs.policy.policy.model.PolicyRuleCondition;
import org.etsi.tfs.policy.policy.model.PolicyRuleDevice;
import org.etsi.tfs.policy.policy.model.PolicyRuleService;
import org.etsi.tfs.policy.policy.model.PolicyRuleState;
import org.etsi.tfs.policy.policy.model.PolicyRuleStateEnum;
import org.etsi.tfs.policy.policy.model.PolicyRuleTypeDevice;
import org.etsi.tfs.policy.policy.model.PolicyRuleTypeService;
import org.etsi.tfs.policy.policy.service.PolicyRuleConditionFieldsGetter;
import org.etsi.tfs.policy.policy.service.PolicyRuleConditionValidator;
import org.etsi.tfs.policy.service.ServiceService;
import org.jboss.logging.Logger;

@ApplicationScoped
public class PolicyServiceImpl implements PolicyService {

    private static final Logger LOGGER = Logger.getLogger(PolicyServiceImpl.class);

    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;
    // TODO: Find a better way to disregard alarms while reconfiguring path
    // Temporary solution for not calling the same rpc more than it's needed
    private static int noAlarms = 0;

    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<>();
    private ConcurrentHashMap<String, Cancellable> subscriptionList = 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.getExceptionMessage());
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleService.getExceptionMessage());

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

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

        final var serviceId = policyRuleService.getServiceId();
        final var deviceIds = policyRuleService.getDeviceIds();
        final var isServiceValid = policyRuleConditionValidator.isServiceIdValid(serviceId, deviceIds);

        return isServiceValid
                .onFailure()
                .transform(failure -> new ExternalServiceFailureException(failure.getMessage()))
                .onItem()
                .transform(
                        isService ->
                                constructPolicyStateBasedOnCriteria(
                                        isService, serviceId, policyRuleService, policyRuleBasic))
                .flatMap(Function.identity());
    }

    private Uni<PolicyRuleState> constructPolicyStateBasedOnCriteria(
            Boolean isService,
            ServiceId serviceId,
            PolicyRuleService policyRuleService,
            PolicyRuleBasic policyRuleBasic) {

        if (!isService) {
            var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, String.format(INVALID_MESSAGE, serviceId));

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

        final var policyRuleTypeService = new PolicyRuleTypeService(policyRuleService);
        final var policyRule = new PolicyRule(policyRuleTypeService);
        final var alarmDescriptorList = createAlarmDescriptorList(policyRule);

        if (alarmDescriptorList.isEmpty()) {
            var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED,
                            String.format(
                                    "Invalid PolicyRuleConditions in PolicyRule with ID: %s",
                                    policyRuleBasic.getPolicyRuleId()));
            return Uni.createFrom().item(policyRuleState);
        }

        return setPolicyRuleOnContextAndReturnState(policyRule, policyRuleService, alarmDescriptorList);
    }

    private Uni<PolicyRuleState> setPolicyRuleOnContextAndReturnState(
            PolicyRule policyRule,
            PolicyRuleService policyRuleService,
            List<AlarmDescriptor> alarmDescriptorList) {
        return contextService
                .setPolicyRule(policyRule)
                .onFailure()
                .transform(failure -> new ExternalServiceFailureException(failure.getMessage()))
                .onItem()
                .transform(
                        policyId -> {
                            startMonitoringBasedOnAlarmDescriptors(
                                    policyId, policyRuleService, alarmDescriptorList);

                            return VALIDATED_POLICYRULE_STATE;
                        });
    }

    private void startMonitoringBasedOnAlarmDescriptors(
            String policyId,
            PolicyRuleService policyRuleService,
            List<AlarmDescriptor> alarmDescriptorList) {
        setPolicyRuleServiceToContext(policyRuleService, VALIDATED_POLICYRULE_STATE);
        noAlarms = 0;

        List<Uni<String>> alarmIds =
                createAlarmList(alarmDescriptorList); // setAllarmtomonitoring get back alarmid

        List<Multi<AlarmResponse>> alarmResponseStreamList =
                transformAlarmIds(alarmIds, policyRuleService);

        // Merge the promised alarms into one stream (Multi Object)
        final var multi = Multi.createBy().merging().streams(alarmResponseStreamList);
        setPolicyRuleServiceToContext(policyRuleService, PROVISIONED_POLICYRULE_STATE);

        subscriptionList.put(policyId, monitorAlarmResponseForService(multi));

        // TODO: Resubscribe to the stream, if it has ended

        // TODO: Redesign evaluation of action
        // evaluateAction(policyRule, alarmDescriptorList, multi);
    }

    /**
    * Transform the alarmIds into promised alarms returned from the getAlarmResponseStream
    *
    * @param alarmIds the list of alarm ids
    * @param policyRuleService the policy rule service
    * @return
    */
    private List<Multi<AlarmResponse>> transformAlarmIds(
            List<Uni<String>> alarmIds, PolicyRuleService policyRuleService) {
        List<Multi<AlarmResponse>> alarmResponseStreamList = new ArrayList<>();
        for (Uni<String> alarmId : alarmIds) {
            Multi<AlarmResponse> alarmResponseStream =
                    alarmId.onItem().transformToMulti(id -> setPolicyMonitor(policyRuleService, id));

            alarmResponseStreamList.add(alarmResponseStream);
        }
        return alarmResponseStreamList;
    }

    private Multi<AlarmResponse> setPolicyMonitor(PolicyRuleService policyRuleService, String id) {
        alarmPolicyRuleServiceMap.put(id, policyRuleService);

        // TODO: Create infinite subscription
        var alarmSubscription = new AlarmSubscription(id, 259200, 5000);
        return monitoringService.getAlarmResponseStream(alarmSubscription);
    }

    /**
    * Create an alarmIds list that contains the promised ids returned from setKpiAlarm
    *
    * @param alarmDescriptorList the list of alarm descriptors
    * @return the list of alarm descriptors
    */
    public List<Uni<String>> createAlarmList(List<AlarmDescriptor> alarmDescriptorList) {
        List<Uni<String>> alarmIds = new ArrayList<Uni<String>>();
        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            LOGGER.infof("alarmDescriptor:");
            LOGGER.infof(alarmDescriptor.toString());
            alarmIds.add(monitoringService.setKpiAlarm(alarmDescriptor));
        }
        return alarmIds;
    }

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

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

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

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

        final var deviceIds = policyRuleDevice.getDeviceIds();
        final var areDevicesValid = returnInvalidDeviceIds(deviceIds);

        return areDevicesValid
                .onFailure()
                .transform(failure -> new ExternalServiceFailureException(failure.getMessage()))
                .onItem()
                .transform(areDevices -> areDeviceOnContext(areDevices, policyRuleDevice, policyRuleBasic))
                .flatMap(Function.identity());
    }

    private Uni<PolicyRuleState> areDeviceOnContext(
            List<Boolean> areDevices,
            PolicyRuleDevice policyRuleDevice,
            PolicyRuleBasic policyRuleBasic) {
        if (areDevices.contains(false)) {
            var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED,
                            String.format(
                                    INVALID_MESSAGE, policyRuleDevice.getPolicyRuleBasic().getPolicyRuleId()));

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

        final var policyRuleTypeDevice = new PolicyRuleTypeDevice(policyRuleDevice);
        final var policyRule = new PolicyRule(policyRuleTypeDevice);

        final var alarmDescriptorList = createAlarmDescriptorList(policyRule);
        if (alarmDescriptorList.isEmpty()) {
            var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED,
                            String.format(
                                    "Invalid PolicyRuleConditions in PolicyRule with ID: %s",
                                    policyRuleBasic.getPolicyRuleId()));
            return Uni.createFrom().item(policyRuleState);
        }

        return contextService
                .setPolicyRule(policyRule)
                .onFailure()
                .transform(failure -> new ExternalServiceFailureException(failure.getMessage()))
                .onItem()
                .transform(
                        policyId -> {
                            startMonitoringBasedOnAlarmDescriptors(
                                    policyId, policyRuleDevice, alarmDescriptorList);
                            return VALIDATED_POLICYRULE_STATE;
                        });
    }

    private void startMonitoringBasedOnAlarmDescriptors(
            String policyId,
            PolicyRuleDevice policyRuleDevice,
            List<AlarmDescriptor> alarmDescriptorList) {
        setPolicyRuleDeviceToContext(policyRuleDevice, VALIDATED_POLICYRULE_STATE);
        noAlarms = 0;

        List<Uni<String>> alarmIds = getAlarmIds(alarmDescriptorList);

        List<Multi<AlarmResponse>> alarmResponseStreamList =
                getAlarmResponse(alarmIds, policyRuleDevice);

        // Merge the promised alarms into one stream (Multi Object)
        final var multi = Multi.createBy().merging().streams(alarmResponseStreamList);
        setPolicyRuleDeviceToContext(policyRuleDevice, PROVISIONED_POLICYRULE_STATE);

        subscriptionList.put(policyId, monitorAlarmResponseForDevice(multi));

        // TODO: Resubscribe to the stream, if it has ended

        // TODO: Redesign evaluation of action
        // evaluateAction(policyRule, alarmDescriptorList, multi);
    }

    private List<Multi<AlarmResponse>> getAlarmResponse(
            List<Uni<String>> alarmIds, PolicyRuleDevice policyRuleDevice) {
        // Transform the alarmIds into promised alarms returned from the
        // getAlarmResponseStream
        List<Multi<AlarmResponse>> alarmResponseStreamList = new ArrayList<>();
        for (Uni<String> alarmId : alarmIds) {
            alarmResponseStreamList.add(
                    alarmId.onItem().transformToMulti(id -> setPolicyMonitoringDevice(policyRuleDevice, id)));
        }
        return alarmResponseStreamList;
    }

    private Multi<AlarmResponse> setPolicyMonitoringDevice(
            PolicyRuleDevice policyRuleDevice, String id) {
        alarmPolicyRuleDeviceMap.put(id, policyRuleDevice);

        // TODO: Create infinite subscription
        var alarmSubscription = new AlarmSubscription(id, 259200, 5000);
        return monitoringService.getAlarmResponseStream(alarmSubscription);
    }

    private List<Uni<String>> getAlarmIds(List<AlarmDescriptor> alarmDescriptorList) {
        List<Uni<String>> alarmIds = new ArrayList<Uni<String>>();
        for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
            LOGGER.infof("alarmDescriptor:");
            LOGGER.infof(alarmDescriptor.toString());
            alarmIds.add(monitoringService.setKpiAlarm(alarmDescriptor));
        }
        return alarmIds;
    }

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

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

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

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

        final var serviceId = policyRuleService.getServiceId();
        final var policyRuleId = policyRuleBasic.getPolicyRuleId();
        final var isPolicyRuleServiceValid =
                policyRuleConditionValidator.isPolicyRuleServiceValid(policyRuleId, serviceId);

        return isPolicyRuleServiceValid
                .onFailure()
                .transform(failure -> new ExternalServiceFailureException(failure.getMessage()))
                .onItem()
                .transform(
                        isPolicyRuleService -> {
                            if (!isPolicyRuleService) {
                                return new PolicyRuleState(
                                        PolicyRuleStateEnum.POLICY_FAILED, String.format(INVALID_MESSAGE, serviceId));
                            }

                            return VALIDATED_POLICYRULE_STATE;
                        });
    }

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

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

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

        final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();
        if (!policyRuleBasic.areArgumentsValid()) {
            final var policyRuleState =
                    new PolicyRuleState(
                            PolicyRuleStateEnum.POLICY_FAILED, policyRuleBasic.getExceptionMessage());
            return Uni.createFrom().item(policyRuleState);
        }

        final var policyRuleId = policyRuleBasic.getPolicyRuleId();
        final var isPolicyRuleValid =
                policyRuleConditionValidator.isUpdatedPolicyRuleIdValid(policyRuleId);

        return isPolicyRuleValid
                .onItem()
                .transform(
                        isPolicyRuleService -> {
                            if (!isPolicyRuleService) {
                                return new PolicyRuleState(
                                        PolicyRuleStateEnum.POLICY_FAILED,
                                        String.format(INVALID_MESSAGE, policyRuleId));
                            }

                            return VALIDATED_POLICYRULE_STATE;
                        });
    }

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

        final var getPolicyRule = contextService.getPolicyRule(policyRuleId);

        return getPolicyRule.onItem().transform(policyRule -> removePolicyFromContext(policyRule));
    }

    private PolicyRuleState removePolicyFromContext(PolicyRule policyRule) {
        var policyRuleBasic = policyRule.getPolicyRuleType().getPolicyRuleBasic();
        String policyId = policyRuleBasic.getPolicyRuleId();

        policyRule
                .getPolicyRuleType()
                .getPolicyRuleBasic()
                .setPolicyRuleState(REMOVED_POLICYRULE_STATE);

        contextService
                .setPolicyRule(policyRule)
                .subscribe()
                .with(
                        tmp ->
                                LOGGER.infof(
                                        "DeletePolicy with id: " + VALID_MESSAGE, policyRuleBasic.getPolicyRuleId()));

        contextService.removePolicyRule(policyId).subscribe().with(x -> {});

        // TODO: When the Map doesn't contains the policyId we should throw an exception?
        if (subscriptionList.contains(policyId)) subscriptionList.get(policyId).cancel();

        return policyRuleBasic.getPolicyRuleState();
    }

    private Uni<List<Boolean>> returnInvalidDeviceIds(List<String> deviceIds) {
        UniJoin.Builder<Boolean> builder = Uni.join().builder();
        for (String deviceId : deviceIds) {
            final var validatedDeviceId = policyRuleConditionValidator.isDeviceIdValid(deviceId);
            builder.add(validatedDeviceId);
        }
        return builder.joinAll().andFailFast();
    }

    private List<AlarmDescriptor> createAlarmDescriptorList(PolicyRule policyRule) {
        final var policyRuleType = policyRule.getPolicyRuleType();
        final var policyRuleTypeSpecificType = policyRuleType.getPolicyRuleType();

        List<AlarmDescriptor> alarmDescriptorList = new ArrayList<>();
        if (policyRuleTypeSpecificType instanceof PolicyRuleService) {
            final var policyRuleService = (PolicyRuleService) policyRuleTypeSpecificType;
            final var policyRuleBasic = policyRuleService.getPolicyRuleBasic();

            alarmDescriptorList = parsePolicyRuleCondition(policyRuleBasic);
            if (alarmDescriptorList.isEmpty()) {
                return List.of();
            }
        } else {
            final var policyRuleDevice = (PolicyRuleDevice) policyRuleTypeSpecificType;
            final var policyRuleBasic = policyRuleDevice.getPolicyRuleBasic();

            alarmDescriptorList = parsePolicyRuleCondition(policyRuleBasic);
            if (alarmDescriptorList.isEmpty()) {
                return List.of();
            }
            for (AlarmDescriptor alarmDescriptor : alarmDescriptorList) {
                alarmPolicyRuleDeviceMap.put(alarmDescriptor.getAlarmId(), policyRuleDevice);
            }
        }

        return alarmDescriptorList;
    }

    private Cancellable monitorAlarmResponseForService(Multi<AlarmResponse> multi) {
        return multi
                .subscribe()
                .with(
                        alarmResponse -> {
                            LOGGER.infof("**************************Received Alarm!**************************");
                            LOGGER.infof("alarmResponse:");
                            LOGGER.info(alarmResponse);
                            LOGGER.info(alarmResponse.getAlarmId());
                            applyActionService(alarmResponse.getAlarmId());
                        });
    }

    private Cancellable monitorAlarmResponseForDevice(Multi<AlarmResponse> multi) {
        return multi
                .subscribe()
                .with(
                        alarmResponse -> {
                            LOGGER.infof("**************************Received Alarm!**************************");
                            LOGGER.infof("alarmResponse:");
                            LOGGER.info(alarmResponse);
                            LOGGER.info(alarmResponse.getAlarmId());
                            applyActionDevice(alarmResponse.getAlarmId());
                        });
    }

    // TODO: To be refactored or deprecated
    //    private void evaluateAction(
    //            PolicyRule policyRule,
    //            List<AlarmDescriptor> alarmDescriptorList,
    //            Multi<AlarmResponse> multi) {
    //
    //        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 as ineffective.\n",
    //                                                alarmDescriptor.getAlarmId()));
    //            }
    //            setPolicyRuleToContext(policyRule, INEFFECTIVE_POLICYRULE_STATE);
    //        } else {
    //            setPolicyRuleToContext(policyRule, EFFECTIVE_POLICYRULE_STATE);
    //        }
    //    }

    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 callRecalculatePathRPC(
            PolicyRuleService policyRuleService, PolicyRuleAction policyRuleAction) {

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

        deserializedServiceUni
                .subscribe()
                .with(
                        deserializedService -> {
                            serviceService
                                    .recomputeConnections(deserializedService)
                                    .subscribe()
                                    .with(
                                            x -> {
                                                LOGGER.info("called recomputeConnections with:");
                                                LOGGER.info(deserializedService);
                                                setPolicyRuleServiceToContext(policyRuleService, ENFORCED_POLICYRULE_STATE);
                                            });
                        });
    }

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

        if (noAlarms == 0) {
            noAlarms++;
            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);
                case POLICY_RULE_ACTION_RECALCULATE_PATH:
                    callRecalculatePathRPC(policyRuleService, policyRuleAction);
                default:
                    LOGGER.errorf(INVALID_MESSAGE, policyRuleAction.getPolicyRuleActionEnum());
                    return;
            }
        } else if (noAlarms == 2) {
            noAlarms = 0;
        } else {
            noAlarms++;
        }
    }

    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(
                            "",
                            "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(
                "", "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(
                "",
                "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(null, policyRuleCondition.getKpiValue(), true, false, false);

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

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

            case POLICY_RULE_CONDITION_NUMERICAL_LESS_THAN_EQUAL:
                return new KpiValueRange(policyRuleCondition.getKpiValue(), null, true, 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 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).subscribe().with(x -> {});
    }

    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).subscribe().with(x -> {});
    }
}
