/**
 * Licensed 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.jboss.loom.migrators.connectionFactories;

import java.io.File;
import java.io.IOException;
import java.util.*;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang.StringUtils;
import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.dmr.ModelNode;
import org.jboss.loom.actions.CliCommandAction;
import org.jboss.loom.actions.CopyFileAction;
import org.jboss.loom.conf.GlobalConfiguration;
import org.jboss.loom.ctx.MigrationContext;
import org.jboss.loom.ctx.MigratorData;
import org.jboss.loom.ex.CliScriptException;
import org.jboss.loom.ex.LoadMigrationException;
import org.jboss.loom.ex.MigrationException;
import org.jboss.loom.migrators.AbstractMigrator;
import org.jboss.loom.migrators.connectionFactories.jaxb.*;
import org.jboss.loom.spi.IConfigFragment;
import org.jboss.loom.spi.ann.ConfigPartDescriptor;
import org.jboss.loom.utils.Utils;
import org.jboss.loom.utils.XmlUtils;
import org.jboss.loom.utils.as7.CliAddScriptBuilder;
import org.jboss.loom.utils.as7.CliApiCommandBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Migrator of Resource Adapter(Connection Factories in AS5) subsystem implementing IMigrator
 *
 * @author Roman Jakubco
 */
@ConfigPartDescriptor(
    name = "Resource adapters configuration"
)
public class ResAdapterMigrator extends AbstractMigrator {
    private static final Logger log = LoggerFactory.getLogger(ResAdapterMigrator.class);

    @Override
    protected String getConfigPropertyModuleName() {
        return "resourceAdapter";
    }

    public ResAdapterMigrator(GlobalConfiguration globalConfig) {
        super(globalConfig);
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void loadSourceServerConfig(MigrationContext ctx) throws LoadMigrationException {
        try {

            // Deployments AS 5 dir.
            File dsFiles = getGlobalConfig().getAS5Config().getDeployDir();
            if (!dsFiles.canRead())
                throw new LoadMigrationException("Can't read: " + dsFiles.getPath());


            // -ds.xml files.
            SuffixFileFilter sf = new SuffixFileFilter("-ds.xml");
            Collection<File> dsXmls = FileUtils.listFiles(dsFiles, sf, FileFilterUtils.trueFileFilter());
            log.debug("  Found -ds.xml files #: " + dsXmls.size());
            if (dsXmls.isEmpty())
                return;

            List<ConnectionFactoriesBean> connFactories = new LinkedList();
            Unmarshaller dataUnmarshaller = JAXBContext.newInstance(ConnectionFactoriesBean.class).createUnmarshaller();

            // For each -ds.xml
            for (File dsXml : dsXmls) {
                Document doc = XmlUtils.parseFileToXmlDoc(dsXml);

                Element element = doc.getDocumentElement();
                if ("connection-factories".equals(element.getTagName())) {
                    ConnectionFactoriesBean conn = (ConnectionFactoriesBean) dataUnmarshaller.unmarshal(dsXml);
                    connFactories.add(conn);
                }
            }

            MigratorData migrData = new MigratorData();

            for (ConnectionFactoriesBean cf : connFactories) {
                if (cf.getConnectionFactories() != null) {
                    migrData.getConfigFragments().addAll(cf.getConnectionFactories());
                }
                if (cf.getNoTxConnectionFactories() != null) {
                    migrData.getConfigFragments().addAll(cf.getNoTxConnectionFactories());
                }
            }

            ctx.getMigrationData().put(ResAdapterMigrator.class, migrData);

        } catch (JAXBException | SAXException | IOException e) {
            throw new LoadMigrationException(e);
        }
    }// loadSourceServerConfig()

    
    
    /**
     * {@inheritDoc}
     */
    @Override
    public void createActions(MigrationContext ctx) throws MigrationException {
        
        Set<String> referencedRARs = new HashSet();
        
        // Process the config fragments found in AS 5.
        for( IConfigFragment fragment : ctx.getMigrationData().get(ResAdapterMigrator.class).getConfigFragments() ) {

            String raType = null;
            try {
                if( fragment instanceof ConnectionFactoryAS5Bean ) {
                    raType = "tx-connection-factory";
                    final ResourceAdapterBean raBean = migrateConnFactory( (ConnectionFactoryAS5Bean) fragment, referencedRARs );
                    ctx.getActions().addAll( createResourceAdapterCliCommand( raBean ) );
                    continue;
                }
                if( fragment instanceof NoTxConnectionFactoryAS5Bean ) {
                    raType = "no-tx-connection-factory";
                    final ResourceAdapterBean raBean = migrateConnFactory( (NoTxConnectionFactoryAS5Bean) fragment, referencedRARs );
                    ctx.getActions().addAll( createResourceAdapterCliCommand( raBean ) );
                    continue;
                }
                throw new MigrationException("Config fragment unrecognized by " + this.getClass().getSimpleName() + ": " + fragment);
            }
            catch (CliScriptException e) {
                throw new MigrationException("Migration of " + raType +" failed: " + e.getMessage(), e);
            }
        }

        // Try to find each .rar referenced in the configuration XML.
        for( String referencedRAR : referencedRARs ) {
            
            final File profileDir = getGlobalConfig().getAS5Config().getProfileDir(); //  .../deployments ?
            Collection<File> foundRARs;
            try {
                foundRARs = Utils.searchForFileOrDir( referencedRAR, profileDir );
            } catch ( IOException ex ) {
                throw new MigrationException("Can't find " + referencedRAR + ": " + ex.getMessage(), ex);
            }
            File rarFrom = foundRARs.iterator().next();
            File rarTo = Utils.createPath(getGlobalConfig().getAS7Config().getDir(), "standalone", "deployments", rarFrom.getName());
            CopyFileAction action = new CopyFileAction( this.getClass(), rarFrom, rarTo, CopyFileAction.IfExists.SKIP);
            if( foundRARs.size() > 1 ){
                String warn = "Found multiple '" + referencedRAR + "' in '" + profileDir + "':\n  " + StringUtils.join( foundRARs, "\n  ");
                action.addWarning( warn );
            }
            ctx.getActions().add( action );
        }
    }// createActions()

    
    
    /**
     * Migrates Connection-Factory (both types) from AS5 to Resource-Adapter in AS7
     *
     * @param connFactoryAS5 Connection-Factory for migration
     * @param rars Files already set for copy into AS7
     * @return created Resource-Adapter with the configuration of the given Connection-Factory
     */
    private static ResourceAdapterBean migrateConnFactory(AbstractConnectionFactoryAS5Bean connFactoryAS5, Set<String> rars){
        ResourceAdapterBean resAdapter = new ResourceAdapterBean();

        rars.add(connFactoryAS5.getRarName());

        resAdapter.setJndiName(connFactoryAS5.getJndiName());
        resAdapter.setArchive(connFactoryAS5.getRarName());

        ConnectionDefinitionBean connDef = new ConnectionDefinitionBean();

        if( connFactoryAS5 instanceof ConnectionFactoryAS5Bean ) {
            if ( ( ( ConnectionFactoryAS5Bean )connFactoryAS5 ).getXaTransaction() != null ) {
                resAdapter.setTransactionSupport( "XATransaction" );
                ConnectionFactoryAS5Bean tempFactory = ( ConnectionFactoryAS5Bean ) connFactoryAS5;

                connDef.setXaResourceTimeout( tempFactory.getXaResourceTimeout() );
                connDef.setXaMaxPoolSize( tempFactory.getMaxPoolSize() );
                connDef.setXaMinPoolSize( tempFactory.getMinPoolSize() );
                connDef.setXaNoTxSeparatePools( tempFactory.getNoTxSeparatePools() );
                connDef.setXaPrefill( tempFactory.getPrefill() );
            } else {
                resAdapter.setTransactionSupport( "LocalTransaction" );
                connDef.setMaxPoolSize( connFactoryAS5.getMaxPoolSize() );
                connDef.setMinPoolSize( connFactoryAS5.getMinPoolSize() );
                connDef.setPrefill( connFactoryAS5.getPrefill() );
            }
        } else {
            resAdapter.setTransactionSupport( "NoTransaction" );
        }

        connDef.setJndiName( "java:jboss/" + connFactoryAS5.getJndiName() );
        connDef.setPoolName( connFactoryAS5.getJndiName() );
        connDef.setEnabled( "true" );
        connDef.setUseJavaCont( "true" );
        connDef.setEnabled( "true" );
        connDef.setClassName( connFactoryAS5.getConnectionDefinition() );
        connDef.setPrefill( connFactoryAS5.getPrefill() );

        if ( connFactoryAS5.getConfigProperties() != null ) {
            for (ConfigPropertyBean configProperty : connFactoryAS5.getConfigProperties()) {
                configProperty.setType( null );
            }
            connDef.setConfigProperties( connFactoryAS5.getConfigProperties() );
        }

        if ( connFactoryAS5.getApplicationManagedSecurity() != null ) {
            connDef.setAppManagedSec(connFactoryAS5.getApplicationManagedSecurity());
        } else if ( connFactoryAS5.getSecurityDomain() != null ) {
            connDef.setSecurityDomain(connFactoryAS5.getSecurityDomain());
        } else if ( connFactoryAS5.getSecDomainAndApp() != null ) {
            connDef.setSecDomainAndApp(connFactoryAS5.getSecDomainAndApp());
        }

        connDef.setMinPoolSize( connFactoryAS5.getMinPoolSize() );
        connDef.setMaxPoolSize( connFactoryAS5.getMaxPoolSize() );

        connDef.setBackgroundValidation( connFactoryAS5.getBackgroundValid() );
        connDef.setBackgroundValiMillis( connFactoryAS5.getBackgroundValiMillis() );

        connDef.setBlockingTimeoutMillis( connFactoryAS5.getBlockingTimeoutMillis() );
        connDef.setIdleTimeoutMinutes( connFactoryAS5.getIdleTimeoutMin() );
        connDef.setAllocationRetry( connFactoryAS5.getAllocationRetry() );
        connDef.setAllocRetryWaitMillis( connFactoryAS5.getAllocRetryWaitMillis() );

        Set<ConnectionDefinitionBean> connDefColl = new HashSet();
        connDefColl.add(connDef);
        resAdapter.setConnectionDefinitions( connDefColl );

        return resAdapter;
    }

    /**
     * Creates a list of CliCommandActions for adding a Resource-Adapter
     *
     * @param adapter Resource-Adapter
     * @return list of created CliCommandActions for adding the Resource-Adapter
     * @throws CliScriptException if required attributes for a creation of the CLI command of the Resource-Adapter are
     *                            missing or are empty (archive-name)
     */
    private static List<CliCommandAction> createResourceAdapterCliCommand(ResourceAdapterBean adapter) throws CliScriptException {
        String errMsg = " in resource-adapter(connection-factories in AS5) must be set.";
        Utils.throwIfBlank(adapter.getArchive(), errMsg, "Archive name");

        List<CliCommandAction> actions = new LinkedList();

        ModelNode adapterCmd = new ModelNode();
        adapterCmd.get(ClientConstants.OP).set(ClientConstants.ADD);
        adapterCmd.get(ClientConstants.OP_ADDR).add("subsystem", "resource-adapters");
        adapterCmd.get(ClientConstants.OP_ADDR).add("resource-adapter", adapter.getJndiName());

        CliApiCommandBuilder builder = new CliApiCommandBuilder(adapterCmd);

        builder.addPropertyIfSet("archive", adapter.getArchive());
        builder.addPropertyIfSet("transaction-support", adapter.getTransactionSupport());

        actions.add( new CliCommandAction( ResAdapterMigrator.class, createResAdapterScript(adapter), builder.getCommand()));

        if (adapter.getConnectionDefinitions() != null) {
            for (ConnectionDefinitionBean connDef : adapter.getConnectionDefinitions()) {
                actions.addAll(createConDefinitionCliAction(adapter, connDef));
            }
        }

        return actions;
    }

    /**
     * Creates CliCommandAction for adding a Connection-Definition of the specific Resource-Adapter
     *
     * @param adapter Resource-Adapter containing connection-definition
     * @param def     Connection-Definition
     * @return created CliCommandAction for adding the Connection-Definition
     * @throws CliScriptException
     *          if required attributes for a creation of the CLI command of the Connection-Definition
     *          are missing or are empty (class-name, pool-name)
     */
    private static List<CliCommandAction> createConDefinitionCliAction( 
            ResourceAdapterBean adapter, ConnectionDefinitionBean def ) throws CliScriptException 
    {
        String errMsg = "in connection-definition in resource-adapter(connection-factories) must be set";
        Utils.throwIfBlank(def.getClassName(), errMsg, "Class-name");
        Utils.throwIfBlank(def.getPoolName(), errMsg, "Pool-name");

        List<CliCommandAction> actions = new LinkedList();

        ModelNode connDefCmd = new ModelNode();
        connDefCmd.get(ClientConstants.OP).set(ClientConstants.ADD);
        connDefCmd.get(ClientConstants.OP_ADDR).add("subsystem", "resource-adapters");
        connDefCmd.get(ClientConstants.OP_ADDR).add("resource-adapter", adapter.getJndiName());
        connDefCmd.get(ClientConstants.OP_ADDR).add("connection-definitions", def.getPoolName());

        CliApiCommandBuilder builder = new CliApiCommandBuilder(connDefCmd);

        builder.addPropertyIfSet("jndi-name", def.getJndiName());
        builder.addPropertyIfSet("enabled", def.getEnabled());
        builder.addPropertyIfSet("use-java-context", def.getUseJavaCont());
        builder.addPropertyIfSet("class-name", def.getClassName());
        builder.addPropertyIfSet("use-ccm", def.getUseCcm());

        builder.addPropertyIfSet("flush-strategy", def.getFlushStrategy());

        if( adapter.getTransactionSupport().equalsIgnoreCase( "xatransaction" ) ) {
            builder.addPropertyIfSet("min-pool-size", def.getXaMinPoolSize());
            builder.addPropertyIfSet("max-pool-size", def.getXaMaxPoolSize());
            builder.addPropertyIfSet("prefill", def.getXaPrefill());
            builder.addPropertyIfSet("xa-resource-timeout", def.getXaResourceTimeout());
            builder.addPropertyIfSet("no-tx-separate-pools", def.getXaNoTxSeparatePools());
            builder.addPropertyIfSet("use-strict-min",def.getXaUseStrictMin());
        } else {
            builder.addPropertyIfSet("min-pool-size", def.getMinPoolSize());
            builder.addPropertyIfSet("max-pool-size", def.getMaxPoolSize());
            builder.addPropertyIfSet("prefill", def.getPrefill());
            builder.addPropertyIfSet("use-strict-min", def.getUseStrictMin());
        }

        if (def.getSecurityDomain() != null) {
            builder.addPropertyIfSet("security-domain", def.getSecurityDomain());
        } else if (def.getSecDomainAndApp() != null) {
            builder.addPropertyIfSet("security-domain-and-application", def.getSecDomainAndApp());
        } else if (def.getAppManagedSec() != null) {
            builder.addPropertyIfSet("application-managed-security", def.getAppManagedSec());
        }

        builder.addPropertyIfSet("background-validation", def.getBackgroundValidation());
        builder.addPropertyIfSet("background-validation-millis", def.getBackgroundValiMillis());
        builder.addPropertyIfSet("blocking-timeout-millis", def.getBlockingTimeoutMillis());
        builder.addPropertyIfSet("idle-timeout-minutes", def.getIdleTimeoutMinutes());
        builder.addPropertyIfSet("allocation-retry", def.getAllocationRetry());
        builder.addPropertyIfSet("allocation-retry-wait-millis", def.getAllocRetryWaitMillis());
        builder.addPropertyIfSet("xa-resource-timeout", def.getXaResourceTimeout());

        actions.add( new CliCommandAction( ResAdapterMigrator.class, createConnDefinitionScript(adapter, def), builder.getCommand()));

        if( def.getConfigProperties() != null ) {
            for( ConfigPropertyBean configProperty : def.getConfigProperties() ) {
                actions.add( createPropertyCliAction( adapter, def, configProperty ) );
            }
        }

        return actions;
    }

    /**
     * Creates CliCommandAction for adding a Config-Property of the specific Connection-Definition of the specific
     * Resource-Adapter
     *
     * @param adapter  Resource-Adapter containing Connection-Definition
     * @param def      Connection-Definition containg Config-Property
     * @param property Config-Property
     * @return created CliCommandAction for adding the Config-Property
     * @throws CliScriptException if required attributes for a creation of the CLI command of the config-property
     *                            are missing or are empty (name)
     */
    private static CliCommandAction createPropertyCliAction(ResourceAdapterBean adapter, ConnectionDefinitionBean def,
                                                            ConfigPropertyBean property) throws CliScriptException {
        String errMsg = "of config-property in connection-definition in resource-adapter must be set.";
        Utils.throwIfBlank(property.getConfigPropertyName(), errMsg, "Name");

        ModelNode propertyCmd = new ModelNode();
        propertyCmd.get(ClientConstants.OP).set(ClientConstants.ADD);
        propertyCmd.get(ClientConstants.OP_ADDR).add("subsystem", "resource-adapters");
        propertyCmd.get(ClientConstants.OP_ADDR).add("resource-adapter", adapter.getJndiName());
        propertyCmd.get(ClientConstants.OP_ADDR).add("connection-definitions", def.getPoolName());
        propertyCmd.get(ClientConstants.OP_ADDR).add("config-properties", property.getConfigPropertyName());

        propertyCmd.get("value").set(property.getConfigProperty());

        return new CliCommandAction( ResAdapterMigrator.class, createPropertyScript(adapter, def, property), propertyCmd);
    }

    /**
     * Creates CLI script for adding of the Resource-Adapter
     *
     * @param resourceAdapter object of Resource-Adapter
     * @return string containing created CLI script
     * @throws CliScriptException if required attributes are missing
     */
    private static String createResAdapterScript(ResourceAdapterBean resourceAdapter)
            throws CliScriptException {
        String errMsg = " in resource-adapter(connection-factories in AS5) must be set.";
        Utils.throwIfBlank(resourceAdapter.getArchive(), errMsg, "Archive name");

        StringBuilder resultBuilder = new StringBuilder();
        CliAddScriptBuilder cliBuilder = new CliAddScriptBuilder();

        String adapterScript = "/subsystem=resource-adapters/resource-adapter=" + resourceAdapter.getJndiName() + ":add(";
        cliBuilder.addProperty("archive", resourceAdapter.getArchive());
        cliBuilder.addProperty("transaction-support", resourceAdapter.getTransactionSupport());

        adapterScript = adapterScript.concat(cliBuilder.formatAndClearProps() + ")");
        resultBuilder.append(adapterScript);

        return resultBuilder.toString();
    }

    /**
     * Creates String containing CLI script for adding a Connection-Definition of the specific Resource-Adapter
     *
     * @param adapter Resource-Adapter containing Connection-Definition
     * @param connDef Connection-Definition
     * @return String containing CLI script for adding of the Connection-Definition
     * @throws CliScriptException if required attributes for a creation of the CLI command of the Connection-Definition
     *                            are missing or are empty (class-name, pool-name)
     */
    private static String createConnDefinitionScript(ResourceAdapterBean adapter, ConnectionDefinitionBean connDef)
            throws CliScriptException {
        String errMsg = "in connection-definition in resource-adapter(connection-factories) must be set";
        Utils.throwIfBlank(connDef.getClassName(), errMsg, "Class-name");
        Utils.throwIfBlank(connDef.getPoolName(), errMsg, "Pool-name");

        CliAddScriptBuilder cliBuilder = new CliAddScriptBuilder();

        StringBuilder script = new StringBuilder("/subsystem=resource-adapters/resource-adapter=" + adapter.getJndiName());

        script.append("/connection-definitions=").append(connDef.getPoolName()).append(":add(");

        cliBuilder.addProperty("jndi-name", connDef.getJndiName());
        cliBuilder.addProperty("enabled", connDef.getEnabled());
        cliBuilder.addProperty("use-java-context", connDef.getUseJavaCont());
        cliBuilder.addProperty("class-name", connDef.getClassName());
        cliBuilder.addProperty("use-ccm", connDef.getUseCcm());
        cliBuilder.addProperty("prefill", connDef.getPrefill());
        cliBuilder.addProperty("use-strict-min", connDef.getUseStrictMin());
        cliBuilder.addProperty("flush-strategy", connDef.getFlushStrategy());
        cliBuilder.addProperty("min-pool-size", connDef.getMinPoolSize());
        cliBuilder.addProperty("max-pool-size", connDef.getMaxPoolSize());

        if( connDef.getSecurityDomain() != null ) {
            cliBuilder.addProperty( "security-domain", connDef.getSecurityDomain() );
        } else if( connDef.getSecDomainAndApp() != null ) {
            cliBuilder.addProperty( "security-domain-and-application", connDef.getSecDomainAndApp() );
        } else if( connDef.getAppManagedSec() != null ) {
            cliBuilder.addProperty( "application-managed-security", connDef.getAppManagedSec() );
        }

        cliBuilder.addProperty("background-validation", connDef.getBackgroundValidation());
        cliBuilder.addProperty("background-validation-millis", connDef.getBackgroundValiMillis());
        cliBuilder.addProperty("blocking-timeout-millis", connDef.getBlockingTimeoutMillis());
        cliBuilder.addProperty("idle-timeout-minutes", connDef.getIdleTimeoutMinutes());
        cliBuilder.addProperty("allocation-retry", connDef.getAllocationRetry());
        cliBuilder.addProperty("allocation-retry-wait-millis", connDef.getAllocRetryWaitMillis());
        cliBuilder.addProperty("xa-resource-timeout", connDef.getXaResourceTimeout());

        script.append(cliBuilder.formatAndClearProps()).append(")");

        return script.toString();
    }

    /**
     * Creates CLI script for adding a Config-Property of the specific Connection-Definition of the specific
     * Resource-Adapter
     *
     * @param adapter  Resource-Adapter containing Connection-Definition
     * @param connDef  Connection-Definition containing Config-Property
     * @param property Config-Property
     * @return string containing CLI script for adding the Config-Property
     * @throws CliScriptException if required attributes for a creation of the CLI command of the config-property
     *                            are missing or are empty (name)
     */
    private static String createPropertyScript(
            ResourceAdapterBean adapter, ConnectionDefinitionBean connDef, ConfigPropertyBean property) throws CliScriptException 
    {
        String errMsg = "of config-property in connection-definition in resource-adapter must be set.";
        Utils.throwIfBlank(property.getConfigPropertyName(), errMsg, "Name");

        StringBuilder script = new StringBuilder("/subsystem=resource-adapters/resource-adapter=");

        script.append(adapter.getJndiName());

        script.append("/connection-definitions=").append(connDef.getPoolName());

        script.append("/config-properties=");
        script.append(property.getConfigPropertyName());

        script.append(":add(");
        script.append("value=").append(property.getConfigProperty()).append(")");

        return script.toString();
    }

}// ResAdapterMigrator
