/*
 * 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.cassandra.tools.nodetool;

import java.io.PrintStream;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.google.common.collect.LinkedHashMultimap;

import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
import org.apache.cassandra.tools.NodeProbe;

import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import static java.lang.String.format;

@Command(name = "ring", description = "Print information about the token ring")
public class Ring extends AbstractCommand
{
    @Parameters(description = "Specify a keyspace for accurate ownership information (topology awareness)", index = "0", arity = "0..1")
    private String keyspace = null;

    @Option(paramLabel = "resolve_ip", names = { "-r", "--resolve-ip" }, description = "Show node domain names instead of IPs")
    private boolean resolveIp = false;

    @Mixin
    private PrintPortMixin printPortMixin = new PrintPortMixin();

    private PrintStream out;
    private EndpointSnitchInfoMBean epSnitchInfo;
    private Collection<String> liveNodes, deadNodes, joiningNodes, leavingNodes, movingNodes;
    private Map<String, String> loadMap;

    @Override
    public void execute(NodeProbe probe)
    {
        out = probe.output().out;
        liveNodes = probe.getLiveNodes(true);
        deadNodes = probe.getUnreachableNodes(true);
        joiningNodes = probe.getJoiningNodes(true);
        leavingNodes = probe.getLeavingNodes(true);
        movingNodes = probe.getMovingNodes(true);
        loadMap = probe.getLoadMap(true);
        epSnitchInfo = probe.getEndpointSnitchInfoProxy();

        Map<String, String> tokensToEndpoints = probe.getTokenToEndpointMap(true);
        LinkedHashMultimap<String, String> endpointsToTokens = LinkedHashMultimap.create();
        boolean haveVnodes = false;
        for (Map.Entry<String, String> entry : tokensToEndpoints.entrySet())
        {
            haveVnodes |= endpointsToTokens.containsKey(entry.getValue());
            endpointsToTokens.put(entry.getValue(), entry.getKey());
        }

        int maxAddressLength = Collections.max(endpointsToTokens.keys(),
                                               Comparator.comparingInt(String::length)).length();

        String formatPlaceholder = "%%-%ds  %%-12s%%-7s%%-8s%%-16s%%-20s%%-44s%%n";
        String format = format(formatPlaceholder, maxAddressLength);

        StringBuilder errors = new StringBuilder();
        boolean showEffectiveOwnership = true;

        // Calculate per-token ownership of the ring
        Map<String, Float> ownerships = null;
        try
        {
            ownerships = probe.effectiveOwnershipWithPort(keyspace);
        }
        catch (IllegalStateException ex)
        {
            try
            {
                ownerships = probe.getOwnershipWithPort();
                errors.append("Note: ").append(ex.getMessage()).append("%n");
                showEffectiveOwnership = false;
            }
            catch (Exception e)
            {
                out.printf("%nError: %s%n", ex.getMessage());
                System.exit(1);
            }
        }
        catch (IllegalArgumentException ex)
        {
            out.printf("%nError: %s%n", ex.getMessage());
            System.exit(1);
        }

        out.println();
        for (Entry<String, SetHostStatWithPort> entry : CommandUtils.getOwnershipByDcWithPort(probe, resolveIp, tokensToEndpoints, ownerships).entrySet())
            printDc(format, entry.getKey(), endpointsToTokens, entry.getValue(), showEffectiveOwnership);

        if (haveVnodes)
        {
            out.println("  Warning: \"nodetool ring\" is used to output all the tokens of a node.");
            out.println("  To view status related info of a node use \"nodetool status\" instead.\n");
        }

        out.printf("%n  " + errors);
    }

    private void printDc(String format, String dc,
                         LinkedHashMultimap<String, String> endpointsToTokens,
                         SetHostStatWithPort hoststats, boolean showEffectiveOwnership)
    {
        out.println("Datacenter: " + dc);
        out.println("==========");

        // get the total amount of replicas for this dc and the last token in this dc's ring
        List<String> tokens = new ArrayList<>();
        String lastToken = "";

        for (HostStatWithPort stat : hoststats)
        {
            tokens.addAll(endpointsToTokens.get(stat.endpointWithPort.getHostAddressAndPort()));
            lastToken = tokens.get(tokens.size() - 1);
        }

        out.printf(format, "Address", "Rack", "Status", "State", "Load", "Owns", "Token");

        if (hoststats.size() > 1)
            out.printf(format, "", "", "", "", "", "", lastToken);
        else
            out.println();

        for (HostStatWithPort stat : hoststats)
        {
            String endpoint = stat.endpointWithPort.getHostAddressAndPort();
            String rack;
            try
            {
                rack = epSnitchInfo.getRack(endpoint);
            }
            catch (UnknownHostException e)
            {
                rack = "Unknown";
            }

            String status = liveNodes.contains(endpoint)
                            ? "Up"
                            : deadNodes.contains(endpoint)
                              ? "Down"
                              : "?";

            String state = "Normal";

            if (joiningNodes.contains(endpoint))
                state = "Joining";
            else if (leavingNodes.contains(endpoint))
                state = "Leaving";
            else if (movingNodes.contains(endpoint))
                state = "Moving";

            String load = loadMap.getOrDefault(endpoint, "?");
            String owns = stat.owns != null && showEffectiveOwnership? new DecimalFormat("##0.00%").format(stat.owns) : "?";
            out.printf(format, stat.ipOrDns(printPortMixin.printPort), rack, status, state, load, owns, stat.token);
        }
        out.println();
    }
}
