/*
 * 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 com.alibaba.metrics.rest;

import com.alibaba.fastjson.JSON;
import com.alibaba.metrics.ClusterHistogram;
import com.alibaba.metrics.Compass;
import com.alibaba.metrics.Counter;
import com.alibaba.metrics.FastCompass;
import com.alibaba.metrics.Gauge;
import com.alibaba.metrics.Histogram;
import com.alibaba.metrics.IMetricManager;
import com.alibaba.metrics.Meter;
import com.alibaba.metrics.MetricFilter;
import com.alibaba.metrics.MetricLevel;
import com.alibaba.metrics.MetricManager;
import com.alibaba.metrics.MetricName;
import com.alibaba.metrics.MetricRegistry;
import com.alibaba.metrics.ReservoirType;
import com.alibaba.metrics.Timer;
import com.alibaba.metrics.common.CollectLevel;
import com.alibaba.metrics.common.MetricObject;
import com.alibaba.metrics.common.MetricsCollector;
import com.alibaba.metrics.common.MetricsCollectorFactory;
import com.alibaba.metrics.common.filter.MetricNameSetFilter;
import com.alibaba.metrics.server.MetricsSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

import static com.alibaba.metrics.rest.Utils.buildResult;
import static com.alibaba.metrics.rest.Utils.buildResultPojo;

@Path("/metrics")
public class MetricsResource {

    private final static Logger logger = LoggerFactory.getLogger(MetricsResource.class);

    private static IMetricManager manager = MetricManager.getIMetricManager();
    private static final double rateFactor = TimeUnit.SECONDS.toSeconds(1);
    private static final double durationFactor = 1.0 / TimeUnit.MILLISECONDS.toNanos(1);
    private static final String MULTI_GROUP_DELIM = ",";

    private static final MetricName baseName = new MetricName("middleware.metrics.rest.url");
    /**
     * list接口序列化MetricObject时过滤掉timestamp/value
     */
    private static final MetricObjectPropertyFilter filter = new MetricObjectPropertyFilter();

    @Path("/list")
    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    public Response listMetrics() {
        if (manager.isEnabled()) {
            Map<String, Set<MetricObject>> metrics = new LinkedHashMap<String, Set<MetricObject>>();
            for (String groupName : manager.listMetricGroups()) {
                MetricRegistry registry = manager.getMetricRegistryByGroup(groupName);
                Set<MetricObject> metricsPerRegistry = new LinkedHashSet<MetricObject>();
                metricsPerRegistry.addAll(buildMetricRegistry(registry));
                metrics.put(groupName, metricsPerRegistry);
            }
            try {
                String data = JSON.toJSONString(buildResultPojo(metrics, true, ""), filter);
                return Response.ok(data).build();
            } catch (Exception e) {
                return buildResult(null, false, e.toString());
            }
        } else {
            return buildResult(null, false, "Metrics has been disabled explicitly!");
        }
    }

    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    public Response getMetrics() {
        return getMetrics("all");
    }

    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/{group}")
    public Response getMetrics(@DefaultValue("all") @PathParam("group") String group) {

        MetricName name = baseName.tagged("url", "/" + group).level(MetricLevel.TRIVIAL);
        Timer urlTimer = manager.getTimer("metrics", name, ReservoirType.BUCKET);
        Timer.Context context = urlTimer.time();

        try {
            if (!manager.isEnabled()) {
                return buildResult(null, false, "Metrics has been disabled explicitly!");
            }

            Map<String, List<MetricObject>> metricsData = new TreeMap<String, List<MetricObject>>();

            if ("all".equalsIgnoreCase(group) || group == null) {
                for (String groupName : manager.listMetricGroups()) {
                    try {
                        MetricRegistry registry = manager.getMetricRegistryByGroup(groupName);
                        metricsData.put(groupName, buildMetricRegistry(registry));

                    } catch (Throwable e) {
                        return buildResult(null, false, e.toString());
                    }
                }

                return buildResult(metricsData, true, "");

            } else if (group.contains(MULTI_GROUP_DELIM)) {

                String[] groups = group.split(MULTI_GROUP_DELIM);

                for (String groupName : groups) {
                    try {
                        // FIXME manager.getMetricRegistryByGroup will create one if group does not exist
                        if (!manager.listMetricGroups().contains(groupName)) {
                            continue;
                        }
                        MetricRegistry registry = manager.getMetricRegistryByGroup(groupName);
                        metricsData.put(groupName, buildMetricRegistry(registry));
                    } catch (Throwable e) {
                        return buildResult(null, false, e.toString());
                    }
                }

                return buildResult(metricsData, true, "");

            } else {
                // FIXME manager.getMetricRegistryByGroup will create one if group does not exist
                List<String> groups = manager.listMetricGroups();
                if (!groups.contains(group)) {
                    return buildResult(null, false, "The specified group is not found!");
                }
                MetricRegistry registry = manager.getMetricRegistryByGroup(group);
                return buildResult(buildMetricRegistry(registry), true, "");
            }

        } finally {
            context.stop();
        }
    }

    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/{group}/level/{level}")
    public Response getMetricByLevel(final @PathParam("group") String group,
                                     final @PathParam("level") String level,
                                     final @QueryParam("above") boolean above) {
        if (!manager.isEnabled()) {
            return Response.status(Response.Status.FORBIDDEN).build();
        }

        if (group.contains(MULTI_GROUP_DELIM)) {
            Map<String, List<MetricObject>> metricsData = new TreeMap<String, List<MetricObject>>();

            String[] groups = group.split(MULTI_GROUP_DELIM);

            String[] levels = level.split(MULTI_GROUP_DELIM);

            for (int i = 0; i < groups.length; i++) {
                try {
                    // FIXME manager.getMetricRegistryByGroup will create one if group does not exist
                    if (!manager.listMetricGroups().contains(groups[i])) {
                        continue;
                    }
                    MetricRegistry registry = manager.getMetricRegistryByGroup(groups[i]);
                    MetricLevelFilter levelFilter = new MetricLevelFilter(MetricLevel.valueOf(levels[i]), above);
                    metricsData.put(groups[i], buildMetricRegistry(registry, levelFilter));
                } catch (Throwable e) {
                    return buildResult(null, false, e.toString());
                }
            }
            return buildResult(metricsData, true, "");

        } else {
            // FIXME manager.getMetricRegistryByGroup will create one if group does not exist
            List<String> groups = manager.listMetricGroups();

            if (!groups.contains(group)) {
                return buildResult(null, false, "The specified group is not found!");
            }

            MetricRegistry registry = manager.getMetricRegistryByGroup(group);

            try {
                MetricLevelFilter levelFilter = new MetricLevelFilter(MetricLevel.valueOf(level), above);
                List<MetricObject> metricObjects = buildMetricRegistry(registry, levelFilter);
                if (metricObjects.isEmpty()) {
                    return buildResult(null, false, "No metric matching the specified level found!");
                }
                return buildResult(metricObjects);
            } catch (IllegalArgumentException e) {
                return buildResult(null, false, e.toString());
            }
        }
    }

    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/{group}/{metric}")
    public Response getMetric(final @PathParam("group") String group,
                              final @PathParam("metric") String metricName,
                              final @QueryParam("tagKey") List<String> tagKeys,
                              final @QueryParam("tagValue") List<String> tagValues) {

        if (!manager.isEnabled()) {
            return Response.status(Response.Status.FORBIDDEN).build();
        }

        List<String> groups = manager.listMetricGroups();
        // FIXME manager.getMetricRegistryByGroup will create one if group does not exist
        if (!groups.contains(group)) {
            return buildResult(null, false, "The specified group is not found!");
        }

        MetricRegistry registry = manager.getMetricRegistryByGroup(group);

        List<MetricObject> metricObjects = buildMetricRegistry(registry, new MetricNameSetFilter(metricName));

        if (metricObjects.isEmpty()) {
            return buildResult(null, false, "The specified metric is not found!");
        }

        return buildResult(metricObjects);
    }

    @GET
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/specific")
    public Response getMetric(final @QueryParam("metric") Set<String> metricNames, @QueryParam("zeroIgnore") boolean zeroIgnore) {

        if (!manager.isEnabled()) {
            return Response.status(Response.Status.FORBIDDEN).build();
        }

        MetricName name = baseName.tagged("url", "/specific").level(MetricLevel.TRIVIAL);
        Timer urlTimer = manager.getTimer("metrics", name, ReservoirType.BUCKET);
        Timer.Context context = urlTimer.time();
        try {
            return getMetricsInternal(metricNames, zeroIgnore);
        } finally {
            context.stop();
        }
    }

    @POST
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/specific")
    public Response getMetricsByPost(MultivaluedMap<String, String> params, @QueryParam("zeroIgnore") boolean zeroIgnore) {
        final Set<String> metricNames = new HashSet<String>(params.get("metric"));
        return getMetric(metricNames, zeroIgnore);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({Constants.PRODUCE_JSON_WITH_QUALITY_SOURCE, MediaType.TEXT_HTML})
    @Path("/search")
    public Response searchMetrics(String metricsSearch, @HeaderParam("REMOTE_ADDR") String remoteAddr,
            @HeaderParam("X-Forwarded-For") String xForwardedFor, @Context HttpServletRequest httpServletRequest) {

        if (httpServletRequest != null){
            logger.info("httpheader REMOTE_ADDR {}, httpheader X-Forwarded-For {};socket remoteaddr {}, port {}",
                    remoteAddr, xForwardedFor, httpServletRequest.getRemoteAddr(), httpServletRequest.getRemotePort());
        }else{
            logger.info("httpheader REMOTE_ADDR {}, httpheader X-Forwarded-For {},httpServletRequest is null",
                    remoteAddr, xForwardedFor);
        }


        if (!manager.isEnabled()) {
            return Response.status(Response.Status.FORBIDDEN).build();
        }

        MetricName name = baseName.tagged("url", "/search").level(MetricLevel.TRIVIAL);
        Timer urlTimer = manager.getTimer("metrics", name, ReservoirType.BUCKET);
        Timer.Context context = urlTimer.time();
        try {
            MetricsSearchService service = MetricsSearchService.getInstance();
            return Response.ok(service.search(metricsSearch)).build();
        } catch (Throwable e) {
            logger.error("Error during handing search request: ", e);
            return Response.serverError().build();
        } finally {
            context.stop();
        }
    }

    private Response getMetricsInternal(final Set<String> metricNames, boolean zeroIgnore) {
        // FIXME manager.getMetricRegistryByGroup will create one if group does not exist

        List<String> groups = manager.listMetricGroups();

        MetricFilter keyFilter = new MetricNameSetFilter(metricNames);

        List<MetricObject> metricObjects = new ArrayList<MetricObject>();

        for (String group : groups) {
            MetricRegistry registry = manager.getMetricRegistryByGroup(group);
            metricObjects.addAll(buildMetricRegistry(registry, keyFilter));
        }

        if (metricObjects.isEmpty()) {
            return buildResult(null, false, "The specified metric is not found!");
        }

        if (zeroIgnore){
            List<MetricObject> allMetricObjects = metricObjects;
            metricObjects = new ArrayList<MetricObject>();

            for(MetricObject o : allMetricObjects){
                if ((o != null) && (!Utils.checkZero(o.getValue()))){
                    metricObjects.add(o);
                }
            }
        }

        return buildResult(metricObjects);
    }

    private List<MetricObject> buildMetricRegistry(MetricRegistry registry) {
        return buildMetricRegistry(registry, null);
    }

    private List<MetricObject> buildMetricRegistry(MetricRegistry registry, MetricFilter filter) {

        long ts = System.currentTimeMillis();

        MetricsCollector collector =
                MetricsCollectorFactory.createNew(CollectLevel.NORMAL, rateFactor, durationFactor, filter);

        SortedMap<MetricName, Gauge> gauges = filter == null ?
                registry.getGauges() : registry.getGauges(filter);
        for (Map.Entry<MetricName, Gauge> entry : gauges.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, Counter> counters = filter == null ?
                registry.getCounters() : registry.getCounters(filter);
        for (Map.Entry<MetricName, Counter> entry : counters.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, Meter> meters = filter == null ?
                registry.getMeters() : registry.getMeters(filter);
        for (Map.Entry<MetricName, Meter> entry : meters.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, Histogram> histograms = filter == null ?
                registry.getHistograms() : registry.getHistograms(filter);
        for (Map.Entry<MetricName, Histogram> entry : histograms.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, Timer> timers = filter == null ?
                registry.getTimers() : registry.getTimers(filter);
        for (Map.Entry<MetricName, Timer> entry : timers.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, Compass> compasses = filter == null ?
                registry.getCompasses() : registry.getCompasses(filter);
        for (Map.Entry<MetricName, Compass> entry : compasses.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, FastCompass> fastCompasses = filter == null ?
                registry.getFastCompasses() : registry.getFastCompasses(filter);
        for (Map.Entry<MetricName, FastCompass> entry : fastCompasses.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        SortedMap<MetricName, ClusterHistogram> clusterHistograms = filter == null ?
                registry.getClusterHistograms() : registry.getClusterHistograms(filter);
        for (Map.Entry<MetricName, ClusterHistogram> entry : clusterHistograms.entrySet()) {
            collector.collect(entry.getKey(), entry.getValue(), ts);
        }

        return collector.build();
    }

}