/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.rest.action.cat;

import com.google.common.collect.Maps;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.Table;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.rest.*;
import org.elasticsearch.rest.action.support.RestActionListener;
import org.elasticsearch.rest.action.support.RestResponseListener;
import org.elasticsearch.rest.action.support.RestTable;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.threadpool.ThreadPoolStats;

import java.util.*;

import static org.elasticsearch.rest.RestRequest.Method.GET;

public class RestThreadPoolAction extends AbstractCatAction {

    private final static String[] SUPPORTED_NAMES = new String[]{
            ThreadPool.Names.BULK,
            ThreadPool.Names.FLUSH,
            ThreadPool.Names.GENERIC,
            ThreadPool.Names.GET,
            ThreadPool.Names.INDEX,
            ThreadPool.Names.MANAGEMENT,
            ThreadPool.Names.MERGE,
            ThreadPool.Names.OPTIMIZE,
            ThreadPool.Names.PERCOLATE,
            ThreadPool.Names.REFRESH,
            ThreadPool.Names.SEARCH,
            ThreadPool.Names.SNAPSHOT,
            ThreadPool.Names.SUGGEST,
            ThreadPool.Names.WARMER
    };

    private final static String[] SUPPORTED_ALIASES = new String[]{
            "b",
            "f",
            "ge",
            "g",
            "i",
            "ma",
            "m",
            "o",
            "p",
            "r",
            "s",
            "sn",
            "su",
            "w"
    };

    static {
        assert SUPPORTED_ALIASES.length == SUPPORTED_NAMES.length: "SUPPORTED_NAMES/ALIASES mismatch";
    }

    private final static String[] DEFAULT_THREAD_POOLS = new String[]{
            ThreadPool.Names.BULK,
            ThreadPool.Names.INDEX,
            ThreadPool.Names.SEARCH,
    };

    private final static Map<String, String> ALIAS_TO_THREAD_POOL;
    private final static Map<String, String> THREAD_POOL_TO_ALIAS;

    static {
        ALIAS_TO_THREAD_POOL = Maps.newHashMapWithExpectedSize(SUPPORTED_NAMES.length);
        for (String supportedThreadPool : SUPPORTED_NAMES) {
            ALIAS_TO_THREAD_POOL.put(supportedThreadPool.substring(0, 3), supportedThreadPool);
        }
        THREAD_POOL_TO_ALIAS = Maps.newHashMapWithExpectedSize(SUPPORTED_NAMES.length);
        for (int i = 0; i < SUPPORTED_NAMES.length; i++) {
            THREAD_POOL_TO_ALIAS.put(SUPPORTED_NAMES[i], SUPPORTED_ALIASES[i]);
        }
    }

    @Inject
    public RestThreadPoolAction(Settings settings, RestController controller, Client client) {
        super(settings, controller, client);
        controller.registerHandler(GET, "/_cat/thread_pool", this);
    }

    @Override
    void documentation(StringBuilder sb) {
        sb.append("/_cat/thread_pool\n");
    }

    @Override
    public void doRequest(final RestRequest request, final RestChannel channel, final Client client) {
        final ClusterStateRequest clusterStateRequest = new ClusterStateRequest();
        clusterStateRequest.clear().nodes(true);
        clusterStateRequest.local(request.paramAsBoolean("local", clusterStateRequest.local()));
        clusterStateRequest.masterNodeTimeout(request.paramAsTime("master_timeout", clusterStateRequest.masterNodeTimeout()));

        client.admin().cluster().state(clusterStateRequest, new RestActionListener<ClusterStateResponse>(channel) {
            @Override
            public void processResponse(final ClusterStateResponse clusterStateResponse) {
                NodesInfoRequest nodesInfoRequest = new NodesInfoRequest();
                nodesInfoRequest.clear().process(true).threadPool(true);
                client.admin().cluster().nodesInfo(nodesInfoRequest, new RestActionListener<NodesInfoResponse>(channel) {
                    @Override
                    public void processResponse(final NodesInfoResponse nodesInfoResponse) {
                        NodesStatsRequest nodesStatsRequest = new NodesStatsRequest();
                        nodesStatsRequest.clear().threadPool(true);
                        client.admin().cluster().nodesStats(nodesStatsRequest, new RestResponseListener<NodesStatsResponse>(channel) {
                            @Override
                            public RestResponse buildResponse(NodesStatsResponse nodesStatsResponse) throws Exception {
                                return RestTable.buildResponse(buildTable(request, clusterStateResponse, nodesInfoResponse, nodesStatsResponse), channel);
                            }
                        });
                    }
                });
            }
        });
    }

    @Override
    Table getTableWithHeader(final RestRequest request) {
        Table table = new Table();
        table.startHeaders();
        table.addCell("id", "default:false;alias:nodeId;desc:unique node id");
        table.addCell("pid", "default:false;alias:p;desc:process id");
        table.addCell("host", "alias:h;desc:host name");
        table.addCell("ip", "alias:i;desc:ip address");
        table.addCell("port", "default:false;alias:po;desc:bound transport port");

        final String[] requestedPools = fetchSortedPools(request, DEFAULT_THREAD_POOLS);
        for (String pool : SUPPORTED_NAMES) {
            String poolAlias = THREAD_POOL_TO_ALIAS.get(pool);
            boolean display = false;
            for (String requestedPool : requestedPools) {
                if (pool.equals(requestedPool)) {
                    display = true;
                    break;
                }
            }

            String defaultDisplayVal = Boolean.toString(display);
            table.addCell(
                    pool + ".type",
                    "alias:" + poolAlias + "t;default:false;desc:" + pool + " thread pool type"
            );
            table.addCell(
                    pool + ".active",
                    "alias:" + poolAlias + "a;default:" + defaultDisplayVal + ";text-align:right;desc:number of active " + pool + " threads"
            );
            table.addCell(
                    pool + ".size",
                    "alias:" + poolAlias + "s;default:false;text-align:right;desc:number of " + pool + " threads"
            );
            table.addCell(
                    pool + ".queue",
                    "alias:" + poolAlias + "q;default:" + defaultDisplayVal + ";text-align:right;desc:number of " + pool + " threads in queue"
            );
            table.addCell(
                    pool + ".queueSize",
                    "alias:" + poolAlias + "qs;default:false;text-align:right;desc:maximum number of " + pool + " threads in queue"
            );
            table.addCell(
                    pool + ".rejected",
                    "alias:" + poolAlias + "r;default:" + defaultDisplayVal + ";text-align:right;desc:number of rejected " + pool + " threads"
            );
            table.addCell(
                    pool + ".largest",
                    "alias:" + poolAlias + "l;default:false;text-align:right;desc:highest number of seen active " + pool + " threads"
            );
            table.addCell(
                    pool + ".completed",
                    "alias:" + poolAlias + "c;default:false;text-align:right;desc:number of completed " + pool + " threads"
            );
            table.addCell(
                    pool + ".min",
                    "alias:" + poolAlias + "mi;default:false;text-align:right;desc:minimum number of " + pool + " threads"
            );
            table.addCell(
                    pool + ".max",
                    "alias:" + poolAlias + "ma;default:false;text-align:right;desc:maximum number of " + pool + " threads"
            );
            table.addCell(
                    pool + ".keepAlive",
                    "alias:" + poolAlias + "k;default:false;text-align:right;desc:" + pool + " thread keep alive time"
            );
        }

        table.endHeaders();
        return table;
    }


    private Table buildTable(RestRequest req, ClusterStateResponse state, NodesInfoResponse nodesInfo, NodesStatsResponse nodesStats) {
        boolean fullId = req.paramAsBoolean("full_id", false);
        DiscoveryNodes nodes = state.getState().nodes();
        Table table = getTableWithHeader(req);

        for (DiscoveryNode node : nodes) {
            NodeInfo info = nodesInfo.getNodesMap().get(node.id());
            NodeStats stats = nodesStats.getNodesMap().get(node.id());
            table.startRow();

            table.addCell(fullId ? node.id() : Strings.substring(node.getId(), 0, 4));
            table.addCell(info == null ? null : info.getProcess().id());
            table.addCell(node.getHostName());
            table.addCell(node.getHostAddress());
            if (node.address() instanceof InetSocketTransportAddress) {
                table.addCell(((InetSocketTransportAddress) node.address()).address().getPort());
            } else {
                table.addCell("-");
            }

            final Map<String, ThreadPoolStats.Stats> poolThreadStats;
            final Map<String, ThreadPool.Info> poolThreadInfo;

            if (stats == null) {
                poolThreadStats = Collections.emptyMap();
                poolThreadInfo = Collections.emptyMap();
            } else {
                poolThreadStats = new HashMap<>(14);
                poolThreadInfo = new HashMap<>(14);

                ThreadPoolStats threadPoolStats = stats.getThreadPool();
                for (ThreadPoolStats.Stats threadPoolStat : threadPoolStats) {
                    poolThreadStats.put(threadPoolStat.getName(), threadPoolStat);
                }
                if (info != null) {
                    for (ThreadPool.Info threadPoolInfo : info.getThreadPool()) {
                        poolThreadInfo.put(threadPoolInfo.getName(), threadPoolInfo);
                    }
                }
            }
            for (String pool : SUPPORTED_NAMES) {
                ThreadPoolStats.Stats poolStats = poolThreadStats.get(pool);
                ThreadPool.Info poolInfo = poolThreadInfo.get(pool);

                Long maxQueueSize = null;
                String keepAlive = null;
                Integer minThreads = null;
                Integer maxThreads = null;

                if (poolInfo != null) {
                    if (poolInfo.getQueueSize() != null) {
                        maxQueueSize = poolInfo.getQueueSize().singles();
                    }
                    if (poolInfo.getKeepAlive() != null) {
                        keepAlive = poolInfo.getKeepAlive().toString();
                    }
                    if (poolInfo.getMin() >= 0) {
                        minThreads = poolInfo.getMin();
                    }
                    if (poolInfo.getMax() >= 0) {
                        maxThreads = poolInfo.getMax();
                    }
                }

                table.addCell(poolInfo == null  ? null : poolInfo.getType());
                table.addCell(poolStats == null ? null : poolStats.getActive());
                table.addCell(poolStats == null ? null : poolStats.getThreads());
                table.addCell(poolStats == null ? null : poolStats.getQueue());
                table.addCell(maxQueueSize);
                table.addCell(poolStats == null ? null : poolStats.getRejected());
                table.addCell(poolStats == null ? null : poolStats.getLargest());
                table.addCell(poolStats == null ? null : poolStats.getCompleted());
                table.addCell(minThreads);
                table.addCell(maxThreads);
                table.addCell(keepAlive);
            }

            table.endRow();
        }

        return table;
    }

    // The thread pool columns should always be in the same order.
    private String[] fetchSortedPools(RestRequest request, String[] defaults) {
        String[] headers = request.paramAsStringArray("h", null);
        if (headers == null) {
            return defaults;
        } else {
            Set<String> requestedPools = new LinkedHashSet<>(headers.length);
            for (String header : headers) {
                int dotIndex = header.indexOf('.');
                if (dotIndex != -1) {
                    String headerPrefix = header.substring(0, dotIndex);
                    if (THREAD_POOL_TO_ALIAS.containsKey(headerPrefix)) {
                        requestedPools.add(headerPrefix);
                    }
                } else if (ALIAS_TO_THREAD_POOL.containsKey(header)) {
                    requestedPools.add(ALIAS_TO_THREAD_POOL.get(header));
                }

            }
            return requestedPools.toArray(new String[requestedPools.size()]);
        }
    }
}
