package io.quarkus.hibernate.orm.runtime.boot;

import java.io.Serializable;
import java.security.NoSuchAlgorithmException;

import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;

import org.hibernate.MultiTenancyStrategy;
import org.hibernate.SessionFactory;
import org.hibernate.SessionFactoryObserver;
import org.hibernate.boot.internal.SessionFactoryOptionsBuilder;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.selector.spi.StrategySelector;
import org.hibernate.bytecode.internal.SessionFactoryObserverForBytecodeEnhancer;
import org.hibernate.bytecode.spi.BytecodeProvider;
import org.hibernate.engine.query.spi.HQLQueryPlan;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jpa.AvailableSettings;
import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.tool.schema.spi.CommandAcceptanceException;
import org.hibernate.tool.schema.spi.DelayedDropRegistryNotAvailableImpl;
import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator;

import io.quarkus.hibernate.orm.runtime.RuntimeSettings;
import io.quarkus.hibernate.orm.runtime.recording.PrevalidatedQuarkusMetadata;
import io.quarkus.hibernate.orm.runtime.tenant.HibernateCurrentTenantIdentifierResolver;

public class FastBootEntityManagerFactoryBuilder implements EntityManagerFactoryBuilder {

    protected final PrevalidatedQuarkusMetadata metadata;
    protected final String persistenceUnitName;
    protected final StandardServiceRegistry standardServiceRegistry;
    private final RuntimeSettings runtimeSettings;
    private final Object validatorFactory;
    private final Object cdiBeanManager;
    protected final MultiTenancyStrategy multiTenancyStrategy;

    public FastBootEntityManagerFactoryBuilder(
            PrevalidatedQuarkusMetadata metadata, String persistenceUnitName,
            StandardServiceRegistry standardServiceRegistry, RuntimeSettings runtimeSettings, Object validatorFactory,
            Object cdiBeanManager, MultiTenancyStrategy strategy) {
        this.metadata = metadata;
        this.persistenceUnitName = persistenceUnitName;
        this.standardServiceRegistry = standardServiceRegistry;
        this.runtimeSettings = runtimeSettings;
        this.validatorFactory = validatorFactory;
        this.cdiBeanManager = cdiBeanManager;
        this.multiTenancyStrategy = strategy;
    }

    @Override
    public EntityManagerFactoryBuilder withValidatorFactory(Object validatorFactory) {
        return null;
    }

    @Override
    public EntityManagerFactoryBuilder withDataSource(DataSource dataSource) {
        return null;
    }

    @Override
    public EntityManagerFactory build() {
        try {
            final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder();
            populate(optionsBuilder, standardServiceRegistry, multiTenancyStrategy);
            return new SessionFactoryImpl(metadata.getOriginalMetadata(), optionsBuilder.buildOptions(), HQLQueryPlan::new);
        } catch (Exception e) {
            throw persistenceException("Unable to build Hibernate SessionFactory", e);
        }
    }

    @Override
    public void cancel() {
        // nothing?
    }

    @Override
    public void generateSchema() {
        try {
            SchemaManagementToolCoordinator.process(metadata, standardServiceRegistry, runtimeSettings.getSettings(),
                    DelayedDropRegistryNotAvailableImpl.INSTANCE);
        } catch (Exception e) {
            throw persistenceException("Error performing schema management", e);
        }

        // release this builder
        cancel();
    }

    protected PersistenceException persistenceException(String message, Exception cause) {
        // Provide a comprehensible message if there is an issue with SSL support
        Throwable t = cause;
        while (t != null) {
            if (t instanceof NoSuchAlgorithmException) {
                message += "Unable to enable SSL support. You might be in the case where you used the `quarkus.ssl.native=false` configuration"
                        + " and SSL was not disabled automatically for your driver.";
                break;
            }

            if (t instanceof CommandAcceptanceException) {
                message = "Invalid import file. Make sure your statements are valid and properly separated by a semi-colon.";
                break;
            }

            t = t.getCause();
        }

        return new PersistenceException(getExceptionHeader() + message, cause);
    }

    private String getExceptionHeader() {
        return "[PersistenceUnit: " + persistenceUnitName + "] ";
    }

    protected void populate(SessionFactoryOptionsBuilder options, StandardServiceRegistry ssr, MultiTenancyStrategy strategy) {

        // will use user override value or default to false if not supplied to follow
        // JPA spec.
        final boolean jtaTransactionAccessEnabled = runtimeSettings.getBoolean(
                AvailableSettings.ALLOW_JTA_TRANSACTION_ACCESS);
        if (!jtaTransactionAccessEnabled) {
            options.disableJtaTransactionAccess();
        }

        final boolean allowRefreshDetachedEntity = runtimeSettings.getBoolean(
                org.hibernate.cfg.AvailableSettings.ALLOW_REFRESH_DETACHED_ENTITY);
        if (!allowRefreshDetachedEntity) {
            options.disableRefreshDetachedEntity();
        }

        // Locate and apply any requested SessionFactoryObserver
        final Object sessionFactoryObserverSetting = runtimeSettings.get(AvailableSettings.SESSION_FACTORY_OBSERVER);
        if (sessionFactoryObserverSetting != null) {

            final StrategySelector strategySelector = ssr.getService(StrategySelector.class);
            final SessionFactoryObserver suppliedSessionFactoryObserver = strategySelector
                    .resolveStrategy(SessionFactoryObserver.class, sessionFactoryObserverSetting);
            options.addSessionFactoryObservers(suppliedSessionFactoryObserver);
        }

        options.addSessionFactoryObservers(new ServiceRegistryCloser());

        options.applyEntityNotFoundDelegate(new JpaEntityNotFoundDelegate());

        if (this.validatorFactory != null) {
            options.applyValidatorFactory(validatorFactory);
        }
        if (this.cdiBeanManager != null) {
            options.applyBeanManager(cdiBeanManager);
        }

        //Small memory optimisations: ensure the class transformation caches of the bytecode enhancer
        //are cleared both on start and on close of the SessionFactory.
        //(On start is useful especially in Quarkus as we won't do any more enhancement after this point)
        BytecodeProvider bytecodeProvider = ssr.getService(BytecodeProvider.class);
        options.addSessionFactoryObservers(new SessionFactoryObserverForBytecodeEnhancer(bytecodeProvider));

        if (strategy != null && strategy != MultiTenancyStrategy.NONE) {
            options.applyMultiTenancyStrategy(strategy);
            options.applyCurrentTenantIdentifierResolver(new HibernateCurrentTenantIdentifierResolver());
        }

    }

    private static class ServiceRegistryCloser implements SessionFactoryObserver {

        @Override
        public void sessionFactoryCreated(SessionFactory sessionFactory) {
            // nothing to do
        }

        @Override
        public void sessionFactoryClosed(SessionFactory sessionFactory) {
            SessionFactoryImplementor sfi = ((SessionFactoryImplementor) sessionFactory);
            sfi.getServiceRegistry().destroy();
            ServiceRegistry basicRegistry = sfi.getServiceRegistry().getParentServiceRegistry();
            ((ServiceRegistryImplementor) basicRegistry).destroy();
        }
    }

    private static class JpaEntityNotFoundDelegate implements EntityNotFoundDelegate, Serializable {

        @Override
        public void handleEntityNotFound(String entityName, Serializable id) {
            throw new EntityNotFoundException("Unable to find " + entityName + " with id " + id);
        }
    }

}
