/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.nifi.cluster.integration;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.curator.test.TestingServer;
import org.apache.nifi.cluster.coordination.flow.FlowElection;
import org.apache.nifi.cluster.coordination.flow.PopularVoteFlowElection;
import org.apache.nifi.cluster.coordination.node.ClusterRoles;
import org.apache.nifi.controller.status.history.StatusHistoryRepository;
import org.apache.nifi.controller.status.history.VolatileComponentStatusRepository;
import org.apache.nifi.nar.ExtensionDiscoveringManager;
import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class Cluster {
    private static final Logger logger = LoggerFactory.getLogger(Cluster.class);

    private final Set<Node> nodes = new HashSet<>();
    private final TestingServer zookeeperServer;

    private final long flowElectionTimeoutMillis;
    private final Integer flowElectionMaxNodes;

    public Cluster() throws IOException {
        this(3, TimeUnit.SECONDS, 3);
    }

    public Cluster(final long flowElectionTimeout, final TimeUnit flowElectionTimeUnit, final Integer flowElectionMaxNodes) throws IOException {
        try {
            zookeeperServer = new TestingServer();
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

        this.flowElectionTimeoutMillis = flowElectionTimeUnit.toMillis(flowElectionTimeout);
        this.flowElectionMaxNodes = flowElectionMaxNodes;
    }


    public void start() {
        try {
            zookeeperServer.start();
        } catch (final RuntimeException e) {
            throw e;
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

        while (getZooKeeperConnectString() == null) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
            }
        }

        logger.info("Start ZooKeeper Server on Port {}, with temporary directory {}", zookeeperServer.getPort(), zookeeperServer.getTempDirectory());
    }

    public void stop() {
        for (final Node node : nodes) {
            try {
                if (node.isRunning()) {
                    node.stop();
                }
            } catch (Exception e) {
                logger.error("Failed to shut down " + node, e);
            }
        }

        try {
            zookeeperServer.stop();
            zookeeperServer.close();
        } catch (final Exception e) {
        }
    }


    public String getZooKeeperConnectString() {
        return zookeeperServer.getConnectString();
    }

    public Set<Node> getNodes() {
        return Collections.unmodifiableSet(nodes);
    }

    public CuratorFramework createCuratorClient() {
        final RetryPolicy retryPolicy = new RetryNTimes(20, 500);
        final CuratorFramework curatorClient = CuratorFrameworkFactory.builder()
            .connectString(getZooKeeperConnectString())
            .sessionTimeoutMs(3000)
            .connectionTimeoutMs(3000)
            .retryPolicy(retryPolicy)
            .defaultData(new byte[0])
            .build();

        curatorClient.start();
        return curatorClient;
    }

    public Node createNode() {
        final Map<String, String> addProps = new HashMap<>();
        addProps.put(NiFiProperties.ZOOKEEPER_CONNECT_STRING, getZooKeeperConnectString());
        addProps.put(NiFiProperties.CLUSTER_IS_NODE, "true");

        final NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties("src/test/resources/conf/nifi.properties", addProps);

        final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
        final FlowElection flowElection = new PopularVoteFlowElection(flowElectionTimeoutMillis, TimeUnit.MILLISECONDS, flowElectionMaxNodes);
        final StatusHistoryRepository statusHistoryRepository = new VolatileComponentStatusRepository(nifiProperties);
        final Node node = new Node(nifiProperties, extensionManager, flowElection, statusHistoryRepository);
        node.start();
        nodes.add(node);

        return node;
    }

    public Node getCurrentClusterCoordinator() {
        return getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null);
    }

    public Node waitForClusterCoordinator(final long time, final TimeUnit timeUnit) {
        return ClusterUtils.waitUntilNonNull(time, timeUnit,
            () -> getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null));
    }

    public Node waitForPrimaryNode(final long time, final TimeUnit timeUnit) {
        return ClusterUtils.waitUntilNonNull(time, timeUnit,
            () -> getNodes().stream().filter(node -> node.hasRole(ClusterRoles.PRIMARY_NODE)).findFirst().orElse(null));
    }

    /**
     * Waits for each node in the cluster to connect. The time given is the maximum amount of time to wait for each node to connect, not for
     * the entire cluster to connect.
     *
     * @param time the max amount of time to wait for a node to connect
     * @param timeUnit the unit of time that the given <code>time</code> value represents
     */
    public void waitUntilAllNodesConnected(final long time, final TimeUnit timeUnit) {
        for (final Node node : nodes) {
            node.waitUntilConnected(time, timeUnit);
        }
    }
}
