/*
 * 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 org.apache.polaris.service.config;

import io.micrometer.core.instrument.MeterRegistry;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.context.SmallRyeManagedExecutor;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Startup;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
import java.time.Clock;
import java.util.stream.Collectors;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.auth.AuthorizationState;
import org.apache.polaris.core.auth.DefaultPolarisAuthorizerFactory;
import org.apache.polaris.core.auth.PolarisAuthorizer;
import org.apache.polaris.core.auth.PolarisAuthorizerFactory;
import org.apache.polaris.core.config.RealmConfig;
import org.apache.polaris.core.config.RealmConfigImpl;
import org.apache.polaris.core.config.RealmConfigurationSource;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.context.RequestIdSupplier;
import org.apache.polaris.core.credentials.PolarisCredentialManager;
import org.apache.polaris.core.persistence.BasePersistence;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactoryImpl;
import org.apache.polaris.core.persistence.resolver.Resolver;
import org.apache.polaris.core.persistence.resolver.ResolverFactory;
import org.apache.polaris.core.secrets.UserSecretsManager;
import org.apache.polaris.core.secrets.UserSecretsManagerFactory;
import org.apache.polaris.core.storage.StorageCredentialsVendor;
import org.apache.polaris.core.storage.cache.StorageCredentialCache;
import org.apache.polaris.core.storage.cache.StorageCredentialCacheConfig;
import org.apache.polaris.service.auth.AuthenticationConfiguration;
import org.apache.polaris.service.auth.AuthenticationRealmConfiguration;
import org.apache.polaris.service.auth.AuthenticationType;
import org.apache.polaris.service.auth.Authenticator;
import org.apache.polaris.service.auth.external.OidcConfiguration;
import org.apache.polaris.service.auth.external.tenant.OidcTenantResolver;
import org.apache.polaris.service.auth.internal.broker.TokenBroker;
import org.apache.polaris.service.auth.internal.broker.TokenBrokerFactory;
import org.apache.polaris.service.catalog.api.IcebergRestOAuth2ApiService;
import org.apache.polaris.service.catalog.io.FileIOConfiguration;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.context.RealmContextConfiguration;
import org.apache.polaris.service.context.RealmContextResolver;
import org.apache.polaris.service.credentials.PolarisCredentialManagerConfiguration;
import org.apache.polaris.service.events.PolarisEventListenerConfiguration;
import org.apache.polaris.service.events.listeners.PolarisEventListener;
import org.apache.polaris.service.persistence.PersistenceConfiguration;
import org.apache.polaris.service.ratelimiter.RateLimiter;
import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration;
import org.apache.polaris.service.ratelimiter.TokenBucketConfiguration;
import org.apache.polaris.service.ratelimiter.TokenBucketFactory;
import org.apache.polaris.service.reporting.MetricsReportingConfiguration;
import org.apache.polaris.service.reporting.PolarisMetricsReporter;
import org.apache.polaris.service.secrets.SecretsManagerConfiguration;
import org.apache.polaris.service.storage.StorageConfiguration;
import org.apache.polaris.service.storage.aws.S3AccessConfig;
import org.apache.polaris.service.storage.aws.StsClientsPool;
import org.apache.polaris.service.task.TaskHandlerConfiguration;
import org.apache.polaris.service.tracing.RequestIdFilter;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.eclipse.microprofile.context.ThreadContext;
import org.jboss.resteasy.reactive.server.core.CurrentRequestManager;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.apache.ApacheHttpClient;

public class ServiceProducers {
  private static final Logger LOGGER = LoggerFactory.getLogger(ServiceProducers.class);

  @Produces
  @ApplicationScoped // cannot be singleton because it is mocked in tests
  public Clock clock() {
    return Clock.systemUTC();
  }

  // Polaris core beans - application scope

  @Produces
  @ApplicationScoped
  public StorageCredentialCache storageCredentialCache(
      PolarisDiagnostics diagnostics, StorageCredentialCacheConfig storageCredentialCacheConfig) {
    return new StorageCredentialCache(diagnostics, storageCredentialCacheConfig);
  }

  @Produces
  @Singleton
  public PolarisDiagnostics polarisDiagnostics() {
    return new PolarisDefaultDiagServiceImpl();
  }

  // Polaris core beans - request scope

  @Produces
  @RequestScoped
  public CallContext polarisCallContext(
      RealmContext realmContext,
      RealmConfigurationSource configurationSource,
      MetaStoreManagerFactory metaStoreManagerFactory) {
    BasePersistence metaStoreSession = metaStoreManagerFactory.getOrCreateSession(realmContext);
    return new PolarisCallContext(realmContext, metaStoreSession, configurationSource);
  }

  @Produces
  @RequestScoped
  public RealmConfig realmConfig(
      RealmContext realmContext, RealmConfigurationSource configurationSource) {
    return new RealmConfigImpl(configurationSource, realmContext);
  }

  @Produces
  @ApplicationScoped
  @Identifier("internal")
  public PolarisAuthorizerFactory defaultPolarisAuthorizerFactory() {
    return new DefaultPolarisAuthorizerFactory();
  }

  @Produces
  @ApplicationScoped
  public PolarisAuthorizerFactory polarisAuthorizerFactory(
      AuthorizationConfiguration authorizationConfig,
      @Any Instance<PolarisAuthorizerFactory> authorizerFactories) {
    PolarisAuthorizerFactory factory =
        authorizerFactories.select(Identifier.Literal.of(authorizationConfig.type())).get();
    return factory;
  }

  @Produces
  @RequestScoped
  public PolarisAuthorizer polarisAuthorizer(
      PolarisAuthorizerFactory factory, RealmConfig realmConfig) {
    return factory.create(realmConfig);
  }

  @Produces
  @RequestScoped
  public AuthorizationState authorizationState() {
    return new AuthorizationState();
  }

  @Produces
  @RequestScoped
  public ResolverFactory resolverFactory(
      PolarisDiagnostics diagnostics,
      RealmContext realmContext,
      RealmConfig realmConfig,
      MetaStoreManagerFactory metaStoreManagerFactory,
      CallContext callContext,
      PolarisMetaStoreManager polarisMetaStoreManager) {
    EntityCache entityCache =
        metaStoreManagerFactory.getOrCreateEntityCache(realmContext, realmConfig);
    return (principal, referenceCatalogName) ->
        new Resolver(
            diagnostics,
            callContext.getPolarisCallContext(),
            polarisMetaStoreManager,
            principal,
            entityCache,
            referenceCatalogName);
  }

  @Produces
  @RequestScoped
  public ResolutionManifestFactory resolutionManifestFactory(
      PolarisDiagnostics diagnostics, RealmContext realmContext, ResolverFactory resolverFactory) {
    return new ResolutionManifestFactoryImpl(diagnostics, realmContext, resolverFactory);
  }

  // Polaris service beans - selected from @Identifier-annotated beans

  @Produces
  public RealmContextResolver realmContextResolver(
      RealmContextConfiguration config, @Any Instance<RealmContextResolver> realmContextResolvers) {
    return realmContextResolvers.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  @RequestScoped
  public FileIOFactory fileIOFactory(
      FileIOConfiguration config, @Any Instance<FileIOFactory> fileIOFactories) {
    return fileIOFactories.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  public PolarisEventListener polarisEventListener(
      PolarisEventListenerConfiguration config,
      @Any Instance<PolarisEventListener> polarisEventListeners) {
    return polarisEventListeners.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  public MetaStoreManagerFactory metaStoreManagerFactory(
      PersistenceConfiguration config,
      @Any Instance<MetaStoreManagerFactory> metaStoreManagerFactories) {
    return metaStoreManagerFactories.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  @RequestScoped
  public PolarisMetaStoreManager polarisMetaStoreManager(
      RealmContext realmContext, MetaStoreManagerFactory metaStoreManagerFactory) {
    return metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
  }

  @Produces
  @RequestScoped
  public StorageCredentialsVendor storageCredentialsVendor(
      PolarisMetaStoreManager metaStoreManager, CallContext callContext) {
    return new StorageCredentialsVendor(metaStoreManager, callContext);
  }

  @Produces
  public UserSecretsManagerFactory userSecretsManagerFactory(
      SecretsManagerConfiguration config,
      @Any Instance<UserSecretsManagerFactory> userSecretsManagerFactories) {
    return userSecretsManagerFactories.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  @Singleton
  @Identifier("aws-sdk-http-client")
  public SdkHttpClient sdkHttpClient(S3AccessConfig config) {
    ApacheHttpClient.Builder httpClient = ApacheHttpClient.builder();
    config.maxHttpConnections().ifPresent(httpClient::maxConnections);
    config.readTimeout().ifPresent(httpClient::socketTimeout);
    config.connectTimeout().ifPresent(httpClient::connectionTimeout);
    config.connectionAcquisitionTimeout().ifPresent(httpClient::connectionAcquisitionTimeout);
    config.connectionMaxIdleTime().ifPresent(httpClient::connectionMaxIdleTime);
    config.connectionTimeToLive().ifPresent(httpClient::connectionTimeToLive);
    config.expectContinueEnabled().ifPresent(httpClient::expectContinueEnabled);
    return httpClient.build();
  }

  public void closeSdkHttpClient(
      @Disposes @Identifier("aws-sdk-http-client") SdkHttpClient client) {
    client.close();
  }

  @Produces
  @ApplicationScoped
  public StsClientsPool stsClientsPool(
      @Identifier("aws-sdk-http-client") SdkHttpClient httpClient,
      StorageConfiguration config,
      MeterRegistry meterRegistry) {
    return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry);
  }

  /**
   * Eagerly initialize the in-memory default realm on startup, so that users can check the
   * credentials printed to stdout immediately.
   */
  public void maybeBootstrap(
      @Observes Startup event,
      Bootstrapper bootstrapper,
      PersistenceConfiguration config,
      RealmContextConfiguration realmContextConfiguration) {
    var rootCredentialsSet = RootCredentialsSet.fromEnvironment();
    var rootCredentials = rootCredentialsSet.credentials();
    if (config.isAutoBootstrap()) {
      var realmIds = realmContextConfiguration.realms();

      LOGGER.info(
          "Bootstrapping realm(s) {}, if necessary, from root credentials set provided via the environment variable {} or Java system property {} ...",
          realmIds.stream().map(r -> "'" + r + "'").collect(Collectors.joining(", ")),
          RootCredentialsSet.ENVIRONMENT_VARIABLE,
          RootCredentialsSet.SYSTEM_PROPERTY);

      var result = bootstrapper.bootstrapRealms(realmIds, rootCredentialsSet);

      result.forEach(
          (realm, secrets) -> {
            var principalSecrets = secrets.getPrincipalSecrets();

            var log =
                LOGGER
                    .atInfo()
                    .addArgument(realm)
                    .addArgument(RootCredentialsSet.ENVIRONMENT_VARIABLE)
                    .addArgument(RootCredentialsSet.SYSTEM_PROPERTY);
            if (rootCredentials.containsKey(realm)) {
              log.log(
                  "Realm '{}' automatically bootstrapped, credentials taken from root credentials set provided via the environment variable {} or Java system property {}, not printed to stdout.");
            } else {
              log.log(
                  "Realm '{}' automatically bootstrapped, credentials were not present in root credentials set provided via the environment variable {} or Java system property {}, see separate message printed to stdout.");
              String msg =
                  String.format(
                      "realm: %1s root principal credentials: %2s:%3s",
                      realm,
                      principalSecrets.getPrincipalClientId(),
                      principalSecrets.getMainSecret());
              System.out.println(msg);
            }
          });

      var unusedRealmSecrets =
          realmIds.stream()
              .filter(rootCredentials::containsKey)
              .filter(r -> !result.containsKey(r))
              .map(r -> "'" + r + "'")
              .collect(Collectors.joining(", "));
      if (!unusedRealmSecrets.isEmpty()) {
        // This is intentionally an error to highlight the importance of the situation.
        LOGGER.error(
            "The realms {} are already fully bootstrapped but the secrets are still available via the environment variable {} or Java system property {}. "
                + "Remove this security sensitive information from the environment / Java system properties!",
            unusedRealmSecrets,
            RootCredentialsSet.ENVIRONMENT_VARIABLE,
            RootCredentialsSet.SYSTEM_PROPERTY);
      }
    } else if (!rootCredentials.isEmpty()) {
      // This is intentionally an error to highlight the importance of the situation.
      LOGGER.error(
          "Secrets for the realms {} are available via the environment variable {} or Java system property {}. "
              + "Remove this security sensitive information from the environment / Java system properties!",
          rootCredentials.keySet(),
          RootCredentialsSet.ENVIRONMENT_VARIABLE,
          RootCredentialsSet.SYSTEM_PROPERTY);
    }
  }

  @Produces
  public RateLimiter rateLimiter(
      RateLimiterFilterConfiguration config, @Any Instance<RateLimiter> rateLimiters) {
    return rateLimiters.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  public TokenBucketFactory tokenBucketFactory(
      TokenBucketConfiguration config, @Any Instance<TokenBucketFactory> tokenBucketFactories) {
    return tokenBucketFactories.select(Identifier.Literal.of(config.type())).get();
  }

  @Produces
  @RequestScoped
  public Authenticator authenticator(
      AuthenticationRealmConfiguration config, @Any Instance<Authenticator> authenticators) {
    return authenticators.select(Identifier.Literal.of(config.authenticator().type())).get();
  }

  @Produces
  @RequestScoped
  public IcebergRestOAuth2ApiService icebergRestOAuth2ApiService(
      AuthenticationRealmConfiguration config,
      @Any Instance<IcebergRestOAuth2ApiService> services) {
    String type =
        config.type() == AuthenticationType.EXTERNAL ? "disabled" : config.tokenService().type();
    return services.select(Identifier.Literal.of(type)).get();
  }

  @Produces
  @RequestScoped
  public TokenBroker tokenBroker(
      AuthenticationRealmConfiguration config,
      @Any Instance<TokenBrokerFactory> tokenBrokerFactories,
      PolarisMetaStoreManager polarisMetaStoreManager,
      CallContext callContext) {
    String type =
        config.type() == AuthenticationType.EXTERNAL ? "none" : config.tokenBroker().type();
    TokenBrokerFactory tokenBrokerFactory =
        tokenBrokerFactories.select(Identifier.Literal.of(type)).get();
    return tokenBrokerFactory.create(polarisMetaStoreManager, callContext.getPolarisCallContext());
  }

  // other beans

  @Produces
  @Singleton
  @Identifier("task-executor")
  public ManagedExecutor taskExecutor(TaskHandlerConfiguration config) {
    return SmallRyeManagedExecutor.builder()
        .injectionPointName("task-executor")
        .propagated(ThreadContext.ALL_REMAINING)
        .cleared(ThreadContext.CDI)
        .maxAsync(config.maxConcurrentTasks())
        .maxQueued(config.maxQueuedTasks())
        .build();
  }

  @Produces
  @RequestScoped
  public UserSecretsManager userSecretsManager(
      RealmContext realmContext, UserSecretsManagerFactory userSecretsManagerFactory) {
    return userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext);
  }

  @Produces
  @RequestScoped
  public AuthenticationRealmConfiguration realmAuthConfig(
      AuthenticationConfiguration config, RealmContext realmContext) {
    return config.forRealm(realmContext);
  }

  @Produces
  public OidcTenantResolver oidcTenantResolver(
      OidcConfiguration config, @Any Instance<OidcTenantResolver> resolvers) {
    return resolvers.select(Identifier.Literal.of(config.tenantResolver())).get();
  }

  @Produces
  @RequestScoped
  public PolarisCredentialManager polarisCredentialManager(
      PolarisCredentialManagerConfiguration config,
      @Any Instance<PolarisCredentialManager> credentialManagers) {
    return credentialManagers.select(Identifier.Literal.of(config.type())).get();
  }

  public void closeTaskExecutor(@Disposes @Identifier("task-executor") ManagedExecutor executor) {
    executor.close();
  }

  /**
   * Produces a RequestIdSupplier that retrieves the request ID from the current request context.
   *
   * <p>The request ID is set by {@link RequestIdFilter} on each incoming request. This producer
   * provides access to that ID via the {@link RequestIdSupplier} interface for components that need
   * request correlation without direct dependency on JAX-RS types.
   */
  @Produces
  @RequestScoped
  public RequestIdSupplier requestIdSupplier() {
    return () -> {
      ResteasyReactiveRequestContext context = CurrentRequestManager.get();
      if (context != null) {
        return (String)
            context.getContainerRequestContext().getProperty(RequestIdFilter.REQUEST_ID_KEY);
      }
      return null;
    };
  }

  @Produces
  @ApplicationScoped
  public PolarisMetricsReporter metricsReporter(
      MetricsReportingConfiguration config, @Any Instance<PolarisMetricsReporter> reporters) {
    return reporters.select(Identifier.Literal.of(config.type())).get();
  }
}
