// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.maintenance;

import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Deployer;
import com.yahoo.config.provision.NodeType;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.vespa.hosted.provision.node.History;

import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * This maintainer detects changes to nodes that must be expedited, and redeploys affected applications.
 *
 * The purpose of this is to redeploy affected applications faster than achieved by
 * {@link PeriodicApplicationMaintainer}, to reduce the time period where the node repository and the application model
 * is out of sync.
 * 
 * Why can't the manual change directly make the application redeployment?
 *
 * Because we want to queue redeployments to avoid overloading config servers.
 *
 * @author bratseth
 * @author mpolden
 */
public class ExpeditedChangeApplicationMaintainer extends ApplicationMaintainer {
    
    ExpeditedChangeApplicationMaintainer(Deployer deployer, Metric metric, NodeRepository nodeRepository, Duration interval) {
        super(deployer, metric, nodeRepository, interval);
    }

    @Override
    protected Set<ApplicationId> applicationsNeedingMaintenance() {
        var applications = new HashSet<ApplicationId>();

        nodeRepository().nodes()
                        .list()
                        .nodeType(NodeType.tenant, NodeType.proxy)
                        .matching(node -> node.allocation().isPresent())
                        .groupingBy(node -> node.allocation().get().owner())
                        .entrySet()
                        .stream()
                        .filter(entry -> hasNodesWithChanges(entry.getKey(), entry.getValue()))
                        .map(Map.Entry::getKey)
                        .forEach(applications::add);

        // A ready proxy node should trigger a redeployment as it will activate the node.
        if (!nodeRepository().nodes().list(Node.State.ready, Node.State.reserved).nodeType(NodeType.proxy).isEmpty()) {
            applications.add(ApplicationId.from("hosted-vespa", "routing", "default"));
        }

        return applications;
    }

    /**
     * Deploy in the maintenance thread to avoid scheduling multiple deployments of the same application if it takes
     * longer to deploy than the (short) maintenance interval of this
     */
    @Override
    protected void deploy(ApplicationId application) {
        boolean deployed = deployWithLock(application);
        if (deployed)
            log.info("Redeployed application " + application.toShortString() +
                     " as an expedited change was made to its nodes");
    }

    private boolean hasBeenReadied(ApplicationId applicationId, NodeList nodes) {
        Optional<Instant> lastDeployTime = deployer().lastDeployTime(applicationId);
        if (lastDeployTime.isEmpty()) return false;

        return nodes.stream()
                    .flatMap(node -> node.history().events().stream())
                    .filter(event -> event.type() == History.Event.Type.readied)
                    .map(History.Event::at)
                    .anyMatch(e -> lastDeployTime.get().isBefore(e));
    }

    private boolean hasNodesWithChanges(ApplicationId applicationId, NodeList nodes) {
        Optional<Instant> lastDeployTime = deployer().lastDeployTime(applicationId);
        if (lastDeployTime.isEmpty()) return false;

        return nodes.stream()
                    .flatMap(node -> node.history().events().stream())
                    .filter(event -> expediteChangeBy(event.agent()))
                    .map(History.Event::at)
                    .anyMatch(e -> lastDeployTime.get().isBefore(e));
    }

    @Override
    protected boolean canDeployNow(ApplicationId application) {
        return activeNodesByApplication().get(application) != null;
    }

    @Override
    protected Map<ApplicationId, NodeList> activeNodesByApplication() {
        return nodeRepository().nodes()
                               .list(Node.State.active)
                               .not().tester()
                               .groupingBy(node -> node.allocation().get().owner());
    }

    /** Returns whether to expedite changes performed by agent */
    private boolean expediteChangeBy(Agent agent) {
        switch (agent) {
            case operator:
            case RebuildingOsUpgrader:
            case HostEncrypter: return true;
        }
        return false;
    }

}

@@ -3113,531 +3113,8 @@
  %7D%0A%0A
-    private boolean hasBeenReadied(ApplicationId applicationId, NodeList nodes) %7B%0A        Optional%3CInstant%3E lastDeployTime = deployer().lastDeployTime(applicationId);%0A        if (lastDeployTime.isEmpty()) return false;%0A%0A        return nodes.stream()%0A                    .flatMap(node -%3E node.history().events().stream())%0A                    .filter(event -%3E event.type() == History.Event.Type.readied)%0A                    .map(History.Event::at)%0A                    .anyMatch(e -%3E lastDeployTime.get().isBefore(e));%0A    %7D%0A%0A
     

Remove unused method