/*
 * 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.gravitino;

import com.google.common.base.Preconditions;
import java.lang.reflect.Proxy;
import org.apache.gravitino.audit.AuditLogManager;
import org.apache.gravitino.authorization.AccessControlDispatcher;
import org.apache.gravitino.authorization.AccessControlManager;
import org.apache.gravitino.authorization.FutureGrantManager;
import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.authorization.OwnerEventManager;
import org.apache.gravitino.authorization.OwnerManager;
import org.apache.gravitino.auxiliary.AuxiliaryServiceManager;
import org.apache.gravitino.catalog.CatalogDispatcher;
import org.apache.gravitino.catalog.CatalogManager;
import org.apache.gravitino.catalog.CatalogNormalizeDispatcher;
import org.apache.gravitino.catalog.FilesetDispatcher;
import org.apache.gravitino.catalog.FilesetNormalizeDispatcher;
import org.apache.gravitino.catalog.FilesetOperationDispatcher;
import org.apache.gravitino.catalog.ModelDispatcher;
import org.apache.gravitino.catalog.ModelNormalizeDispatcher;
import org.apache.gravitino.catalog.ModelOperationDispatcher;
import org.apache.gravitino.catalog.OperationDispatcherInterceptor;
import org.apache.gravitino.catalog.PartitionDispatcher;
import org.apache.gravitino.catalog.PartitionNormalizeDispatcher;
import org.apache.gravitino.catalog.PartitionOperationDispatcher;
import org.apache.gravitino.catalog.SchemaDispatcher;
import org.apache.gravitino.catalog.SchemaNormalizeDispatcher;
import org.apache.gravitino.catalog.SchemaOperationDispatcher;
import org.apache.gravitino.catalog.TableDispatcher;
import org.apache.gravitino.catalog.TableNormalizeDispatcher;
import org.apache.gravitino.catalog.TableOperationDispatcher;
import org.apache.gravitino.catalog.TopicDispatcher;
import org.apache.gravitino.catalog.TopicNormalizeDispatcher;
import org.apache.gravitino.catalog.TopicOperationDispatcher;
import org.apache.gravitino.credential.CredentialOperationDispatcher;
import org.apache.gravitino.hook.AccessControlHookDispatcher;
import org.apache.gravitino.hook.CatalogHookDispatcher;
import org.apache.gravitino.hook.FilesetHookDispatcher;
import org.apache.gravitino.hook.JobHookDispatcher;
import org.apache.gravitino.hook.MetalakeHookDispatcher;
import org.apache.gravitino.hook.ModelHookDispatcher;
import org.apache.gravitino.hook.PolicyHookDispatcher;
import org.apache.gravitino.hook.SchemaHookDispatcher;
import org.apache.gravitino.hook.TableHookDispatcher;
import org.apache.gravitino.hook.TagHookDispatcher;
import org.apache.gravitino.hook.TopicHookDispatcher;
import org.apache.gravitino.job.JobManager;
import org.apache.gravitino.job.JobOperationDispatcher;
import org.apache.gravitino.listener.AccessControlEventDispatcher;
import org.apache.gravitino.listener.CatalogEventDispatcher;
import org.apache.gravitino.listener.EventBus;
import org.apache.gravitino.listener.EventListenerManager;
import org.apache.gravitino.listener.FilesetEventDispatcher;
import org.apache.gravitino.listener.JobEventDispatcher;
import org.apache.gravitino.listener.MetalakeEventDispatcher;
import org.apache.gravitino.listener.ModelEventDispatcher;
import org.apache.gravitino.listener.PartitionEventDispatcher;
import org.apache.gravitino.listener.PolicyEventDispatcher;
import org.apache.gravitino.listener.SchemaEventDispatcher;
import org.apache.gravitino.listener.StatisticEventDispatcher;
import org.apache.gravitino.listener.TableEventDispatcher;
import org.apache.gravitino.listener.TagEventDispatcher;
import org.apache.gravitino.listener.TopicEventDispatcher;
import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.metalake.MetalakeDispatcher;
import org.apache.gravitino.metalake.MetalakeManager;
import org.apache.gravitino.metalake.MetalakeNormalizeDispatcher;
import org.apache.gravitino.metrics.MetricsSystem;
import org.apache.gravitino.metrics.source.JVMMetricsSource;
import org.apache.gravitino.policy.PolicyDispatcher;
import org.apache.gravitino.policy.PolicyManager;
import org.apache.gravitino.stats.StatisticDispatcher;
import org.apache.gravitino.stats.StatisticManager;
import org.apache.gravitino.storage.IdGenerator;
import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.tag.TagDispatcher;
import org.apache.gravitino.tag.TagManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
 * This class manages the Apache Gravitino environment.
 */
public class GravitinoEnv {

  private static final Logger LOG = LoggerFactory.getLogger(GravitinoEnv.class);

  private Config config;
  // Iceberg REST server use base components while Gravitino Server use full components.
  private boolean manageFullComponents = true;

  private EntityStore entityStore;

  private CatalogDispatcher catalogDispatcher;

  private CatalogManager catalogManager;

  private MetalakeManager metalakeManager;

  private SchemaDispatcher schemaDispatcher;

  private TableDispatcher tableDispatcher;

  private PartitionDispatcher partitionDispatcher;

  private FilesetDispatcher filesetDispatcher;

  private TopicDispatcher topicDispatcher;

  private ModelDispatcher modelDispatcher;

  private MetalakeDispatcher metalakeDispatcher;

  private CredentialOperationDispatcher credentialOperationDispatcher;

  private TagDispatcher tagDispatcher;

  private PolicyDispatcher policyDispatcher;

  private AccessControlDispatcher accessControlDispatcher;

  private IdGenerator idGenerator;

  private AuxiliaryServiceManager auxServiceManager;

  private MetricsSystem metricsSystem;

  private LockManager lockManager;

  private EventListenerManager eventListenerManager;

  private AuditLogManager auditLogManager;

  private JobOperationDispatcher jobOperationDispatcher;

  private EventBus eventBus;
  private OwnerDispatcher ownerDispatcher;
  private FutureGrantManager futureGrantManager;
  private GravitinoAuthorizer gravitinoAuthorizer;
  private StatisticDispatcher statisticDispatcher;

  protected GravitinoEnv() {}

  private static class InstanceHolder {
    private static final GravitinoEnv INSTANCE = new GravitinoEnv();
  }

  /**
   * Get the singleton instance of the GravitinoEnv.
   *
   * @return The singleton instance of the GravitinoEnv.
   */
  public static GravitinoEnv getInstance() {
    return InstanceHolder.INSTANCE;
  }

  /**
   * Initialize base components, used for Iceberg REST server.
   *
   * @param config The configuration object to initialize the environment.
   */
  public void initializeBaseComponents(Config config) {
    LOG.info("Initializing Gravitino base environment...");
    this.config = config;
    this.manageFullComponents = false;
    initBaseComponents();
    LOG.info("Gravitino base environment is initialized.");
  }

  /**
   * Initialize all components, used for Gravitino server.
   *
   * @param config The configuration object to initialize the environment.
   */
  public void initializeFullComponents(Config config) {
    LOG.info("Initializing Gravitino full environment...");
    this.config = config;
    this.manageFullComponents = true;
    initBaseComponents();
    initGravitinoServerComponents();
    LOG.info("Gravitino full environment is initialized.");
  }

  /**
   * Get the configuration object associated with the Gravitino environment.
   *
   * @return The configuration object.
   */
  public Config config() {
    return config;
  }

  /**
   * Get the EntityStore associated with the Gravitino environment.
   *
   * @return The EntityStore instance.
   */
  public EntityStore entityStore() {
    Preconditions.checkArgument(entityStore != null, "GravitinoEnv is not initialized.");
    return entityStore;
  }

  /**
   * Get the CatalogDispatcher associated with the Gravitino environment.
   *
   * @return The CatalogDispatcher instance.
   */
  public CatalogDispatcher catalogDispatcher() {
    return catalogDispatcher;
  }

  /**
   * Get the SchemaDispatcher associated with the Gravitino environment.
   *
   * @return The SchemaDispatcher instance.
   */
  public SchemaDispatcher schemaDispatcher() {
    return schemaDispatcher;
  }

  /**
   * Get the TableDispatcher associated with the Gravitino environment.
   *
   * @return The TableDispatcher instance.
   */
  public TableDispatcher tableDispatcher() {
    return tableDispatcher;
  }

  /**
   * Get the ModelDispatcher associated with the Gravitino environment.
   *
   * @return The ModelDispatcher instance.
   */
  public ModelDispatcher modelDispatcher() {
    return modelDispatcher;
  }

  /**
   * Get the PartitionDispatcher associated with the Gravitino environment.
   *
   * @return The PartitionDispatcher instance.
   */
  public PartitionDispatcher partitionDispatcher() {
    return partitionDispatcher;
  }

  /**
   * Get the FilesetDispatcher associated with the Gravitino environment.
   *
   * @return The FilesetDispatcher instance.
   */
  public FilesetDispatcher filesetDispatcher() {
    return filesetDispatcher;
  }

  /**
   * Get the TopicDispatcher associated with the Gravitino environment.
   *
   * @return The TopicDispatcher instance.
   */
  public TopicDispatcher topicDispatcher() {
    return topicDispatcher;
  }

  /**
   * Get the MetalakeDispatcher associated with the Gravitino environment.
   *
   * @return The MetalakeDispatcher instance.
   */
  public MetalakeDispatcher metalakeDispatcher() {
    return metalakeDispatcher;
  }

  /**
   * Get the {@link CredentialOperationDispatcher} associated with the Gravitino environment.
   *
   * @return The {@link CredentialOperationDispatcher} instance.
   */
  public CredentialOperationDispatcher credentialOperationDispatcher() {
    return credentialOperationDispatcher;
  }

  /**
   * Get the IdGenerator associated with the Gravitino environment.
   *
   * @return The IdGenerator instance.
   */
  public IdGenerator idGenerator() {
    return idGenerator;
  }

  /**
   * Get the CatalogManager associated with the Gravitino environment.
   *
   * @return The CatalogManager instance.
   */
  public CatalogManager catalogManager() {
    Preconditions.checkArgument(catalogManager != null, "GravitinoEnv is not initialized.");
    return catalogManager;
  }

  /**
   * Get the EventBus associated with the Gravitino environment.
   *
   * @return The EventBus instance.
   */
  public EventBus eventBus() {
    Preconditions.checkArgument(eventBus != null, "GravitinoEnv is not initialized.");
    return eventBus;
  }

  /**
   * Get the MetricsSystem associated with the Gravitino environment.
   *
   * @return The MetricsSystem instance.
   */
  public MetricsSystem metricsSystem() {
    return metricsSystem;
  }

  public LockManager lockManager() {
    return lockManager;
  }

  /**
   * Get the AccessControlDispatcher associated with the Gravitino environment.
   *
   * @return The AccessControlDispatcher instance.
   */
  public AccessControlDispatcher accessControlDispatcher() {
    return accessControlDispatcher;
  }

  /**
   * Get the tagDispatcher associated with the Gravitino environment.
   *
   * @return The tagDispatcher instance.
   */
  public TagDispatcher tagDispatcher() {
    return tagDispatcher;
  }

  /**
   * Get the PolicyDispatcher associated with the Gravitino environment.
   *
   * @return The PolicyDispatcher instance.
   */
  public PolicyDispatcher policyDispatcher() {
    return policyDispatcher;
  }

  /**
   * Get the Owner dispatcher associated with the Gravitino environment.
   *
   * @return The OwnerManager instance.
   */
  public OwnerDispatcher ownerDispatcher() {
    return ownerDispatcher;
  }

  /**
   * Get the FutureGrantManager associated with the Gravitino environment.
   *
   * @return The FutureGrantManager instance.
   */
  public FutureGrantManager futureGrantManager() {
    return futureGrantManager;
  }

  /**
   * Get the EventListenerManager associated with the Gravitino environment.
   *
   * @return The EventListenerManager instance.
   */
  public EventListenerManager eventListenerManager() {
    return eventListenerManager;
  }

  /**
   * Set GravitinoAuthorizer to GravitinoEnv
   *
   * @param gravitinoAuthorizer the GravitinoAuthorizer instance
   */
  public void setGravitinoAuthorizer(GravitinoAuthorizer gravitinoAuthorizer) {
    this.gravitinoAuthorizer = gravitinoAuthorizer;
  }

  /**
   * Get The GravitinoAuthorizer
   *
   * @return the GravitinoAuthorizer instance
   */
  public GravitinoAuthorizer gravitinoAuthorizer() {
    return gravitinoAuthorizer;
  }

  /**
   * Get the JobOperationDispatcher associated with the Gravitino environment.
   *
   * @return The JobOperationDispatcher instance.
   */
  public JobOperationDispatcher jobOperationDispatcher() {
    Preconditions.checkArgument(jobOperationDispatcher != null, "GravitinoEnv is not initialized.");
    return jobOperationDispatcher;
  }

  public StatisticDispatcher statisticDispatcher() {
    return statisticDispatcher;
  }

  public void start() {
    metricsSystem.start();
    eventListenerManager.start();
    if (manageFullComponents) {
      auxServiceManager.serviceStart();
    }
  }

  /** Shutdown the Gravitino environment. */
  public void shutdown() {
    LOG.info("Shutting down Gravitino Environment...");

    if (entityStore != null) {
      try {
        entityStore.close();
      } catch (Exception e) {
        LOG.warn("Failed to close EntityStore.", e);
      }
    }

    if (catalogManager != null) {
      catalogManager.close();
    }

    if (auxServiceManager != null) {
      try {
        auxServiceManager.serviceStop();
      } catch (Exception e) {
        LOG.warn("Failed to stop AuxServiceManager", e);
      }
    }

    if (metricsSystem != null) {
      metricsSystem.close();
    }

    if (eventListenerManager != null) {
      eventListenerManager.stop();
    }

    if (metalakeManager != null) {
      metalakeManager.close();
    }

    if (jobOperationDispatcher != null) {
      try {
        jobOperationDispatcher.close();
        jobOperationDispatcher = null;
      } catch (Exception e) {
        LOG.warn("Failed to close JobOperationDispatcher", e);
      }
    }

    if (statisticDispatcher != null) {
      try {
        statisticDispatcher.close();
      } catch (Exception e) {
        LOG.warn("Failed to close StatisticDispatcher", e);
      }
    }

    LOG.info("Gravitino Environment is shut down.");
  }

  private void initBaseComponents() {
    this.metricsSystem = new MetricsSystem();
    metricsSystem.register(new JVMMetricsSource());

    this.eventListenerManager = new EventListenerManager();
    eventListenerManager.init(
        config.getConfigsWithPrefix(EventListenerManager.GRAVITINO_EVENT_LISTENER_PREFIX));
    this.eventBus = eventListenerManager.createEventBus();

    this.auditLogManager = new AuditLogManager();
    auditLogManager.init(config, eventListenerManager);
  }

  private void initGravitinoServerComponents() {
    // Initialize EntityStore
    this.entityStore = EntityStoreFactory.createEntityStore(config);
    entityStore.initialize(config);

    // create and initialize a random id generator
    this.idGenerator = new RandomIdGenerator();

    // Tree lock
    this.lockManager = new LockManager(config);

    // Create and initialize metalake related modules, the operation chain is:
    // MetalakeEventDispatcher -> MetalakeNormalizeDispatcher -> MetalakeHookDispatcher ->
    // MetalakeManager
    this.metalakeManager = new MetalakeManager(entityStore, idGenerator);
    MetalakeHookDispatcher metalakeHookDispatcher = new MetalakeHookDispatcher(metalakeManager);
    MetalakeNormalizeDispatcher metalakeNormalizeDispatcher =
        new MetalakeNormalizeDispatcher(metalakeHookDispatcher);
    this.metalakeDispatcher = new MetalakeEventDispatcher(eventBus, metalakeNormalizeDispatcher);

    // Create and initialize Catalog related modules, the operation chain is:
    // CatalogEventDispatcher -> CatalogNormalizeDispatcher -> CatalogHookDispatcher ->
    // CatalogManager
    this.catalogManager = new CatalogManager(config, entityStore, idGenerator);
    CatalogHookDispatcher catalogHookDispatcher = new CatalogHookDispatcher(catalogManager);
    CatalogNormalizeDispatcher catalogNormalizeDispatcher =
        new CatalogNormalizeDispatcher(catalogHookDispatcher);
    this.catalogDispatcher = new CatalogEventDispatcher(eventBus, catalogNormalizeDispatcher);

    this.credentialOperationDispatcher =
        new CredentialOperationDispatcher(catalogManager, entityStore, idGenerator);

    SchemaOperationDispatcher schemaOperationDispatcher =
        new SchemaOperationDispatcher(catalogManager, entityStore, idGenerator);
    SchemaDispatcher schemaDispatcherProxy =
        (SchemaDispatcher)
            Proxy.newProxyInstance(
                SchemaDispatcher.class.getClassLoader(),
                new Class[] {SchemaDispatcher.class},
                new OperationDispatcherInterceptor(
                    schemaOperationDispatcher, catalogManager, entityStore));
    SchemaHookDispatcher schemaHookDispatcher = new SchemaHookDispatcher(schemaDispatcherProxy);
    SchemaNormalizeDispatcher schemaNormalizeDispatcher =
        new SchemaNormalizeDispatcher(schemaHookDispatcher, catalogManager);
    this.schemaDispatcher = new SchemaEventDispatcher(eventBus, schemaNormalizeDispatcher);

    TableOperationDispatcher tableOperationDispatcher =
        new TableOperationDispatcher(catalogManager, entityStore, idGenerator);
    TableDispatcher tableDispatcherProxy =
        (TableDispatcher)
            Proxy.newProxyInstance(
                TableDispatcher.class.getClassLoader(),
                new Class[] {TableDispatcher.class},
                new OperationDispatcherInterceptor(
                    tableOperationDispatcher, catalogManager, entityStore));
    TableHookDispatcher tableHookDispatcher = new TableHookDispatcher(tableDispatcherProxy);
    TableNormalizeDispatcher tableNormalizeDispatcher =
        new TableNormalizeDispatcher(tableHookDispatcher, catalogManager);
    this.tableDispatcher = new TableEventDispatcher(eventBus, tableNormalizeDispatcher);

    // TODO: We can install hooks when we need, we only supports ownership post hook,
    //  partition doesn't have ownership, so we don't need it now.
    PartitionOperationDispatcher partitionOperationDispatcher =
        new PartitionOperationDispatcher(catalogManager, entityStore, idGenerator);
    PartitionDispatcher partitionDispatcherProxy =
        (PartitionDispatcher)
            Proxy.newProxyInstance(
                PartitionDispatcher.class.getClassLoader(),
                new Class[] {PartitionDispatcher.class},
                new OperationDispatcherInterceptor(
                    partitionOperationDispatcher, catalogManager, entityStore));
    PartitionNormalizeDispatcher partitionNormalizeDispatcher =
        new PartitionNormalizeDispatcher(partitionDispatcherProxy, catalogManager);
    this.partitionDispatcher = new PartitionEventDispatcher(eventBus, partitionNormalizeDispatcher);

    FilesetOperationDispatcher filesetOperationDispatcher =
        new FilesetOperationDispatcher(catalogManager, entityStore, idGenerator);
    FilesetDispatcher filesetDispatcherProxy =
        (FilesetDispatcher)
            Proxy.newProxyInstance(
                FilesetDispatcher.class.getClassLoader(),
                new Class[] {FilesetDispatcher.class},
                new OperationDispatcherInterceptor(
                    filesetOperationDispatcher, catalogManager, entityStore));
    FilesetHookDispatcher filesetHookDispatcher = new FilesetHookDispatcher(filesetDispatcherProxy);
    FilesetNormalizeDispatcher filesetNormalizeDispatcher =
        new FilesetNormalizeDispatcher(filesetHookDispatcher, catalogManager);
    this.filesetDispatcher = new FilesetEventDispatcher(eventBus, filesetNormalizeDispatcher);

    TopicOperationDispatcher topicOperationDispatcher =
        new TopicOperationDispatcher(catalogManager, entityStore, idGenerator);
    TopicDispatcher topicDispatcherProxy =
        (TopicDispatcher)
            Proxy.newProxyInstance(
                TopicDispatcher.class.getClassLoader(),
                new Class[] {TopicDispatcher.class},
                new OperationDispatcherInterceptor(
                    topicOperationDispatcher, catalogManager, entityStore));
    TopicHookDispatcher topicHookDispatcher = new TopicHookDispatcher(topicDispatcherProxy);
    TopicNormalizeDispatcher topicNormalizeDispatcher =
        new TopicNormalizeDispatcher(topicHookDispatcher, catalogManager);
    this.topicDispatcher = new TopicEventDispatcher(eventBus, topicNormalizeDispatcher);

    ModelOperationDispatcher modelOperationDispatcher =
        new ModelOperationDispatcher(catalogManager, entityStore, idGenerator);
    ModelDispatcher modelDispatcherProxy =
        (ModelDispatcher)
            Proxy.newProxyInstance(
                ModelDispatcher.class.getClassLoader(),
                new Class[] {ModelDispatcher.class},
                new OperationDispatcherInterceptor(
                    modelOperationDispatcher, catalogManager, entityStore));
    ModelHookDispatcher modelHookDispatcher = new ModelHookDispatcher(modelDispatcherProxy);
    ModelNormalizeDispatcher modelNormalizeDispatcher =
        new ModelNormalizeDispatcher(modelHookDispatcher, catalogManager);
    this.modelDispatcher = new ModelEventDispatcher(eventBus, modelNormalizeDispatcher);
    this.statisticDispatcher =
        new StatisticEventDispatcher(
            eventBus, new StatisticManager(entityStore, idGenerator, config));

    // Create and initialize access control related modules
    boolean enableAuthorization = config.get(Configs.ENABLE_AUTHORIZATION);
    if (enableAuthorization) {
      AccessControlManager accessControlManager =
          new AccessControlManager(entityStore, idGenerator, config);
      AccessControlHookDispatcher accessControlHookDispatcher =
          new AccessControlHookDispatcher(accessControlManager);
      this.accessControlDispatcher =
          new AccessControlEventDispatcher(eventBus, accessControlHookDispatcher);
      OwnerDispatcher ownerManager = new OwnerManager(entityStore);
      this.ownerDispatcher = new OwnerEventManager(eventBus, ownerManager);
      this.futureGrantManager = new FutureGrantManager(entityStore, ownerManager);
    } else {
      this.accessControlDispatcher = null;
      this.ownerDispatcher = null;
      this.futureGrantManager = null;
    }

    this.auxServiceManager = new AuxiliaryServiceManager();
    this.auxServiceManager.serviceInit(config);

    // Create and initialize Tag related modules
    TagManager tagManager = new TagManager(idGenerator, entityStore);
    TagHookDispatcher tagHookDispatcher = new TagHookDispatcher(tagManager);
    this.tagDispatcher = new TagEventDispatcher(eventBus, tagHookDispatcher);

    PolicyEventDispatcher policyEventDispatcher =
        new PolicyEventDispatcher(eventBus, new PolicyManager(idGenerator, entityStore));
    this.policyDispatcher = new PolicyHookDispatcher(policyEventDispatcher);

    this.jobOperationDispatcher =
        new JobEventDispatcher(
            eventBus, new JobHookDispatcher(new JobManager(config, entityStore, idGenerator)));
  }
}
