/*
 * 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.geode.internal.jndi;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NoInitialContextException;
import javax.sql.DataSource;

import jakarta.transaction.SystemException;
import jakarta.transaction.TransactionManager;
import org.apache.logging.log4j.Logger;

import org.apache.geode.LogWriter;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.annotations.internal.MutableForTesting;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.internal.datasource.ClientConnectionFactoryWrapper;
import org.apache.geode.internal.datasource.ConfigProperty;
import org.apache.geode.internal.datasource.DataSourceCreateException;
import org.apache.geode.internal.datasource.DataSourceFactory;
import org.apache.geode.internal.datasource.GemFireTransactionDataSource;
import org.apache.geode.internal.jta.TransactionManagerImpl;
import org.apache.geode.internal.jta.TransactionUtils;
import org.apache.geode.internal.jta.UserTransactionImpl;
import org.apache.geode.internal.util.DriverJarUtils;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.util.internal.GeodeGlossary;

/**
 * <p>
 * Utility class for binding DataSources and Transactional resources to JNDI Tree. If there is a
 * pre-existing JNDI tree in the system, the GemFire JNDI tree is not not generated.
 * <p>
 * Datasources are bound to jndi tree after getting initialised. The initialisation parameters are
 * read from cache.xml.
 * <p>
 * If there is a pre-existing TransactionManager/UserTransaction (possible in presence of
 * application server scenerio), the GemFire TransactionManager and UserTransaction will not be
 * initialised. In that case, application server TransactionManager/UserTransaction will handle the
 * transactional activity. But even in this case the datasource element will be bound to the
 * available JNDI tree. The transactional datasource (XADataSource) will make use of available
 * TransactionManager.
 * <p>
 *
 */
public class JNDIInvoker {

  private static final Logger logger = LogService.getLogger();

  // private static boolean DEBUG = false;
  /**
   * JNDI Context, this may refer to GemFire JNDI Context or external Context, in case the external
   * JNDI tree exists.
   */
  @MakeNotStatic
  private static Context ctx;
  /**
   * transactionManager TransactionManager, this refers to GemFire TransactionManager only.
   */
  @MakeNotStatic
  private static TransactionManager transactionManager;
  // most of the following came from the javadocs at:
  // http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/transaction/jta/JtaTransactionManager.html
  private static final String[][] knownJNDIManagers = {{"java:/TransactionManager", "JBoss"},
      {"java:comp/TransactionManager", "Cosminexus"}, // and many others
      {"java:appserver/TransactionManager", "GlassFish"}, {"java:pm/TransactionManager", "SunONE"},
      {"java:comp/UserTransaction", "Orion, JTOM, BEA WebLogic"},
      // not sure about the following but leaving it for backwards compat
      {"javax.transaction.TransactionManager", "BEA WebLogic"}};
  /* ************************************************* */
  /**
   * WebSphere 5.1 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_1 =
      "com.ibm.ws.Transaction.TransactionManagerFactory";
  /**
   * WebSphere 5.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_0 =
      "com.ibm.ejs.jts.jta.TransactionManagerFactory";
  /**
   * WebSphere 4.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_4 = "com.ibm.ejs.jts.jta.JTSXA";
  /**
   * Maps data source name to the data source instance itself.
   */
  @MakeNotStatic
  private static final ConcurrentMap<String, Object> dataSourceMap = new ConcurrentHashMap<>();

  /**
   * If this system property is set to true, GemFire will not try to lookup for an existing JTA
   * transaction manager bound to JNDI context or try to bind itself as a JTA transaction manager.
   * Also region operations will <b>not</b> participate in an ongoing JTA transaction.
   */
  @MutableForTesting
  private static boolean IGNORE_JTA =
      Boolean.getBoolean(GeodeGlossary.GEMFIRE_PREFIX + "ignoreJTA");

  @Immutable
  private static final DataSourceFactory dataSourceFactory = new DataSourceFactory();

  /**
   * Bind the transaction resources. Bind UserTransaction and TransactionManager.
   * <p>
   * If there is pre-existing JNDI tree in the system and TransactionManager / UserTransaction is
   * already bound, GemFire will make use of these resources, but if TransactionManager /
   * UserTransaction is not available, the GemFire TransactionManager / UserTransaction will be
   * bound to the JNDI tree.
   * <p>
   *
   */
  public static void mapTransactions(DistributedSystem distSystem) {
    try {
      TransactionUtils.setLogWriter(distSystem.getLogWriter());
      cleanup();
      ctx = new InitialContext();
      if (IGNORE_JTA) {
        // Jakarta EE migration fix: When ignoreJTA is true, we must still bind TransactionManager
        // to JNDI so that JNDI lookups don't fail with NameNotFoundException.
        // However, we intentionally do NOT set the transactionManager static field, which ensures
        // that getTransactionManager() returns null. This allows region operations to check
        // cache.getJTATransactionManager() and correctly skip JTA participation when IGNORE_JTA is
        // true.
        try {
          initializeGemFireContext();
          // Create a TransactionManager for JNDI binding but do NOT store it in the static field
          TransactionManager tm = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", tm);
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
        } catch (NamingException | SystemException e) {
          // Log and continue - TransactionManager initialization failed
          LogWriter writer = TransactionUtils.getLogWriter();
          if (writer.infoEnabled()) {
            writer.info("Failed to initialize TransactionManager with ignoreJTA=true", e);
          }
        }
        return;
      }
      doTransactionLookup();
    } catch (NamingException ne) {
      LogWriter writer = TransactionUtils.getLogWriter();
      if (ne instanceof NoInitialContextException) {
        String exception =
            "JNDIInvoker::mapTransactions:: No application server context found, Starting GemFire JNDI Context Context ";
        if (writer.finerEnabled()) {
          writer.finer(exception);
        }
        try {
          initializeGemFireContext();
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled()) {
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound TransactionManager to Context GemFire JNDI Tree");
          }
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled()) {
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound Transaction to Context GemFire JNDI Tree");
          }
        } catch (NamingException ne1) {
          if (writer.infoEnabled()) {
            writer.info(
                "JNDIInvoker::mapTransactions::NamingException while binding TransactionManager/UserTransaction to GemFire JNDI Tree");
          }
        } catch (SystemException se1) {
          if (writer.infoEnabled()) {
            writer.info(
                "JNDIInvoker::mapTransactions::SystemException while binding UserTransaction to GemFire JNDI Tree");
          }
        }
      } else if (ne instanceof NameNotFoundException) {
        String exception =
            "JNDIInvoker::mapTransactions:: No TransactionManager associated to Application server context, trying to bind GemFire TransactionManager";
        if (writer.finerEnabled()) {
          writer.finer(exception);
        }
        try {
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled()) {
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound TransactionManager to Application Server Context");
          }
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled()) {
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound UserTransaction to Application Server Context");
          }
        } catch (NamingException ne1) {
          if (writer.infoEnabled()) {
            writer.info(
                "JNDIInvoker::mapTransactions::NamingException while binding TransactionManager/UserTransaction to Application Server JNDI Tree");
          }
        } catch (SystemException se1) {
          if (writer.infoEnabled()) {
            writer.info(
                "JNDIInvoker::mapTransactions::SystemException while binding TransactionManager/UserTransaction to Application Server JNDI Tree");
          }
        }
      }
    }
  }

  /*
   * Cleans all the DataSource ans its associated threads gracefully, before start of the GemFire
   * system. Also kills all of the threads associated with the TransactionManager before making it
   * null.
   */
  private static void cleanup() {
    if (transactionManager instanceof TransactionManagerImpl) {
      TransactionManagerImpl.refresh();
      // unbind and set TransactionManager to null, so as to
      // initialize TXManager correctly if the cache is being restarted
      transactionManager = null;
      try {
        if (ctx != null) {
          ctx.unbind("java:/TransactionManager");
          // Also unbind UserTransaction to fully clean up JTA resources on cache restart
          ctx.unbind("java:/UserTransaction");
        }
      } catch (NamingException e) {
        // ok to ignore, rebind will be tried later
      }
    }
    dataSourceMap.values().stream().forEach(JNDIInvoker::closeDataSource);
    dataSourceMap.clear();
    IGNORE_JTA = Boolean.getBoolean(GeodeGlossary.GEMFIRE_PREFIX + "ignoreJTA");
  }

  private static void closeDataSource(Object dataSource) {
    if (dataSource instanceof AutoCloseable) {
      try {
        ((AutoCloseable) dataSource).close();
      } catch (Exception e) {
        if (logger.isDebugEnabled()) {
          logger.debug("Exception closing DataSource", e);
        }
      }
    }
  }

  /*
   * Helps in locating TransactionManager in presence of application server scenario. Stores the
   * value of TransactionManager for reference in GemFire system.
   */
  private static void doTransactionLookup() throws NamingException {
    Object jndiObject = null;
    LogWriter writer = TransactionUtils.getLogWriter();
    for (final String[] knownJNDIManager : knownJNDIManagers) {
      try {
        jndiObject = ctx.lookup(knownJNDIManager[0]);
      } catch (NamingException e) {
        String exception = "JNDIInvoker::doTransactionLookup::Couldn't lookup ["
            + knownJNDIManager[0] + " (" + knownJNDIManager[1] + ")]";
        if (writer.finerEnabled()) {
          writer.finer(exception);
        }
      }
      if (jndiObject instanceof TransactionManager) {
        transactionManager = (TransactionManager) jndiObject;
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager for "
            + knownJNDIManager[1];
        if (writer.fineEnabled()) {
          writer.fine(exception);
        }
        return;
      } else {
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager of class "
            + (jndiObject == null ? "null" : jndiObject.getClass())
            + " but is not of type javax.transaction.TransactionManager";
        if (writer.fineEnabled()) {
          writer.fine(exception);
        }
      }
    }
    Class clazz;
    try {
      if (writer.finerEnabled()) {
        writer.finer(
            "JNDIInvoker::doTransactionLookup::Trying WebSphere 5.1: " + WS_FACTORY_CLASS_5_1);
      }
      clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_1);
      if (writer.fineEnabled()) {
        writer
            .fine("JNDIInvoker::doTransactionLookup::Found WebSphere 5.1: " + WS_FACTORY_CLASS_5_1);
      }
    } catch (ClassNotFoundException ex) {
      try {
        if (writer.finerEnabled()) {
          writer.finer(
              "JNDIInvoker::doTransactionLookup::Trying WebSphere 5.0: " + WS_FACTORY_CLASS_5_0);
        }
        clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_0);
        if (writer.fineEnabled()) {
          writer.fine(
              "JNDIInvoker::doTransactionLookup::Found WebSphere 5.0: " + WS_FACTORY_CLASS_5_0);
        }
      } catch (ClassNotFoundException ex2) {
        try {
          clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_4);
          String exception =
              "JNDIInvoker::doTransactionLookup::Found WebSphere 4: " + WS_FACTORY_CLASS_4;
          if (writer.fineEnabled()) {
            writer.fine(exception, ex);
          }
        } catch (ClassNotFoundException ex3) {
          if (writer.finerEnabled()) {
            writer.finer(
                "JNDIInvoker::doTransactionLookup::Couldn't find any WebSphere TransactionManager factory class, neither for WebSphere version 5.1 nor 5.0 nor 4");
          }
          throw new NoInitialContextException();
        }
      }
    }
    try {
      Method method = clazz.getMethod("getTransactionManager", (Class[]) null);
      transactionManager = (TransactionManager) method.invoke(null, (Object[]) null);
    } catch (Exception ex) {
      writer.warning(
          String.format(
              "JNDIInvoker::doTransactionLookup::Found WebSphere TransactionManager factory class [%s], but could not invoke its static 'getTransactionManager' method",
              clazz.getName()),
          ex);
      throw new NameNotFoundException(
          String.format(
              "JNDIInvoker::doTransactionLookup::Found WebSphere TransactionManager factory class [%s], but could not invoke its static 'getTransactionManager' method",
              clazz.getName()));
    }
  }

  /**
   * Initialises the GemFire context. This is called when no external JNDI Context is found.
   *
   */
  private static void initializeGemFireContext() throws NamingException {
    // Jakarta EE migration fix: Get the GemFire context directly from the factory
    // instead of through InitialContext wrapper to avoid JNDI provider conflicts
    // In Jetty 12 environments, InitialContext may delegate to Jetty's JNDI provider,
    // causing lookup failures when trying to retrieve GemFire-bound resources
    // Direct factory access ensures we always use GemFire's ContextImpl
    InitialContextFactoryImpl factory = new InitialContextFactoryImpl();
    ctx = factory.getInitialContext(null);
  }

  /**
   * Binds a single Datasource to the existing JNDI tree. The JNDI tree may be The Datasource
   * properties are contained in the map. The Datasource implementation class is populated based on
   * properties in the map.
   *
   * @param map contains Datasource configuration properties.
   */
  public static void mapDatasource(Map map, List<ConfigProperty> props)
      throws NamingException, DataSourceCreateException, ClassNotFoundException, SQLException,
      InstantiationException, IllegalAccessException {
    mapDatasource(map, props, dataSourceFactory, ctx);
  }

  static void mapDatasource(Map map, List<ConfigProperty> props,
      DataSourceFactory dataSourceFactory, Context context)
      throws NamingException, DataSourceCreateException, ClassNotFoundException, SQLException,
      InstantiationException, IllegalAccessException {
    String driverClassName = null;
    if (map != null) {
      driverClassName = (String) map.get("jdbc-driver-class");
      DriverJarUtils util = new DriverJarUtils();
      if (driverClassName != null) {
        util.registerDriver(driverClassName);
      }
    }

    String value = (String) map.get("type");
    String jndiName = "";
    jndiName = (String) map.get("jndi-name");
    if (value.equals("PooledDataSource")) {
      validateAndBindDataSource(context, jndiName,
          dataSourceFactory.getPooledDataSource(map, props), props);
    } else if (value.equals("XAPooledDataSource")) {
      validateAndBindDataSource(context, jndiName,
          dataSourceFactory.getTranxDataSource(map, props), props);
    } else if (value.equals("SimpleDataSource")) {
      validateAndBindDataSource(context, jndiName, dataSourceFactory.getSimpleDataSource(map),
          props);
    } else if (value.equals("ManagedDataSource")) {
      ClientConnectionFactoryWrapper wrapper = dataSourceFactory.getManagedDataSource(map, props);
      ctx.rebind("java:/" + jndiName, wrapper.getClientConnFactory());
      dataSourceMap.put(jndiName, wrapper);
      if (logger.isDebugEnabled()) {
        logger.debug("Bound java:/" + jndiName + " to Context");
      }
    } else {
      String exception = "JNDIInvoker::mapDataSource::No correct type of DataSource";
      if (logger.isDebugEnabled()) {
        logger.debug(exception);
      }
      throw new DataSourceCreateException(exception);
    }
  }

  private static void validateAndBindDataSource(Context context, String jndiName,
      DataSource dataSource, List<ConfigProperty> props)
      throws NamingException, DataSourceCreateException {
    try (Connection connection = getConnection(dataSource, props)) {
    } catch (SQLException sqlEx) {
      closeDataSource(dataSource);
      logger.warn("Failed DataSource validation for JNDI name {}", jndiName, sqlEx);
      throw new DataSourceCreateException(
          "Failed to connect to \"" + jndiName + "\". See log for details", sqlEx);
    }
    context.rebind("java:/" + jndiName, dataSource);
    dataSourceMap.put(jndiName, dataSource);
    if (logger.isDebugEnabled()) {
      logger.debug("Bound java:/" + jndiName + " to Context");
    }
  }

  private static Connection getConnection(DataSource dataSource, List<ConfigProperty> props)
      throws SQLException {
    return dataSource.getConnection();
  }

  public static void unMapDatasource(String jndiName) throws NamingException {
    ctx.unbind("java:/" + jndiName);
    Object removedDataSource = dataSourceMap.remove(jndiName);
    closeDataSource(removedDataSource);
  }

  public static DataSource getDataSource(String name) {
    Object result = dataSourceMap.get(name);
    if (result instanceof DataSource) {
      return (DataSource) result;
    } else {
      return null;
    }
  }

  public static boolean isValidDataSource(String name) {
    Object dataSource = dataSourceMap.get(name);

    return dataSource == null || (dataSource instanceof DataSource
        && !(dataSource instanceof GemFireTransactionDataSource));
  }

  /**
   * @return Context the existing JNDI Context. If there is no pre-esisting JNDI Context, the
   *         GemFire JNDI Context is returned.
   */
  public static Context getJNDIContext() {
    return ctx;
  }

  /**
   * returns the GemFire TransactionManager.
   *
   */
  public static TransactionManager getTransactionManager() {
    return transactionManager;
  }

  public static int getNoOfAvailableDataSources() {
    return dataSourceMap.size();
  }

  public static Map<String, String> getBindingNamesRecursively(Context ctx) throws Exception {
    Map<String, String> result = new HashMap<>();
    NamingEnumeration<Binding> enumeration = ctx.listBindings("");

    while (enumeration.hasMore()) {
      Binding binding = enumeration.next();
      String name = binding.getName();
      String separator = name.endsWith(":") ? "" : "/";
      Object o = binding.getObject();
      if (o instanceof Context) {
        Map<String, String> innerBindings = getBindingNamesRecursively((Context) o);
        innerBindings.forEach((k, v) -> result.put(name + separator + k, v));
      } else {
        result.put(name, binding.getClassName());
      }
    }

    return result;
  }
}
