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

package io.opentelemetry.javaagent.tooling.muzzle;

import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.HelperInjector;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;

/**
 * This class verifies that a given {@link ClassLoader} satisfies all expectations of a given {@link
 * InstrumentationModule}. It is used to make sure than a module's transformations can be safely
 * applied to a given class loader.
 *
 * @see InstrumentationModule
 * @see ReferenceMatcher
 */
public class ClassLoaderMatcher {

  /**
   * For all {@link InstrumentationModule}s found in the current thread's context class loader calls
   * {@link #matches(InstrumentationModule, ClassLoader, boolean)} and returns the aggregated
   * result.
   *
   * <p>The returned map will be empty if and only if no instrumentation modules were found.
   */
  public static Map<String, List<Mismatch>> matchesAll(
      ClassLoader classLoader, boolean injectHelpers, Set<String> excludedInstrumentationNames) {
    disableMatcherCache();

    Map<String, List<Mismatch>> result = new HashMap<>();
    ServiceLoader.load(InstrumentationModule.class, ClassLoaderMatcher.class.getClassLoader())
        .forEach(
            module -> {
              if (module.instrumentationNames().stream()
                  .noneMatch(excludedInstrumentationNames::contains)) {
                result.put(
                    module.getClass().getName(), matches(module, classLoader, injectHelpers));
              }
            });
    return result;
  }

  /**
   * Returns a list of {@link Mismatch}s between expectations of the given {@link
   * InstrumentationModule} and what the given ClassLoader can provide.
   */
  private static List<Mismatch> matches(
      InstrumentationModule instrumentationModule, ClassLoader classLoader, boolean injectHelpers) {
    List<Mismatch> mismatches = checkReferenceMatcher(instrumentationModule, classLoader);
    mismatches = checkModuleClassLoaderMatcher(instrumentationModule, classLoader, mismatches);
    if (injectHelpers) {
      mismatches = checkHelperInjection(instrumentationModule, classLoader, mismatches);
    }
    return mismatches;
  }

  private static List<Mismatch> checkReferenceMatcher(
      InstrumentationModule instrumentationModule, ClassLoader classLoader) {
    ReferenceMatcher muzzle = ReferenceMatcher.of(instrumentationModule);
    return muzzle.getMismatchedReferenceSources(classLoader);
  }

  private static List<Mismatch> checkModuleClassLoaderMatcher(
      InstrumentationModule instrumentationModule,
      ClassLoader classLoader,
      List<Mismatch> mismatches) {
    if (!instrumentationModule.classLoaderMatcher().matches(classLoader)) {
      mismatches =
          ReferenceMatcher.add(mismatches, new Mismatch.InstrumentationModuleClassLoaderMismatch());
    }
    return mismatches;
  }

  private static List<Mismatch> checkHelperInjection(
      InstrumentationModule instrumentationModule,
      ClassLoader classLoader,
      List<Mismatch> mismatches) {
    try {
      // verify helper injector works
      List<String> allHelperClasses =
          InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule);
      HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl();
      instrumentationModule.registerHelperResources(helperResourceBuilder);
      if (!allHelperClasses.isEmpty()) {
        new HelperInjector(
                instrumentationModule.instrumentationName(),
                allHelperClasses,
                helperResourceBuilder.getResources(),
                ClassLoaderMatcher.class.getClassLoader(),
                null)
            .transform(null, null, classLoader, null, null);
      }
    } catch (RuntimeException e) {
      mismatches = ReferenceMatcher.add(mismatches, new Mismatch.HelperClassesInjectionError());
    }
    return mismatches;
  }

  private static void disableMatcherCache() {
    try {
      Class<?> matcherClass =
          Class.forName(
              "io.opentelemetry.javaagent.extension.matcher.ClassLoaderHasClassesNamedMatcher");
      Field field = matcherClass.getDeclaredField("useCache");
      field.setAccessible(true);
      field.setBoolean(null, false);
    } catch (Exception exception) {
      throw new IllegalStateException(
          "Failed to disable cache for ClassLoaderHasClassesNamedMatcher", exception);
    }
  }

  private ClassLoaderMatcher() {}
}
