/* LanguageTool, a natural language style checker
 * Copyright (C) 2019 Fabian Richter
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.server;

import com.google.common.cache.Cache;
import io.prometheus.client.Counter;
import io.prometheus.client.Gauge;
import io.prometheus.client.Histogram;
import io.prometheus.client.Info;
import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.guava.cache.CacheMetricsCollector;
import io.prometheus.client.hotspot.DefaultExports;
import org.languagetool.JLanguageTool;
import org.languagetool.Language;
import org.languagetool.Premium;

import java.io.IOException;
import java.util.Objects;

public class ServerMetricsCollector {

  public static final String UNKNOWN = "unknown";

  public enum RequestErrorType {
    QUEUE_FULL,
    TOO_MANY_ERRORS,
    MAX_CHECK_TIME,
    MAX_TEXT_SIZE,
    INVALID_REQUEST
  }

  private static final double[] LATENCY_BUCKETS = {
    0.025, 0.05, .1, .25, .5, .75, 1., 2., 4., 6., 8., 10., 15.
  };

  private static final double[] SIZE_BUCKETS = {
    25, 100, 500, 1000, 2500, 5000, 10000, 20000, 40000
  };

  // buckets for processing speed in chars/s;
  private static final double[] SPEED_BUCKETS = {
    10, 100, 500, 1000, 2500, 5000, 7500, 10000, 20000, 50000
  };

  private static final ServerMetricsCollector collector = new ServerMetricsCollector();
  
  private static HTTPServer server;

  private final Counter matchCounter = Counter
    .build("languagetool_check_matches_total", "Total amount of rule matches")
    .labelNames("language", "mode").register();

  // TODO: deprecate - replaced by histograms below
  private final Counter checkCounter = Counter
    .build("languagetool_checks_total", "Total text checks")
    .labelNames("language", "mode").register();
  private final Counter charactersCounter = Counter
    .build("languagetool_characters_total", "Total processed characters")
    .labelNames("language", "mode").register();
  private final Counter computationTimeCounter = Counter
    .build("languagetool_computation_time_seconds_total", "Total computation time, in seconds")
    .labelNames("language", "mode").register();


  // need to be very careful about cardinality with those
  private final Histogram checkLatency = Histogram
    .build("languagetool_check_latency_seconds", "Histogram of check times")
    .buckets(LATENCY_BUCKETS)
    .labelNames("language", "mode").register();
  private final Histogram checkSize = Histogram
    .build("languagetool_check_size_characters", "Histogram of check sizes")
    .buckets(SIZE_BUCKETS).labelNames("language", "mode").register();
  private final Histogram checkSpeed = Histogram
    .build("languagetool_check_speed_chars_per_second", "Histogram of relative check speed")
    .buckets(SPEED_BUCKETS).labelNames("language", "mode").register();

  private final Counter requestErrorCounter = Counter
    .build("languagetool_request_errors_total", "Various request errors")
    .labelNames("reason").register();

  // TODO add label for route, method, ...?
  private final Counter httpRequestCounter = Counter
    .build("languagetool_http_requests_total", "Received HTTP requests").register();

  private final Counter httpResponseCounter = Counter
    .build("languagetool_http_responses_total", "HTTP responses by code")
    .labelNames("code").register();

  private final Counter failedHealthcheckCounter = Counter
    .build("languagetool_failed_healthchecks_total", "Failed healthchecks").register();

  private final Info buildInfo = Info
    .build("languagetool_build", "Build information").register();

  private final Gauge configValues = Gauge
    .build("languagetool_configuration_values", "Configuration settings").labelNames("name").register();


  private final CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();


  public static void init(HTTPServerConfig config) throws IOException {
    DefaultExports.initialize();
    server = new HTTPServer(config.getPrometheusPort(), true);
    Gauge c = getInstance().configValues;
    exposeConfigurationValues(config, c);
  }

  private static void exposeConfigurationValues(HTTPServerConfig config, Gauge c) {
    c.labels("maxCheckThreads").set(config.getMaxCheckThreads());
    c.labels("maxWorkQueueSize").set(config.getMaxWorkQueueSize());
    c.labels("cacheSize").set(config.getCacheSize());
    c.labels("cacheTTLSeconds").set(config.getCacheTTLSeconds());
    c.labels("maxCheckTimeMillisAnonymous").set(config.getMaxCheckTimeMillisAnonymous());
    c.labels("maxCheckTimeMillisLoggedIn").set(config.getMaxCheckTimeMillisLoggedIn());
    c.labels("maxCheckTimeMillisPremium").set(config.getMaxCheckTimeMillisPremium());
    c.labels("maxTextLengthAnonymous").set(config.getMaxTextLengthAnonymous());
    c.labels("maxTextLengthLoggedIn").set(config.getMaxTextLengthLoggedIn());
    c.labels("maxTextLengthPremium").set(config.getMaxTextLengthPremium());
  }

  public static void stop() {
    server.stop();
  }

  public static ServerMetricsCollector getInstance() {
    return collector;
  }

  public ServerMetricsCollector() {
    buildInfo.info(
            "version", Objects.toString(JLanguageTool.VERSION), 
            "buildDate", Objects.toString(JLanguageTool.BUILD_DATE), 
            "revision", Objects.toString(JLanguageTool.GIT_SHORT_ID), 
            "premium", Objects.toString(String.valueOf(Premium.isPremiumVersion())),
            "premiumVersion", Objects.toString(Premium.get().getVersion()),
            "premiumBuildDate",Objects.toString(Premium.get().getBuildDate()),
            "premiumRevision",Objects.toString(Premium.get().getShortGitId())
    );
  }


  public void monitorCache(String name, Cache cache) {
    cacheMetrics.addCache(name, cache);
  }

  public void logCheck(Language language, long milliseconds, int textSize, int matchCount,
                       JLanguageTool.Mode mode) {
    String langLabel = language != null ? language.getShortCode() : UNKNOWN;
    String modeLabel = mode != null ? mode.name() : UNKNOWN;

    checkCounter.labels(langLabel, modeLabel).inc();
    matchCounter.labels(langLabel, modeLabel).inc(matchCount);

    // split metrics: measure latency by language/mode, but not combined
    // so we can e.g. notice latency increases in a language or
    // create an alert for check speed of ALL_BUT_TEXT_LEVEL_ONLY requests

    charactersCounter.labels(langLabel, modeLabel).inc(textSize);
    checkSize.labels(langLabel, "").observe(textSize);
    checkSize.labels("", modeLabel).observe(textSize);

    double seconds = (double) milliseconds / 1000.0;
    computationTimeCounter.labels(langLabel, modeLabel).inc(seconds);
    checkLatency.labels(langLabel, "").observe(seconds);
    checkLatency.labels("", modeLabel).observe(seconds);

    double speed = textSize / seconds;
    checkSpeed.labels(langLabel, "").observe(speed);
    checkSpeed.labels("", modeLabel).observe(speed);
  }

  public void logRequestError(RequestErrorType type) {
    requestErrorCounter.labels(type.name().toLowerCase()).inc();
  }

  public void logRequest() {
    httpRequestCounter.inc();
  }

  public void logResponse(int httpCode) {
    httpResponseCounter.labels(String.valueOf(httpCode)).inc();
  }

  public void logFailedHealthcheck() {
    failedHealthcheckCounter.inc();
  }

}
