/*
 *  Copyright (c) 2005-2011, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. 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.wso2.carbon.apimgt.impl.workflow;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.api.APIManagementException;
import org.wso2.carbon.apimgt.impl.internal.ServiceReferenceHolder;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.securevault.SecretResolver;
import org.wso2.securevault.SecretResolverFactory;
import org.wso2.securevault.commons.MiscellaneousUtil;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

public class TenantWorkflowConfigHolder implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final Log log = LogFactory.getLog(TenantWorkflowConfigHolder.class);

    private static final QName PROP_Q = new QName("Property");

    private static final QName ATT_NAME = new QName("name");

    private static final String DEFAULT_SUBSCRIPTION_UPDATE_EXECUTOR_CLASS =
            "org.wso2.carbon.apimgt.impl.workflow.SubscriptionUpdateSimpleWorkflowExecutor";

    private transient SecretResolver secretResolver;

    private String tenantDomain;

    private int tenantId;

    private Map<String, WorkflowExecutor> workflowExecutorMap;

    public TenantWorkflowConfigHolder(String tenantDomain, int tenantId) {
        this.tenantDomain = tenantDomain;
        this.tenantId = tenantId;
    }

    public WorkflowExecutor getWorkflowExecutor(String workflowExecutorType) {

        return workflowExecutorMap.get(workflowExecutorType);
    }

    public void load() throws WorkflowException, RegistryException {

        workflowExecutorMap = new ConcurrentHashMap<>();
        InputStream in = null;

        try {
            String workFlowConfig =
                    ServiceReferenceHolder.getInstance().getApimConfigService().getWorkFlowConfig(tenantDomain);
            if (StringUtils.isNotEmpty(workFlowConfig)){
                in = new ByteArrayInputStream(workFlowConfig.getBytes());

                StAXOMBuilder builder = new StAXOMBuilder(in);

                secretResolver = SecretResolverFactory.create(builder.getDocumentElement(), true);

                OMElement workflowExtensionsElem = builder.getDocument().getFirstChildWithName(
                        new QName(WorkflowConstants.WORKFLOW_EXTENSIONS));

                OMElement workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.APPLICATION_CREATION));
                String executorClass;
                Class clazz;
                WorkflowExecutor workFlowExecutor;
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e1) {
                    workFlowExecutor = new ApplicationCreationSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_APPLICATION_CREATION, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.PRODUCTION_APPLICATION_REGISTRATION));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e1) {
                    workFlowExecutor = new ApplicationRegistrationSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_APPLICATION_REGISTRATION_PRODUCTION, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.SANDBOX_APPLICATION_REGISTRATION));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e1) {
                    workFlowExecutor = new ApplicationRegistrationSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_APPLICATION_REGISTRATION_SANDBOX, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.USER_SIGN_UP));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new UserSignUpSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_USER_SIGNUP, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.SUBSCRIPTION_CREATION));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new SubscriptionCreationSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_SUBSCRIPTION_CREATION, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.SUBSCRIPTION_UPDATE));
                if (workflowElem != null) {
                    executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                    try {
                        clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                        workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                        loadProperties(workflowElem, workFlowExecutor);
                    } catch (ClassNotFoundException e) {
                        workFlowExecutor = new SubscriptionUpdateSimpleWorkflowExecutor();
                    }

                    workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_SUBSCRIPTION_UPDATE, workFlowExecutor);
                } else {
                    workFlowExecutor = new SubscriptionUpdateSimpleWorkflowExecutor();
                    workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_SUBSCRIPTION_UPDATE, workFlowExecutor);
                }

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.SUBSCRIPTION_DELETION));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new SubscriptionDeletionSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_SUBSCRIPTION_DELETION, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(
                        new QName(WorkflowConstants.APPLICATION_DELETION));
                executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new ApplicationDeletionSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_APPLICATION_DELETION, workFlowExecutor);

                workflowElem = workflowExtensionsElem.getFirstChildWithName(new QName(WorkflowConstants.API_STATE_CHANGE));
                if (workflowElem == null) {
                    // TO handle migrated environment, create the default simple workflow executor
                    workflowElem = OMAbstractFactory.getOMFactory()
                            .createOMElement(new QName(WorkflowConstants.API_STATE_CHANGE));
                    executorClass = WorkflowConstants.DEFAULT_EXECUTOR_API_STATE_CHANGE;
                    workflowElem.addAttribute(WorkflowConstants.EXECUTOR, executorClass, null);
                } else {
                    executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                }
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new APIStateChangeSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_API_STATE, workFlowExecutor);

                workflowElem =
                        workflowExtensionsElem.getFirstChildWithName(new QName(WorkflowConstants
                                .API_PRODUCT_STATE_CHANGE));
                if (workflowElem == null) {
                    // To handle migration, create the default simple workflow executor
                    workflowElem =
                            OMAbstractFactory.getOMFactory().createOMElement(new QName(WorkflowConstants
                                    .API_PRODUCT_STATE_CHANGE));
                    executorClass = WorkflowConstants.DEFAULT_EXECUTOR_API_PRODUCT_STATE_CHANGE;
                    workflowElem.addAttribute(WorkflowConstants.EXECUTOR, executorClass, null);
                } else {
                    executorClass = workflowElem.getAttributeValue(new QName(WorkflowConstants.EXECUTOR));
                }
                try {
                    clazz = TenantWorkflowConfigHolder.class.getClassLoader().loadClass(executorClass);
                    workFlowExecutor = (WorkflowExecutor) clazz.newInstance();
                    loadProperties(workflowElem, workFlowExecutor);
                } catch (ClassNotFoundException e) {
                    workFlowExecutor = new APIProductStateChangeSimpleWorkflowExecutor();
                }

                workflowExecutorMap.put(WorkflowConstants.WF_TYPE_AM_API_PRODUCT_STATE, workFlowExecutor);
            }
        } catch (XMLStreamException e) {
            log.error("Error building xml", e);
            handleException("Error building xml", e);
        } catch (InstantiationException e) {
            log.error("Unable to instantiate class", e);
            handleException("Unable to instantiate class", e);
        } catch (IllegalAccessException e) {
            log.error("Illegal attempt to invoke class methods", e);
            handleException("Illegal attempt to invoke class methods", e);
        } catch (WorkflowException e) {
            log.error("Unable to load workflow executor class", e);
            handleException("Unable to load workflow executor class", e);
        } catch (APIManagementException e) {
            log.error("Unable to retrieve workflow configurations", e);
            handleException("Unable to retrieve workflow configurations", e);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    private void loadProperties(OMElement executorElem, Object workflowClass) throws WorkflowException {

        for (Iterator it = executorElem.getChildrenWithName(PROP_Q); it.hasNext(); ) {
            OMElement propertyElem = (OMElement) it.next();
            OMAttribute attribute = propertyElem.getAttribute(ATT_NAME);
            if (attribute == null) {
                handleException("An Executor class property must specify the name attribute");
            } else {
                String propName = attribute.getAttributeValue();
                OMNode omElt = propertyElem.getFirstElement();
                if (omElt != null) {
                    setInstanceProperty(propName, omElt, workflowClass);
                } else if (propertyElem.getText() != null) {
                    String value = MiscellaneousUtil.resolve(propertyElem, secretResolver);
                    setInstanceProperty(propName, value, workflowClass);
                } else {
                    handleException("An Executor class property must specify " +
                            "name and text value, or a name and a child XML fragment");
                }
            }
        }
    }

    /**
     * Find and invoke the setter method with the name of form setXXX passing in the value given
     * on the POJO object
     *
     * @param name name of the setter field
     * @param val  value to be set
     * @param obj  POJO instance
     */
    public void setInstanceProperty(String name, Object val, Object obj) throws WorkflowException {

        String mName = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
        Method method;

        try {
            Method[] methods = obj.getClass().getMethods();
            boolean invoked = false;

            for (Method method1 : methods) {
                if (mName.equals(method1.getName())) {
                    Class[] params = method1.getParameterTypes();
                    if (params.length != 1) {
                        handleException("Did not find a setter method named : " + mName +
                                "() that takes a single String, int, long, float, double ," +
                                "OMElement or boolean parameter");
                    } else if (val instanceof String) {
                        String value = (String) val;
                        if (String.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, String.class);
                            method.invoke(obj, new String[]{value});
                        } else if (int.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, int.class);
                            method.invoke(obj, new Integer[]{Integer.valueOf(value)});
                        } else if (long.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, long.class);
                            method.invoke(obj, new Long[]{Long.valueOf(value)});
                        } else if (float.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, float.class);
                            method.invoke(obj, new Float[]{new Float(value)});
                        } else if (double.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, double.class);
                            method.invoke(obj, new Double[]{new Double(value)});
                        } else if (boolean.class.equals(params[0])) {
                            method = obj.getClass().getMethod(mName, boolean.class);
                            method.invoke(obj, new Boolean[]{Boolean.valueOf(value)});
                        } else if (Class.forName("[C").equals(params[0])) {
                            method = obj.getClass().getMethod(mName, Class.forName("[C"));
                            method.invoke(obj, value.toCharArray());
                        } else {
                            continue;
                        }
                    } else if (val instanceof OMElement && OMElement.class.equals(params[0])) {
                        method = obj.getClass().getMethod(mName, OMElement.class);
                        method.invoke(obj, new OMElement[]{(OMElement) val});
                    } else {
                        continue;
                    }
                    invoked = true;
                    break;
                }
            }
            if (!invoked) {
                handleException("Did not find a setter method named : " + mName +
                        "() that takes a single String, int, long, float, double " +
                        "or boolean parameter");
            }
        } catch (Exception e) {
            handleException("Error invoking setter method named : " + mName +
                    "() that takes a single String, int, long, float, double " +
                    "or boolean parameter", e);
        }
    }

    private static void handleException(String msg) throws WorkflowException {

        log.error(msg);
        throw new WorkflowException(msg);
    }

    private static void handleException(String msg, Exception e) throws WorkflowException {

        log.error(msg, e);
        throw new WorkflowException(msg, e);
    }
}
