package org.jboss.windup.rules.apps.javaee.rules.spring;

import static org.joox.JOOX.$;
import static org.joox.JOOX.attr;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.metadata.RuleMetadata;
import org.jboss.windup.config.phase.InitialAnalysisPhase;
import org.jboss.windup.config.query.Query;
import org.jboss.windup.config.ruleprovider.IteratingRuleProvider;
import org.jboss.windup.rules.apps.javaee.model.DataSourceModel;
import org.jboss.windup.rules.apps.javaee.model.SpringBeanModel;
import org.jboss.windup.rules.apps.javaee.model.SpringConfigurationFileModel;
import org.jboss.windup.rules.apps.javaee.model.association.JNDIReferenceModel;
import org.jboss.windup.rules.apps.javaee.rules.DiscoverSpringConfigurationFilesRuleProvider;
import org.jboss.windup.rules.apps.javaee.service.DataSourceService;
import org.jboss.windup.rules.apps.javaee.service.SpringBeanService;
import org.jboss.windup.rules.apps.javaee.util.HibernateDialectDataSourceTypeResolver;
import org.jboss.windup.rules.apps.javaee.util.SpringDataSourceTypeResolver;
import org.joox.Context;
import org.joox.FastFilter;
import org.ocpsoft.rewrite.config.ConditionBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Discovers server resource types from specific Spring bean types eg. Hibernate Dialect in LocalSessionFactoryBean -> Spring Bean JNDI resource ->
 * Oracle Data Source
 *
 * @author <a href="mailto:bradsdavis@gmail.com">Brad Davis</a>
 */
@RuleMetadata(phase = InitialAnalysisPhase.class, after = DiscoverSpringConfigurationFilesRuleProvider.class, perform = "Resolve Spring JNDI to DataSource")
public class ResolveSpringHibernateJPADataSourceRuleProvider extends IteratingRuleProvider<SpringBeanModel>
{
    private static final Logger LOG = Logger.getLogger(ResolveSpringHibernateJPADataSourceRuleProvider.class.getSimpleName());

    @Override
    public ConditionBuilder when()
    {
        return Query.fromType(SpringBeanModel.class);
    }

    @Override
    public void perform(GraphRewrite event, EvaluationContext context, SpringBeanModel payload)
    {
        // handles only xml based spring beans with certain java classes, such as LocalSessionFactoryBean
        if (payload.getSpringConfiguration() == null || payload.getJavaClass() == null
                    || !isLocalSessionFactoryBean(payload.getJavaClass().getQualifiedName()))
        {
            return;
        }

        SpringConfigurationFileModel springConfig = payload.getSpringConfiguration();
        Document doc = springConfig.asDocument();

        if (doc == null)
        {
            LOG.warning("Document corrupt. Skipping.");
            return;
        }

        for (Element bean : $(doc).find("bean").filter(springid(payload.getSpringBeanName())))
        {
            String dsBeanNameRef = extractJndiRefBeanName(bean);
            if (StringUtils.isBlank(dsBeanNameRef))
            {
                continue;
            }

            String hibernateDialect = null;
            Map<String, String> hibernateProperties = extractProperties(doc, bean);
            hibernateProperties.putAll(extractHibernateJpaVendorJpaProperties(doc, bean));
            if (hibernateProperties.containsKey("hibernate.dialect"))
            {
                hibernateDialect = hibernateProperties.get("hibernate.dialect");
            }

            String springDbName = extractHibernateJpaVendorDatabase(doc, bean);
            processHibernateSessionFactoryBean(event, dsBeanNameRef, hibernateDialect, springDbName);
            return;
        }
        LOG.warning("Did not find bean: " + payload.getSpringBeanName() + " to process within: " + springConfig.getFileName());
    }

    private boolean isLocalSessionFactoryBean(String qualifiedName)
    {
        if (qualifiedName == null)
        {
            return false;
        }
        return (qualifiedName.equals("org.springframework.orm.hibernate3.LocalSessionFactoryBean")
        			|| qualifiedName.equals("org.springframework.orm.hibernate3.AbstractSessionFactoryBean")
        			|| qualifiedName.equals("org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean")
                    || qualifiedName.equals("org.springframework.orm.hibernate4.LocalSessionFactoryBean")
                    || qualifiedName.equals("org.springframework.orm.hibernate5.LocalSessionFactoryBean")
                    || qualifiedName.equals("org.springframework.orm.jpa.LocalEntityManagerFactoryBean")
                    || qualifiedName.equals("org.springframework.orm.jpa.vendor.HibernateJpaSessionFactoryBean")
                    || qualifiedName.equals("org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"));
    }

    /*
     * <bean id="jpaVendorAdapter2" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="dataSource" value="HSQL"/>
     * </bean>
     */
    private String extractHibernateJpaVendorDatabase(Document doc, Element bean)
    {
        for (Element jpaVendorAdapterProperty : $(bean).children("property").filter(attr("name", "jpaVendorAdapter")).get())
        {
            String propertyRef = $(jpaVendorAdapterProperty).attr("ref");
            if (StringUtils.isNotBlank(propertyRef))
            {
                // look for the properties referenced by a local bean..
                for (Element jpaVendorAdapter : findLocalBeanById(doc, propertyRef))
                {
                    // check attribute on element
                    String propAttrValue = $(jpaVendorAdapter).attr("database");
                    if (StringUtils.isNotBlank(propAttrValue))
                    {
                        return propAttrValue;
                    }

                    // now look for the property "dataSource" off of that bean.
                    for (Element p : $(jpaVendorAdapter).children("property").filter(attr("name", "database")).get())
                    {
                        String value = $(p).attr("value");
                        if (StringUtils.isNotBlank(value))
                        {
                            return value;
                        }
                    }
                }
            }
        }

        return null;
    }

    private Map<String, String> extractHibernateJpaVendorJpaProperties(Document doc, Element bean)
    {
        Map<String, String> properties = new HashMap<>();
        for (Element jpaVendorAdapterProperty : $(bean).children("property").filter(attr("name", "jpaVendorAdapter")).get())
        {
            String propertyRef = $(jpaVendorAdapterProperty).attr("ref");
            if (StringUtils.isNotBlank(propertyRef))
            {
                // look for the properties referenced by a local bean..
                for (Element jpaVendorAdapter : findLocalBeanById(doc, propertyRef))
                {
                    properties = extractProperties(doc, jpaVendorAdapter);

                    // read the hibernate dialect
                    for (Element p : $(jpaVendorAdapter).children("property").filter(attr("name", "databasePlatform")).get())
                    {
                        String dialect = $(p).attr("value");
                        if (StringUtils.isNotBlank(dialect))
                        {
                            if (!properties.containsKey("hibernate.dialect"))
                            {
                                properties.put("hibernate.dialect", dialect);
                            }
                        }
                    }
                }
            }
        }
        return properties;
    }

    private Map<String, String> extractProperties(Document doc, Element bean)
    {
        Map<String, String> properties = new HashMap<>();
        for (Element p : $(bean).children("property").filter(attr("name", "hibernateProperties", "jpaProperties", "jpaPropertyMap")).get())
        {
            // first, check to see if it uses a ref attribute...
            String propertyRef = $(p).attr("ref");
            if (StringUtils.isNotBlank(propertyRef))
            {
                // look for the properties referenced by a local bean..
                for (Element ref : findLocalBeanById(doc, propertyRef))
                {
                    properties.putAll(readProperties(ref));
                }
            }
            else
            {
                properties.putAll(readProperties(p));
            }
        }
        return properties;
    }

    private Iterable<Element> findLocalBeanById(Document doc, String id)
    {
        List<Element> elements = new LinkedList<>();
        elements.addAll($(doc).children().filter(attr("id", id)).get());

        if (elements.isEmpty())
        {
            elements.addAll($(doc).children().filter(attr("name", id)).get());
        }
        return elements;
    }

    private String extractJndiRefBeanName(Element bean)
    {
        for (Element dataSource : $(bean).children("property").filter(attr("name", "dataSource")).get())
        {
            // read ref...
            String jndiRef = $(dataSource).attr("ref");
            if (StringUtils.isBlank(jndiRef))
            {
                LOG.info("Looking at ref child of property tag...");
                //look to see if the ref is a child of the property tag...
                jndiRef = $(dataSource).child("ref").attr("bean");
            }
            if(StringUtils.isNotBlank(jndiRef)) {
                return jndiRef;
            }
        }

        return null;
    }

    /*
     * Reads Spring maps, properties, and value pair xml
     */
    private Map<String, String> readProperties(Element properties)
    {
        Map<String, String> values = new HashMap<>();
        for (Element p : $(properties).find("prop"))
        {
            String key = $(p).attr("key");
            String val = $(p).text();
            values.put(key, val);
        }
        for (Element p : $(properties).find("entry"))
        {
            String key = $(p).attr("key");
            String val = $(p).attr("value");
            values.put(key, val);
        }
        for (Element p : $(properties).find("value"))
        {
            String propVal = StringUtils.trim($(p).text());
            String key = StringUtils.substringBefore(propVal, "=");
            String val = StringUtils.substringAfter(propVal, "=");
            values.put(key, val);
        }
        return values;
    }

    /*
     * Takes a JNDI reference and turns the JNDI reference into a database, typing the database based on either the hibernate dialect or the spring
     * database name
     */
    private void processHibernateSessionFactoryBean(GraphRewrite event, String dsBeanName, String hibernateDialect, String springDatabaseName)
    {
        LOG.info("DS Name: " + dsBeanName + ", " + hibernateDialect + ", " + springDatabaseName);
        SpringBeanService springBeanService = new SpringBeanService(event.getGraphContext());
        DataSourceService dataSourceService = new DataSourceService(event.getGraphContext());

        for (SpringBeanModel model : springBeanService.findAllBySpringBeanName(dsBeanName))
        {
            if (model instanceof JNDIReferenceModel && ((JNDIReferenceModel) model).getJndiReference() != null)
            {
                // then this is likely a datasource; set JNDI to Datasource
                JNDIReferenceModel ref = (JNDIReferenceModel) model;
                DataSourceModel dataSource = dataSourceService.addTypeToModel(ref.getJndiReference());

                if (StringUtils.isNotBlank(hibernateDialect))
                {
                    LOG.info(" - Resolved Hibernate dialect: " + hibernateDialect);
                    String resolvedType = HibernateDialectDataSourceTypeResolver.resolveDataSourceTypeFromDialect(hibernateDialect);
                    if (StringUtils.isNotBlank(resolvedType))
                    {
                        dataSource.setDatabaseTypeName(resolvedType);
                    }
                }
                else if (StringUtils.isNotBlank(springDatabaseName))
                {
                    LOG.info(" - Resolved Spring database type: " + springDatabaseName);
                    String resolvedType = SpringDataSourceTypeResolver.resolveDataSourceTypeFromDialect(springDatabaseName);
                    if (StringUtils.isNotBlank(resolvedType))
                    {
                        dataSource.setDatabaseTypeName(resolvedType);
                    }
                }

            }
            else
            {
                LOG.warning("Not JNDI Reference.");
            }
        }
    }

    /*
     * Filters Joox by the spring bean's name (name or id) leveraging name to support legacy spring
     */
    public static FastFilter springid(final String id)
    {
        return new FastFilter()
        {
            @Override
            public boolean filter(Context context)
            {
                String name = $(context).attr("name");
                String idVal = $(context).attr("id");

                LOG.info("Matching: " + id + " Against -- ID: " + idVal + " Name: " + name);

                return (StringUtils.equals(id, idVal) || StringUtils.equals(id, name));
            }
        };
    }
}
