/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.instrumentation.api.internal;

import io.opentelemetry.api.trace.SpanKind;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.logging.Logger;

/**
 * This class is internal and is hence not for public use. Its APIs are unstable and can change at
 * any time.
 */
public final class SupportabilityMetrics {
  private static final Logger logger = Logger.getLogger(SupportabilityMetrics.class.getName());
  private final boolean agentDebugEnabled;
  private final Consumer<String> reporter;

  private final ConcurrentMap<String, KindCounters> suppressionCounters = new ConcurrentHashMap<>();
  private final ConcurrentMap<String, AtomicLong> counters = new ConcurrentHashMap<>();

  private static final SupportabilityMetrics INSTANCE =
      new SupportabilityMetrics(
              ConfigPropertiesUtil.getBoolean("otel.javaagent.debug", false), logger::fine)
          .start();

  public static SupportabilityMetrics instance() {
    return INSTANCE;
  }

  // visible for testing
  SupportabilityMetrics(boolean agentDebugEnabled, Consumer<String> reporter) {
    this.agentDebugEnabled = agentDebugEnabled;
    this.reporter = reporter;
  }

  public void recordSuppressedSpan(SpanKind kind, String instrumentationName) {
    if (!agentDebugEnabled) {
      return;
    }

    suppressionCounters
        .computeIfAbsent(instrumentationName, s -> new KindCounters())
        .increment(kind);
  }

  public void incrementCounter(String counterName) {
    if (!agentDebugEnabled) {
      return;
    }

    counters.computeIfAbsent(counterName, k -> new AtomicLong()).incrementAndGet();
  }

  // visible for testing
  void report() {
    suppressionCounters.forEach(
        (instrumentationName, countsByKind) -> {
          for (SpanKind kind : SpanKind.values()) {
            long value = countsByKind.getAndReset(kind);
            if (value > 0) {
              reporter.accept(
                  "Suppressed Spans by '" + instrumentationName + "' (" + kind + ") : " + value);
            }
          }
        });
    counters.forEach(
        (counterName, counter) -> {
          long value = counter.getAndSet(0);
          if (value > 0) {
            reporter.accept("Counter '" + counterName + "' : " + value);
          }
        });
  }

  // this private method is designed for assignment of the return value
  @SuppressWarnings("CanIgnoreReturnValueSuggester")
  private SupportabilityMetrics start() {
    if (agentDebugEnabled) {
      ScheduledExecutorService executor =
          Executors.newScheduledThreadPool(
              1,
              runnable -> {
                Thread result = new Thread(runnable, "supportability_metrics_reporter");
                result.setDaemon(true);
                result.setContextClassLoader(null);
                return result;
              });
      executor.scheduleAtFixedRate(this::report, 5, 5, TimeUnit.SECONDS);
      // the condition below will always be false, but by referencing the executor it ensures the
      // executor can't become unreachable in the middle of the scheduleAtFixedRate() method
      // execution above (and prior to the task being registered), which can lead to the executor
      // being terminated and scheduleAtFixedRate throwing a RejectedExecutionException
      // (see https://bugs.openjdk.org/browse/JDK-8145304)
      if (executor.isTerminated()) {
        throw new AssertionError();
      }
    }
    return this;
  }

  /**
   * This class is internal and is hence not for public use. Its APIs are unstable and can change at
   * any time.
   */
  public static final class CounterNames {
    public static final String SQL_STATEMENT_SANITIZER_CACHE_MISS =
        "SqlStatementSanitizer cache miss";

    private CounterNames() {}
  }

  // this class is threadsafe.
  private static class KindCounters {
    private final AtomicLong server = new AtomicLong();
    private final AtomicLong client = new AtomicLong();
    private final AtomicLong internal = new AtomicLong();
    private final AtomicLong consumer = new AtomicLong();
    private final AtomicLong producer = new AtomicLong();

    void increment(SpanKind kind) {
      switch (kind) {
        case INTERNAL:
          internal.incrementAndGet();
          break;
        case SERVER:
          server.incrementAndGet();
          break;
        case CLIENT:
          client.incrementAndGet();
          break;
        case PRODUCER:
          producer.incrementAndGet();
          break;
        case CONSUMER:
          consumer.incrementAndGet();
          break;
      }
    }

    long getAndReset(SpanKind kind) {
      switch (kind) {
        case INTERNAL:
          return internal.getAndSet(0);
        case SERVER:
          return server.getAndSet(0);
        case CLIENT:
          return client.getAndSet(0);
        case PRODUCER:
          return producer.getAndSet(0);
        case CONSUMER:
          return consumer.getAndSet(0);
      }
      return 0;
    }
  }
}
