/*
 * Copyright (c) 2005-2026, WSO2 LLC. (http://www.wso2.com).
 *
 * WSO2 LLC. 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.event.output.adapter.core.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterConfiguration;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterFactory;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterSchema;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterService;
import org.wso2.carbon.event.output.adapter.core.Property;
import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterException;
import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterRuntimeException;
import org.wso2.carbon.event.output.adapter.core.exception.TestConnectionNotSupportedException;
import org.wso2.carbon.event.output.adapter.core.internal.ds.OutputEventAdapterServiceValueHolder;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.wso2.carbon.event.output.adapter.core.internal.EventAdapterConstants.ADAPTER_EMAIL_AUTH_TYPE;
import static org.wso2.carbon.event.output.adapter.core.internal.EventAdapterConstants.ADAPTER_EMAIL_SMTP_USER;
import static org.wso2.carbon.event.output.adapter.core.internal.EventAdapterConstants.BASIC;

/**
 * EventAdapter service implementation.
 */
@org.osgi.annotation.bundle.Capability(
        namespace = "osgi.service",
        attribute = {
                "objectClass=org.wso2.carbon.event.output.adapter.core.OutputEventAdapterService",
                "service.scope=singleton"
        }
)
public class CarbonOutputEventAdapterService implements OutputEventAdapterService {

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

    private final Map<String, OutputEventAdapterFactory> eventAdapterFactoryMap;
    private final ConcurrentHashMap<Integer, ConcurrentHashMap<String, OutputAdapterRuntime>> tenantSpecificEventAdapters;


    public CarbonOutputEventAdapterService() {
        this.eventAdapterFactoryMap = new ConcurrentHashMap<String, OutputEventAdapterFactory>();
        this.tenantSpecificEventAdapters = new ConcurrentHashMap<Integer, ConcurrentHashMap<String, OutputAdapterRuntime>>();
    }

    public void registerEventAdapterFactory(OutputEventAdapterFactory outputEventAdapterFactory) {
        OutputEventAdapterSchema outputEventAdapterSchema = outputEventAdapterFactory.getOutputEventAdapterSchema();
        this.eventAdapterFactoryMap.put(outputEventAdapterSchema.getType(), outputEventAdapterFactory);
    }

    public void unRegisterEventAdapterFactory(OutputEventAdapterFactory outputEventAdapterFactory) {
        OutputEventAdapterSchema outputEventAdapterSchema = outputEventAdapterFactory.getOutputEventAdapterSchema();
        this.eventAdapterFactoryMap.remove(outputEventAdapterSchema.getType());
    }


    @Override
    public List<String> getOutputEventAdapterTypes() {
        return new ArrayList<String>(eventAdapterFactoryMap.keySet());
    }

    /**
     * This method returns the event adapter dto for a specific event adapter type
     *
     * @param eventAdapterType
     * @return
     */
    @Override
    public OutputEventAdapterSchema getOutputEventAdapterSchema(String eventAdapterType) {
        OutputEventAdapterFactory outputEventAdapterFactory = eventAdapterFactoryMap.get(eventAdapterType);
        if (outputEventAdapterFactory != null) {
            return outputEventAdapterFactory.getOutputEventAdapterSchema();
        }
        return null;
    }

    @Override
    public void create(OutputEventAdapterConfiguration outputEventAdapterConfiguration) throws OutputEventAdapterException {
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        ConcurrentHashMap<String, OutputAdapterRuntime> eventAdapters = tenantSpecificEventAdapters.get(tenantId);
        if (eventAdapters == null) {
            tenantSpecificEventAdapters.putIfAbsent(tenantId, new ConcurrentHashMap<String, OutputAdapterRuntime>());
            eventAdapters = tenantSpecificEventAdapters.get(tenantId);
        }
        OutputEventAdapterFactory adapterFactory = eventAdapterFactoryMap.get(outputEventAdapterConfiguration.getType());
        if (adapterFactory == null) {
            throw new OutputEventAdapterException("Output Event Adapter not created as no adapter factory is registered " +
                    "with type " + outputEventAdapterConfiguration.getType());
        }
        if (outputEventAdapterConfiguration.getName() == null) {
            throw new OutputEventAdapterException("Output Event Adapter name cannot by null, for the adapter type " +
                    outputEventAdapterConfiguration.getType());
        }
        if (eventAdapters.get(outputEventAdapterConfiguration.getName()) != null) {
            throw new OutputEventAdapterException("Output Event Adapter not created as another adapter with same name '"
                    + outputEventAdapterConfiguration.getName() + "' already exist for tenant " + tenantId);
        }
        //check if all the required properties are given here
        List<Property> staticPropertyList = adapterFactory.getStaticPropertyList();
        if (staticPropertyList != null) {
            Map<String, String> staticPropertyMap = outputEventAdapterConfiguration.getStaticProperties();
            for (Property property : staticPropertyList) {
                if (property.isRequired()) {
                    if (staticPropertyMap == null) {
                        throw new OutputEventAdapterException("Output Event Adapter not created as the 'staticProperties' are null, " +
                                "which means, the required property " + property.getPropertyName() + " is not  being set, for the adapter type " +
                                outputEventAdapterConfiguration.getType());
                    }
                    if (staticPropertyMap.get(property.getPropertyName()) == null) {
                        throw new OutputEventAdapterException("Output Event Adapter not created as the required property: " + property.getPropertyName() +
                                " is not set, for the adapter type " + outputEventAdapterConfiguration.getType());
                    }
                }
            }
        }
        Map<String, String> globalProperties = OutputEventAdapterServiceValueHolder.getGlobalAdapterConfigs().
                getAdapterConfig(outputEventAdapterConfiguration.getType()).getGlobalPropertiesAsMap();
        if (outputEventAdapterConfiguration.getStaticProperties() != null) {
            overrideGlobalPropertiesWithTenantProperties(globalProperties,
                    outputEventAdapterConfiguration.getStaticProperties());
        }
        eventAdapters.put(outputEventAdapterConfiguration.getName(), new OutputAdapterRuntime(adapterFactory.
                createEventAdapter(outputEventAdapterConfiguration, globalProperties), outputEventAdapterConfiguration.getName()));
    }

    /**
     * publishes the message using the given event adapter to the given topic.
     *
     * @param name              - name of the event adapter
     * @param dynamicProperties
     * @param message
     */
    @Override
    public void publish(String name, Map<String, String> dynamicProperties, Object message) {
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        getRuntime(name, tenantId).publish(message, dynamicProperties);
    }

    @Override
    public void publishSync(String name, Map<String, String> dynamicProperties, Object message)
            throws OutputEventAdapterException {

        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        getRuntime(name, tenantId).publishSync(message, dynamicProperties);
    }

    @Override
    public boolean isSync(String name, Map<String, String> dynamicProperties) {

        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        return getRuntime(name, tenantId).isSync(dynamicProperties);
    }

    private OutputAdapterRuntime getRuntime(String name, int tenantId) {
        
        ConcurrentHashMap<String, OutputAdapterRuntime> eventAdapters = tenantSpecificEventAdapters.get(tenantId);
        if (eventAdapters == null) {
            throw new OutputEventAdapterRuntimeException("Event not published as no Output Event Adapter found for" +
                    " tenant id " + tenantId);
        }
        OutputAdapterRuntime outputAdapterRuntime = eventAdapters.get(name);
        if (outputAdapterRuntime == null) {
            throw new OutputEventAdapterRuntimeException("Event not published as no Output Event Adapter found with name" +
                    " '" + name + "' for tenant id " + tenantId);
        }
        return outputAdapterRuntime;
    }

    /**
     * publish testConnect message using the given event adapter.
     *
     * @param outputEventAdapterConfiguration - Configuration Details of the event adapter
     */
    @Override
    public void testConnection(OutputEventAdapterConfiguration outputEventAdapterConfiguration)
            throws OutputEventAdapterException, TestConnectionNotSupportedException {
        OutputEventAdapter outputEventAdapter = null;
        try {
            OutputEventAdapterFactory outputEventAdapterFactory = this.eventAdapterFactoryMap.get(outputEventAdapterConfiguration.getType());
            OutputEventAdapterFactory adapterFactory = eventAdapterFactoryMap.get(outputEventAdapterConfiguration.getType());
            if (adapterFactory == null) {
                throw new OutputEventAdapterException("Output Event Adapter not created as no adapter factory is " +
                        "registered with type " + outputEventAdapterConfiguration.getType());
            }
            if (outputEventAdapterConfiguration.getName() == null) {
                throw new OutputEventAdapterException("Output Event Adapter name cannot by null, for the adapter type "
                        + outputEventAdapterConfiguration.getType());
            }
            Map<String, String> globalProperties = OutputEventAdapterServiceValueHolder.getGlobalAdapterConfigs().
                    getAdapterConfig(outputEventAdapterConfiguration.getType()).getGlobalPropertiesAsMap();
            outputEventAdapter = outputEventAdapterFactory.createEventAdapter(outputEventAdapterConfiguration, globalProperties);
            outputEventAdapter.init();
            outputEventAdapter.testConnect();
            outputEventAdapter.disconnect();
            outputEventAdapter.destroy();
        } finally {
            if (outputEventAdapter != null) {
                outputEventAdapter.destroy();
            }
        }
    }

    @Override
    public void destroy(String name) {
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        ConcurrentHashMap<String, OutputAdapterRuntime> eventAdapters = tenantSpecificEventAdapters.get(tenantId);
        if (eventAdapters == null) {
            return;
        }
        OutputAdapterRuntime outputAdapterRuntime = eventAdapters.remove(name);
        if (outputAdapterRuntime != null) {
            outputAdapterRuntime.destroy();
        }
    }

    @Override
    public boolean isPolled(String outputAdapterName) throws OutputEventAdapterException {
        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        ConcurrentHashMap<String, OutputAdapterRuntime> inputRuntimeMap = tenantSpecificEventAdapters.get(tenantId);
        if (inputRuntimeMap != null) {
            OutputAdapterRuntime outputAdapterRuntime = inputRuntimeMap.get(outputAdapterName);
            if (outputAdapterRuntime != null) {
                return outputAdapterRuntime.isPolled();
            }
        }
        throw new OutputEventAdapterException("Adopter with name'" + outputAdapterName + "' not found");
    }

    /**
     * This method override the global properties if tenant properties are present.
     *
     * @param globalProps global properties from output-event-adapters.xml.
     * @param tenantProps tenant wise properties configured in tenant wise Publishers.
     * @param tenantProps
     */
    private void overrideGlobalPropertiesWithTenantProperties(Map<String, String> globalProps,
                                                              Map<String, String> tenantProps) {

        for (String key : tenantProps.keySet()) {
            if (globalProps.containsKey(key) && tenantProps.get(key) != null) {
                globalProps.put(key, tenantProps.get(key));
            }
            // If the email provider is set with notification sender v1 API, auth type will be null. In case global
            // properties are set with CLIENT_CREDENTIAL grant type, we need to set the auth type as BASIC.
            if (ADAPTER_EMAIL_AUTH_TYPE.equals(key) && tenantProps.get(ADAPTER_EMAIL_SMTP_USER) != null) {
                globalProps.put(ADAPTER_EMAIL_AUTH_TYPE, BASIC);
            }
        }
    }

}
