/*
*  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.synapse.commons.throttle.core;

import org.apache.axiom.om.OMElement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyEngine;
import org.apache.neethi.builders.xml.XmlPrimtiveAssertion;
import org.apache.synapse.commons.resolvers.ResolverFactory;
import org.apache.synapse.commons.throttle.core.factory.CallerConfigurationFactory;
import org.apache.synapse.commons.throttle.core.factory.ThrottleConfigurationFactory;
import org.apache.synapse.commons.throttle.core.factory.ThrottleContextFactory;
import javax.xml.namespace.QName;
import java.util.Iterator;
import java.util.List;

/**
 * Factory for creating a throttle instance using throttle policy
 */
public class ThrottleFactory {

    private ThrottleFactory() {
    }

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

    /**
     * Abstraction for processing module policy assertion and create a throttle based on it
     *
     * @param policy Throttle policy
     * @return Throttle instance , if there any module policy assertion , otherwise null
     * @throws ThrottleException
     */
    public static Throttle createModuleThrottle(Policy policy) throws ThrottleException {
        return createThrottle(policy, ThrottleConstants.MODULE_THROTTLE_ASSERTION_QNAME);
    }

    /**
     * Abstraction for processing service policy assertion and create a throttle based on it
     *
     * @param policy Throttle policy
     * @return Throttle instance , if there any service policy assertion , otherwise null
     * @throws ThrottleException
     */
    public static Throttle createServiceThrottle(Policy policy) throws ThrottleException {
        return createThrottle(policy, ThrottleConstants.SERVICE_THROTTLE_ASSERTION_QNAME);
    }

    /**
     * Abstraction for processing operation policy assertion and create a throttle based on it
     *
     * @param policy Throttle policy
     * @return Throttle instance , if there any operation policy assertion , otherwise null
     * @throws ThrottleException
     */
    public static Throttle createOperationThrottle(Policy policy) throws ThrottleException {
        return createThrottle(policy, ThrottleConstants.OPERATION_THROTTLE_ASSERTION_QNAME);
    }

    /**
     * Abstraction for processing mediator policy assertion and create a throttle based on it
     *
     * @param policy Throttle policy
     * @return Throttle instance , if there any mediator policy assertion , otherwise null
     * @throws ThrottleException
     */
    public static Throttle createMediatorThrottle(Policy policy) throws ThrottleException {
        return createThrottle(policy, ThrottleConstants.MEDIATOR_THROTTLE_ASSERTION_QNAME);
    }


    /**
     * Factory method to create a throttle from a given throttle policy
     *
     * @param policy    Throttle policy
     * @param forceRoot Root assertion QName for select correct policy assertion
     * @return Throttle instance
     * @throws ThrottleException
     */
    private static Throttle createThrottle(Policy policy, QName forceRoot)
            throws ThrottleException {

        if (policy == null) {
            if (log.isDebugEnabled()) {
                log.debug("Policy cannot be found");
            }
            //if policy is not available ,then return null for ThrottleConfiguration
            return null; // no policy is available in the module description
        }

        if (forceRoot == null) {
            if (log.isDebugEnabled()) {
                log.debug("Given root assertion QName is null");
            }
            return null;
        }

        for (Iterator iterator = policy.getAlternatives();
             iterator.hasNext(); ) {
            Object object = iterator.next();
            if (object instanceof List) {
                List list = (List) object;
                for (Iterator it = list.iterator(); it.hasNext(); ) {
                    Object assertObj = it.next();
                    if (assertObj instanceof XmlPrimtiveAssertion) {
                        XmlPrimtiveAssertion primitiveAssertion = (XmlPrimtiveAssertion)
                                assertObj;
                        QName qName = primitiveAssertion.getName();

                        if (qName == null) {
                            handleException("Invalid Throttle Policy - QName of the " +
                                    "assertion cannot be null.");
                        }
                        // top policy must contains ThrottleAssertion
                        Policy throttlePolicy = PolicyEngine.
                                getPolicy(primitiveAssertion.getValue());
                        if (ThrottleConstants.THROTTLE_ASSERTION_QNAME.equals(qName)) {
                            return ThrottlePolicyProcessor.processPolicy(throttlePolicy);
                        } else if (forceRoot.equals(qName)) {
                            return buildThrottle(throttlePolicy);
                        } else {
                            if (log.isDebugEnabled()) {
                                log.debug("There is no throttle policy " +
                                        "for given QName : " + forceRoot);
                            }
                        }
                    }
                }
            }
        }
        return null;

    }


    /**
     * Factory method to help to create a throttle
     *
     * @param throtlePolicy Throttle assertion policy
     * @return Throttle instance
     * @throws ThrottleException
     */
    private static Throttle buildThrottle(Policy throtlePolicy) throws ThrottleException {
        Throttle throttle = new Throttle();   // throttle instance
        ThrottleConfiguration configuration = null;  // configuration data
        List list = throtlePolicy.getPolicyComponents();

        if (list == null || (list != null && list.isEmpty())) {
            handleException("Empty the policy components" +
                    " as ThrottleAssertion's children");
        }
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            Object object = iterator.next();
            if (object instanceof Policy) {

                // boolean isOtherConfiguration = false;
                //  // To track default callerConfiguration for all ips
                CallerConfiguration callerConfiguration = null;
                Policy policy = null;
                List assertList = ((Policy) object).getAssertions();
                if (assertList != null) {
                    for (Iterator assertIterator =
                         assertList.iterator(); assertIterator.hasNext(); ) {
                        Object ca = assertIterator.next();
                        if (ca instanceof XmlPrimtiveAssertion) {
                            XmlPrimtiveAssertion id = (XmlPrimtiveAssertion) ca;
                            configuration = createThrottleConfiguration(id, throttle);
                            if (configuration == null) {
                                handleException("Invalid throttle - Throttle configuration " +
                                        "cannot be created from given policy");
                            }

                            if (configuration.getType() == ThrottleConstants.IP_BASE) {
                                //create a caller context for ip based throttle
                                callerConfiguration = CallerConfigurationFactory.
                                        createCallerConfiguration(
                                                ThrottleConstants.IP_BASE);
                            } else if (configuration.getType() == ThrottleConstants.DOMAIN_BASE) {
                                //create a caller context for ip based throttle
                                callerConfiguration = CallerConfigurationFactory.
                                        createCallerConfiguration(
                                                ThrottleConstants.DOMAIN_BASE);
                            } else if (configuration.getType() == ThrottleConstants.ROLE_BASE) {
                                //create a caller context for ip based throttle
                                callerConfiguration = CallerConfigurationFactory.
                                        createCallerConfiguration(
                                                ThrottleConstants.ROLE_BASE);
                            } else {
                                handleException("Invalid throttle type - Only" +
                                        " support IP ,DOMAIN and ROLE as types ");
                            }
                            if (callerConfiguration != null) {
                                OMElement element = id.getValue();

                                // Name of the policy assertion
                                String name = element.getLocalName();
                                // Value of the policy assertion
                                String value = element.getText();

                                // If Value and Name  are null,
                                // then it is a invalid policy configuration
                                if (name == null || value == null) {
                                    handleException("Either Value or" +
                                            " Name of the policy cannot be null");
                                } else if (name.equals(ThrottleConstants.ID_PARAMETER_NAME)) {

                                    if (!value.equals("")) {
                                        callerConfiguration.setID(value);
                                    } else {
                                        handleException("Value of ID cannot find " +
                                                "- invalid configuration");
                                    }
                                } else {
                                    handleException("Undefined policy property for" +
                                            " throttle - Expect ID  ");
                                }
                            }


                        } else if (ca instanceof Policy) {
                            policy = (Policy) ca;
                        }
                    }
                }
                if (callerConfiguration != null) {
                    if (policy != null) {
                        fillCallerConfiguration(policy, callerConfiguration);
                        configuration.addCallerConfiguration(callerConfiguration);
                    }

                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("Couldn't find a callerConfiguration for a throttle" +
                                " configuration for an one caller  ");
                    }
                }

            } else if (object instanceof XmlPrimtiveAssertion) {

                XmlPrimtiveAssertion xmlPrimitiveAssertion = (XmlPrimtiveAssertion) object;
                OMElement element = xmlPrimitiveAssertion.getValue();
                // Name of the policy assertion
                String name = element.getLocalName();
                //Value of the policy assertion
                String value = element.getText();

                //if Value and Name  are null,then
                // it is a invalid policy configuration
                if (name == null || value == null) {
                    handleException("Either value or name of the policy cannot be null");
                } else if (name.equals(
                        ThrottleConstants.MAXIMUM_CONCURRENT_ACCESS_PARAMETER_NAME)) {
                    int intValue = 0;
                    try {
                        intValue = Integer.parseInt(value.trim());
                    } catch (NumberFormatException ignored) {
                        log.error("Error occurred - Invalid number for maximum " +
                                "concurrent access ", ignored);
                    }
                    if (intValue > 0) {
                        throttle.setConcurrentAccessController(
                                new ConcurrentAccessController(intValue));
                    }
                } else {
                    handleException("Invalid throttle policy configuration : unexpected policy " +
                            "element with name " + name);
                }
            }
        }
        return throttle;
    }

    /**
     * Factory method to create a Throttle Configuration instance
     *
     * @param id       Policy assertion relates to a particular caller
     * @param throttle Throttle instance
     * @return Throttle Configuration instance
     * @throws ThrottleException
     */
    private static ThrottleConfiguration createThrottleConfiguration(XmlPrimtiveAssertion id,
                                                      Throttle throttle) throws ThrottleException {
        OMElement element = id.getValue();
        ThrottleConfiguration configuration = null;

        String type = element.getAttributeValue(
                ThrottleConstants.THROTTLE_TYPE_ATTRIBUTE_QNAME);
        if (type == null) {
            handleException("Type of Throttle " +
                    "in the policy cannot be null");
        }
        if ("IP".equals(type)) {
            // create a ip based throttle context and configuration
            configuration = throttle.getThrottleConfiguration
                    (ThrottleConstants.IP_BASED_THROTTLE_KEY);
            if (configuration == null) {
                configuration =
                        ThrottleConfigurationFactory.
                                createThrottleConfiguration(
                                        ThrottleConstants.IP_BASE);
                throttle.addThrottleContext(
                        ThrottleConstants.IP_BASED_THROTTLE_KEY,
                        ThrottleContextFactory.createThrottleContext(
                                ThrottleConstants.IP_BASE, configuration));
                throttle.addThrottleConfiguration(
                        ThrottleConstants.IP_BASED_THROTTLE_KEY, configuration);
            }

        } else if (("DOMAIN".equals(type))) {
            // create a domain based throttle context and configuration
            configuration = throttle.getThrottleConfiguration(
                    ThrottleConstants.DOMAIN_BASED_THROTTLE_KEY);
            if (configuration == null) {
                configuration =
                        ThrottleConfigurationFactory.
                                createThrottleConfiguration(
                                        ThrottleConstants.DOMAIN_BASE);
                throttle.addThrottleContext(
                        ThrottleConstants.DOMAIN_BASED_THROTTLE_KEY,
                        ThrottleContextFactory.createThrottleContext(
                                ThrottleConstants.DOMAIN_BASE, configuration));
                throttle.addThrottleConfiguration(
                        ThrottleConstants.DOMAIN_BASED_THROTTLE_KEY, configuration);
            }
            //create a caller context for domain based throttle

        } else if (("ROLE".equals(type))) {
            // create a domain based throttle context and configuration
            configuration = throttle.getThrottleConfiguration(
                    ThrottleConstants.ROLE_BASED_THROTTLE_KEY);
            if (configuration == null) {
                configuration =
                        ThrottleConfigurationFactory.
                                createThrottleConfiguration(
                                        ThrottleConstants.ROLE_BASE);
                throttle.addThrottleContext(
                        ThrottleConstants.ROLE_BASED_THROTTLE_KEY,
                        ThrottleContextFactory.createThrottleContext(
                                ThrottleConstants.ROLE_BASE, configuration));
                throttle.addThrottleConfiguration(
                        ThrottleConstants.ROLE_BASED_THROTTLE_KEY, configuration);
            }
            //create a caller context for domain based throttle

        } else {
            handleException("Unsupported throttle type : " + type);
        }

        return configuration;
    }

    /**
     * Fills the caller configuration information based on given policy
     *
     * @param policy              Policy instance
     * @param callerConfiguration Caller configuration instance
     * @throws ThrottleException
     */
    private static void fillCallerConfiguration(Policy policy,
                                CallerConfiguration callerConfiguration) throws ThrottleException {
        List list = policy.getPolicyComponents();
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            Object object = iterator.next();

            XmlPrimtiveAssertion primitiveAssertion = (XmlPrimtiveAssertion) object;
            OMElement element = primitiveAssertion.getValue();
            // Name of the policy assertion
            String name = element.getLocalName();
            if (name.equals(
                    ThrottleConstants.ALLOW_PARAMETER_NAME)) {

                callerConfiguration.setAccessState(
                        ThrottleConstants.ACCESS_ALLOWED);

            } else if (name.equals(
                    ThrottleConstants.DENY_PARAMETER_NAME)) {

                callerConfiguration.setAccessState(
                        ThrottleConstants.ACCESS_DENIED);

            } else if (name.equals(
                    ThrottleConstants.CONTROL_PARAMETER_NAME)) {

                callerConfiguration.setAccessState(
                        ThrottleConstants.ACCESS_CONTROLLED);
                OMElement controlElement = primitiveAssertion.getValue();
                if (controlElement == null) {
                    handleException("Invalid throttle configuration - " +
                            "Control assertion cannot be empty");
                }

                Policy controlPolicy = PolicyEngine.getPolicy(controlElement);
                if (controlPolicy != null) {
                    fillControlConfiguration(controlPolicy, callerConfiguration);
                } else {
                    handleException("Invalid throttle configuration - " +
                            "Cannot create a policy object(Control Assertion ) " +
                            "form given policy file ");
                }

            } else {
                handleException("Invalid Throttle" +
                        " Policy configuration");
            }
        }
    }

    /**
     * Helper method to process control assertion
     *
     * @param policy              Policy for Control Assertion
     * @param callerConfiguration Caller to whom control need to be applied.
     * @throws ThrottleException
     */
    private static void fillControlConfiguration(Policy policy,
                                                 CallerConfiguration callerConfiguration)
            throws ThrottleException {
        boolean isFoundMaxCount = false;
        boolean isFoundUnitTime = false;
        List list = policy.getPolicyComponents();
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            Object obj = iterator.next();
            if (obj instanceof Policy) {
                List controlList = ((Policy) obj).getPolicyComponents();
                for (Iterator controlIterator = controlList.iterator();
                                                     controlIterator.hasNext(); ) {
                    Object object = controlIterator.next();
                    if (object instanceof XmlPrimtiveAssertion) {
                        XmlPrimtiveAssertion primitiveAssertion =
                                (XmlPrimtiveAssertion) object;
                        OMElement element = primitiveAssertion.getValue();
                        // Name of the policy assertion
                        String name = element.getLocalName();
                        //Value of the policy assertion
                        String value = element.getText();

                        //if Value and Name  are null,then it is a
                        // invalid policy configuration
                        if (name == null || value == null) {
                            handleException("Either Value or " +
                                    "Name of the policy cannot be null");
                        }
                        if (!value.equals("")) {
                            String resolvedValue = ResolverFactory.getInstance().getResolver(value.trim()).resolve();

                            if (name.equals(
                                    ThrottleConstants.
                                            MAXIMUM_COUNT_PARAMETER_NAME)) {

                                isFoundMaxCount = true;
                                try {
                                    callerConfiguration.setMaximumRequestPerUnitTime(
                                            Integer.parseInt(resolvedValue));
                                } catch (NumberFormatException ignored) {
                                    log.error("Error occurred - " +
                                            "Invalid number for maximum " +
                                            "request number ", ignored);
                                    if (log.isDebugEnabled()) {
                                        log.debug("Access" +
                                                " will be fully allowed");
                                    }
                                    callerConfiguration.setAccessState(
                                            ThrottleConstants.ACCESS_ALLOWED);
                                }
                            } else if (name.equals(
                                    ThrottleConstants.
                                            UNIT_TIME_PARAMETER_NAME)) {
                                //TODO need to verify that value is in milisecond
                                long timeInMilliSec = 0;
                                try {
                                    timeInMilliSec =
                                            Long.parseLong(resolvedValue);
                                } catch (NumberFormatException ignored) {
                                    log.error("Error occurred " +
                                                    "- Invalid number for unit time",
                                            ignored);
                                }
                                if (timeInMilliSec == 0) {
                                    handleException("Unit Time cannot " +
                                            "find - invalid throttle " +
                                            "policy configuration");
                                }
                                isFoundUnitTime = true;
                                callerConfiguration.setUnitTime(timeInMilliSec);

                            } else if (name.equals(
                                    ThrottleConstants.
                                            PROHIBIT_TIME_PERIOD_PARAMETER_NAME)) {
                                try {
                                    callerConfiguration.setProhibitTimePeriod(
                                            Long.parseLong(resolvedValue));
                                } catch (NumberFormatException ignored) {
                                    log.error("Error occurred - Invalid" +
                                                    " number for prohibit time ",
                                            ignored);
                                }
                            } else {
                                handleException("Undefined Policy" +
                                        " property for Throttle Policy");
                            }
                        } else {
                            if (!name.equals(
                                    ThrottleConstants.
                                            PROHIBIT_TIME_PERIOD_PARAMETER_NAME)) {
                                handleException("The policy which have " +
                                        " defined as optional " +
                                        "should have value ");
                            }
                        }
                    }
                }
            } else {
                handleException("Invalid policy - " +
                        "Control Assertion must contain a wsp:Policy as child ");
            }

        }
        if (!isFoundUnitTime && !isFoundMaxCount) {
            handleException("Maximum Count and UnitTime are " +
                    "Mandatory in Throttle Policy ");

        }
    }

    /**
     * Helper method to handle exception
     *
     * @param message Debug message
     * @throws ThrottleException
     */
    private static void handleException(String message) throws ThrottleException {
        String msg = "Error was occurred during throttle policy processing :  " + message;
        log.error(msg);
        throw new ThrottleException(msg);
    }
}
