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

package io.opentelemetry.javaagent.tooling.instrumentation;

import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.BOOTSTRAP_LOADER;

import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TransformSafeLogger;
import io.opentelemetry.javaagent.tooling.Utils;
import io.opentelemetry.javaagent.tooling.config.AgentConfig;
import io.opentelemetry.javaagent.tooling.muzzle.Mismatch;
import io.opentelemetry.javaagent.tooling.muzzle.ReferenceMatcher;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.utility.JavaModule;

/**
 * A ByteBuddy matcher that decides whether this instrumentation should be applied. Calls generated
 * {@link ReferenceMatcher}: if any mismatch with the passed {@code classLoader} is found this
 * instrumentation is skipped.
 */
class MuzzleMatcher implements AgentBuilder.RawMatcher {

  private static final Logger muzzleLogger = Logger.getLogger(MuzzleMatcher.class.getName());

  private final TransformSafeLogger instrumentationLogger;
  private final InstrumentationModule instrumentationModule;
  private final Level muzzleLogLevel;
  private final AtomicBoolean initialized = new AtomicBoolean(false);
  private final Cache<ClassLoader, Boolean> matchCache = Cache.weak();
  private volatile ReferenceMatcher referenceMatcher;

  MuzzleMatcher(
      TransformSafeLogger instrumentationLogger,
      InstrumentationModule instrumentationModule,
      ConfigProperties config) {
    this.instrumentationLogger = instrumentationLogger;
    this.instrumentationModule = instrumentationModule;
    this.muzzleLogLevel = AgentConfig.isDebugModeEnabled(config) ? WARNING : FINE;
  }

  @Override
  public boolean matches(
      TypeDescription typeDescription,
      ClassLoader classLoader,
      JavaModule module,
      Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain) {
    if (classLoader == BOOTSTRAP_LOADER) {
      classLoader = Utils.getBootstrapProxy();
    }
    return matchCache.computeIfAbsent(classLoader, this::doesMatch);
  }

  private boolean doesMatch(ClassLoader classLoader) {
    ReferenceMatcher muzzle = getReferenceMatcher();
    boolean isMatch = muzzle.matches(classLoader);

    if (!isMatch) {
      MuzzleFailureCounter.inc();
      if (muzzleLogger.isLoggable(muzzleLogLevel)) {
        muzzleLogger.log(
            muzzleLogLevel,
            "Instrumentation skipped, mismatched references were found: {0} [class {1}] on {2}",
            new Object[] {
              instrumentationModule.instrumentationName(),
              instrumentationModule.getClass().getName(),
              classLoader
            });
        List<Mismatch> mismatches = muzzle.getMismatchedReferenceSources(classLoader);
        for (Mismatch mismatch : mismatches) {
          muzzleLogger.log(muzzleLogLevel, "-- {0}", mismatch);
        }
      }
    } else {
      if (instrumentationLogger.isLoggable(FINE)) {
        instrumentationLogger.log(
            FINE,
            "Applying instrumentation: {0} [class {1}] on {2}",
            new Object[] {
              instrumentationModule.instrumentationName(),
              instrumentationModule.getClass().getName(),
              classLoader
            });
      }
    }

    return isMatch;
  }

  // ReferenceMatcher is lazily created to avoid unnecessarily loading the muzzle references from
  // the module during the agent setup
  private ReferenceMatcher getReferenceMatcher() {
    if (initialized.compareAndSet(false, true)) {
      referenceMatcher = ReferenceMatcher.of(instrumentationModule);
    }
    return referenceMatcher;
  }
}
