package resa.optimize;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import resa.util.ConfigUtil;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Created by Tom.fu on 23/4/2014.
 * Chain topology and not tuple split
 */
public class SimpleGeneralServiceModel {

    private static final Logger LOG = LoggerFactory.getLogger(SimpleGeneralServiceModel.class);

    /**
     * Like module A in our discussion
     *
     * @param components, the service node configuration, in this function, chain topology is assumed.
     * @param allocation, can be null input, in this case, directly return Infinity to indicator topology unstable
     * @return Double.MAX_VALUE when a) input allocation is null (i.e., system is unstable)
     * b) any one of the node is unstable (i.e., lambda/mu > 1, in which case, sn.estErlangT will be Double.MAX_VALUE)
     * else the validate estimated erlang service time.
     */
    public static double getErlangGeneralTopCompleteTime(Map<String, ServiceNode> components,
                                                         Map<String, Integer> allocation) {

        if (allocation == null) {
            return Double.MAX_VALUE;
        }
        double retVal = 0.0;
        for (Map.Entry<String, ServiceNode> e : components.entrySet()) {
            String cid = e.getKey();
            ServiceNode sn = e.getValue();
            Integer serverCount = allocation.get(cid);
            // Objects.requireNonNull(serverCount, "No allocation entry find for this component" + cid);
            double est = sn.estErlangT(serverCount);
            if (est < Double.MAX_VALUE) {
                retVal += (est * sn.getI2oRatio());
            } else {
                return Double.MAX_VALUE;
            }
        }
        return retVal;
    }

    public static double getErlangGeneralTopCompleteTimeMilliSec(Map<String, ServiceNode> components,
                                                                 Map<String, Integer> allocation) {
        double result = getErlangGeneralTopCompleteTime(components, allocation);
        return result < Double.MAX_VALUE ? (result * 1000.0) : Double.MAX_VALUE;
    }

    public static Map<String, Integer> getAllocation(Map<String, ServiceNode> components, Map<String, Object> para) {
        Map<String, Integer> retVal = new HashMap<>();

        components.forEach((cid, sn) -> {
            int curr = ConfigUtil.getInt(para, cid, 0);
            retVal.put(cid, curr);
        });
        return retVal;
    }

    public static boolean checkStable(Map<String, ServiceNode> components, Map<String, Integer> allocation) {
        return components.entrySet().stream().map(e -> e.getValue().isStable(allocation.get(e.getKey())))
                .allMatch(Boolean.TRUE::equals);
    }

    public static int getTotalMinRequirement(Map<String, ServiceNode> components) {
        return components.values().stream().mapToInt(ServiceNode::getMinReqServerCount).sum();
    }

    public static void printAllocation(Map<String, Integer> allocation) {
        if (allocation == null) {
            LOG.warn("Null allocation input -> system is unstable.");
        } else {
            LOG.info("allocation->" + allocation);
        }
    }

    /**
     * @param components
     * @param totalResourceCount
     * @return null if a) minReq of any component is Integer.MAX_VALUE (invalid parameter mu = 0.0)
     * b) total minReq can not be satisfied (total minReq > totalResourceCount)
     * otherwise, the Map data structure.
     */
    public static Map<String, Integer> suggestAllocationGeneralTop(Map<String, ServiceNode> components, int totalResourceCount) {
        Map<String, Integer> retVal = components.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
                e -> e.getValue().getMinReqServerCount()));
        int topMinReq = retVal.values().stream().mapToInt(Integer::intValue).sum();

        LOG.info("totalResourceCount: " + totalResourceCount + ", topMinReq: " + topMinReq);
        if (topMinReq <= totalResourceCount) {
            int remainCount = totalResourceCount - topMinReq;
            for (int i = 0; i < remainCount; i++) {
                double maxDiff = -1;
                String maxDiffCid = null;

                for (Map.Entry<String, ServiceNode> e : components.entrySet()) {
                    String cid = e.getKey();
                    ServiceNode sn = e.getValue();
                    int currentAllocated = retVal.get(cid);

                    double beforeAddT = sn.estErlangT(currentAllocated);
                    double afterAddT = sn.estErlangT(currentAllocated + 1);

                    double diff = (beforeAddT - afterAddT) * sn.getI2oRatio();
                    if (diff > maxDiff) {
                        maxDiff = diff;
                        maxDiffCid = cid;
                    }
                }
                if (maxDiffCid != null) {
                    int newAllocate = retVal.compute(maxDiffCid, (k, count) -> count + 1);
                    LOG.debug((i + 1) + " of " + remainCount + ", assigned to " + maxDiffCid + ", newAllocate: "
                            + newAllocate);
                } else {
                    LOG.info("Null MaxDiffCid returned in " + (i + 1) + " of " + remainCount);
                    for (Map.Entry<String, ServiceNode> e : components.entrySet()) {
                        String cid = e.getKey();
                        ServiceNode sn = e.getValue();
                        int currentAllocated = retVal.get(cid);

                        double beforeAddT = sn.estErlangT(currentAllocated);
                        double afterAddT = sn.estErlangT(currentAllocated + 1);

                        LOG.info(cid + ", currentAllocated: " + currentAllocated
                                + ", beforeAddT: " + beforeAddT
                                + ", afterAddT: " + afterAddT);
                    }
                    return retVal;
                }
            }
        } else {
            return null;
        }
        return retVal;
    }

    /**
     * Like Module A', input required QoS, output #threads required
     * Here we separate to two modules: first output allocation, then calculate total #threads included.
     * Caution all the computation involved is in second unit.
     *
     * @param components
     * @param maxAllowedCompleteTime, the unit here is second! consistent with function getErlangChainTopCompleteTime()
     * @param lowerBoundDelta,        this is to set the offset of the lowerBoundServiceTime, we require delta to be positive, and 0 as default.
     * @param adjRatio,               this is to adjust the estimated ErlangServiceTime to fit more closely to the real measured complte time
     * @return null if a) any service node is not in the valid state (mu = 0.0), this is not the case of rho > 1.0, just for checking mu
     * b) lowerBoundServiceTime > requiredQoS
     */
    public static Map<String, Integer> getMinReqServerAllocationGeneralTop(Map<String, ServiceNode> components,
                                                                           double maxAllowedCompleteTime,
                                                                           double lowerBoundDelta,
                                                                           double adjRatio,
                                                                           int maxAvailableExec) {
        double lowerBoundServiceTime = 0.0;
        int totalMinReq = 0;
        for (Map.Entry<String, ServiceNode> e : components.entrySet()) {
            double mu = e.getValue().getMu();
            ///caution, the unit should be millisecond
            lowerBoundServiceTime += (1.0 / mu);
            totalMinReq += e.getValue().getMinReqServerCount();
        }

        Map<String, Integer> currAllocation = null;
        if (lowerBoundServiceTime * adjRatio + lowerBoundDelta < maxAllowedCompleteTime) {
            double currTime;
            do {
                currAllocation = suggestAllocationGeneralTop(components, totalMinReq);
                currTime = getErlangGeneralTopCompleteTime(components, currAllocation) * adjRatio;

                LOG.info("getMinReqServAllcQoS: " + maxAllowedCompleteTime * 1000.0 + ", currTime(ms): "
                        + currTime * 1000.0 / adjRatio + ", currAdj(ms): "
                        + currTime * 1000.0 + ", totalMinReqQoS: " + totalMinReq);

                totalMinReq++;
                //check: we need to check totalMinReq to avoid infinite loop!
            } while (currTime > maxAllowedCompleteTime && totalMinReq <= maxAvailableExec);
        }
        return totalMinReq <= maxAvailableExec ? currAllocation : null;
    }

    public static Map<String, Integer> getMinReqServerAllocationGeneralTop(Map<String, ServiceNode> components,
                                                                           double maxAllowedCompleteTime,
                                                                           int maxAvailableExec) {
        return getMinReqServerAllocationGeneralTop(components, maxAllowedCompleteTime, 0.0, 1.0, maxAvailableExec);
    }

    public static Map<String, Integer> getMinReqServerAllocationGeneralTop(Map<String, ServiceNode> components,
                                                                           double maxAllowedCompleteTime,
                                                                           double adjRatio,
                                                                           int maxAvailableExec) {
        return getMinReqServerAllocationGeneralTop(components, maxAllowedCompleteTime, 0.0, adjRatio, maxAvailableExec);
    }

    public static int totalServerCountInvolved(Map<String, Integer> allocation) {
        return Objects.requireNonNull(allocation).values().stream().mapToInt(i -> i).sum();
    }


    /**
     * @param queueingNetwork
     * @param realLatencyMilliSec
     * @param targetQoSMilliSec
     * @param currBoltAllocation
     * @param maxAvailable4Bolt
     * @return status indicates whether the demanded QoS can be achieved or not
     * minReqAllocaiton, the minimum required resource (under optimized allocation) which can satisfy QoS
     * after: the optimized allocation under given maxAvailable4Bolt
     */
    public static AllocResult checkOptimized(Map<String, ServiceNode> queueingNetwork, double realLatencyMilliSec,
                                             double targetQoSMilliSec, Map<String, Integer> currBoltAllocation,
                                             int maxAvailable4Bolt) {

        ///Caution about the time unit!, second is used in all the functions of calculation
        /// millisecond is used in the output display!
        double estimatedLatencyMilliSec = getErlangGeneralTopCompleteTimeMilliSec(queueingNetwork, currBoltAllocation);

        ///for better estimation, we remain (learn) this ratio, and assume that the estimated is always smaller than real.
        double underEstimateRatio = Math.max(1.0, realLatencyMilliSec / estimatedLatencyMilliSec);
        LOG.info("estLatency(ms): " + estimatedLatencyMilliSec + ", realLatency(ms)" + realLatencyMilliSec
                + ", underEstRatio: " + underEstimateRatio);
        LOG.info("Find out minReqAllocation under QoS requirement.");
        Map<String, Integer> minReqAllocation = getMinReqServerAllocationGeneralTop(queueingNetwork,
                targetQoSMilliSec / 1000.0, underEstimateRatio, maxAvailable4Bolt * 2);
        AllocResult.Status status = AllocResult.Status.FEASIBALE;
        if (minReqAllocation == null) {
            status = AllocResult.Status.INFEASIBLE;
        }
        LOG.info("Find out best allocation given available executors.");
        Map<String, Integer> after = suggestAllocationGeneralTop(queueingNetwork, maxAvailable4Bolt);
        Map<String, Object> context = new HashMap<>();
        context.put("estLatency", estimatedLatencyMilliSec);
        context.put("realLatency", realLatencyMilliSec);
        context.put("underEstRatio", underEstimateRatio);
        return new AllocResult(status, minReqAllocation, after).setContext(context);
    }
}
