/*
 * Copyright (c) 2005 - 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * 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.wso2.carbon.event.input.adapter.http;

import org.apache.axiom.om.util.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.event.input.adapter.core.InputEventAdapterListener;
import org.wso2.carbon.event.input.adapter.http.internal.ds.HTTPEventAdapterServiceValueHolder;
import org.wso2.carbon.event.input.adapter.http.internal.util.HTTPEventAdapterConstants;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class HTTPMessageServlet extends HttpServlet {

    private static final String AUTHORIZATION_HEADER = "Authorization";

    private static final String AUTH_MESSAGE_STORE_TENANT_ID = "AUTH_MESSAGE_STORE_TENANT_ID";

    private static final String AUTH_FAILURE_RESPONSE = "_AUTH_FAILURE_";

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

    private InputEventAdapterListener eventAdaptorListener;
    private int tenantId;
    private String exposedTransports;
    private boolean isBasicAuthEnabled;

    public HTTPMessageServlet(InputEventAdapterListener eventAdaptorListener, int tenantId, String exposedTransports, boolean isBasicAuthEnabled) {
        this.eventAdaptorListener = eventAdaptorListener;
        this.tenantId = tenantId;
        this.exposedTransports = exposedTransports;
        this.isBasicAuthEnabled = isBasicAuthEnabled;
    }

    private String[] getUserPassword(HttpServletRequest req) {
        String authHeader = req.getHeader(AUTHORIZATION_HEADER);
        if (authHeader == null) {
            return null;
        }
        if (!authHeader.startsWith("Basic ")) {
            return null;
        }
        String[] userPassword = new String(Base64.decode(authHeader.substring(6))).split(":");
        if (userPassword.length != 2) {
            return null;
        }
        return userPassword;
    }

    private int checkAuthentication(HttpServletRequest req) {
        Object tidObj = req.getSession().getAttribute(AUTH_MESSAGE_STORE_TENANT_ID);
        if (tidObj != null) {
            return (Integer) tidObj;
        }
        String[] userPassword = this.getUserPassword(req);
        if (userPassword == null) {
            return -1;
        }
        String username = userPassword[0];
        String password = userPassword[1];

        String tenantDomain = MultitenantUtils.getTenantDomain(username);
        String tenantAwareUserName = MultitenantUtils.getTenantAwareUsername(username);
        username = (tenantAwareUserName + "@" + tenantDomain).toLowerCase();
        RealmService realmService = HTTPEventAdapterServiceValueHolder.getRealmService();
        int tenantId;
        try {
            tenantId = realmService.getTenantManager().getTenantId(tenantDomain);
            if (tenantId == -1) {
                return -1;
            }
            UserStoreManager usm = realmService.getTenantUserRealm(tenantId).getUserStoreManager();
            boolean success = usm.authenticate(tenantAwareUserName, password);
            if (success) {
                req.getSession().setAttribute(AUTH_MESSAGE_STORE_TENANT_ID, tenantId);
                return tenantId;
            } else {
                return -1;
            }
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("checkAuthentication() fail: " + e.getMessage(), e);
            }
            return -1;
        }
    }


    private String inputStreamToString(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buff = new byte[1024];
        int i;
        while ((i = in.read(buff)) > 0) {
            out.write(buff, 0, i);
        }
        out.close();
        return out.toString();
    }

    @Override
    protected void doPost(HttpServletRequest req,
                          HttpServletResponse res) throws IOException {

        String data = this.inputStreamToString(req.getInputStream());
        if (data == null) {
            log.warn("Event Object is empty/null");
            return;
        }

        if(exposedTransports.equalsIgnoreCase(HTTPEventAdapterConstants.HTTPS)){
            if(! req.isSecure()){
                res.setStatus(403);
                log.error("Only Secured endpoint is enabled for requests");
                return;
            }else {
                if(isBasicAuthEnabled){
                    int tenantId = this.checkAuthentication(req);
                    if (tenantId == -1) {
                        res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes());
                        res.setStatus(401);
                        log.error("Authentication failed for the request");
                        return;
                    } else if (tenantId != this.tenantId) {
                        res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes());
                        res.setStatus(401);
                        log.error("Authentication failed for the request");
                        return;
                    }
                }
            }
        }else if(exposedTransports.equalsIgnoreCase(HTTPEventAdapterConstants.HTTP)){
            if(req.isSecure()){
                res.setStatus(403);
                log.error("Only unsecured endpoint is enabled for requests");
                return;
            }
        }else {
            if(req.isSecure()){
                if(isBasicAuthEnabled){
                    int tenantId = this.checkAuthentication(req);
                    if (tenantId == -1) {
                        res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes());
                        res.setStatus(401);
                        log.error("Authentication failed for the request");
                        return;
                    } else if (tenantId != this.tenantId) {
                        res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes());
                        res.setStatus(401);
                        log.error("Authentication failed for the request");
                        return;
                    }
                }
            }

        }

        if (log.isDebugEnabled()) {
            log.debug("Message : " + data);
        }
        HTTPEventAdapter.executorService.submit(new HTTPRequestProcessor(eventAdaptorListener, data, tenantId));

    }

    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse res) throws IOException {
        doPost(req,res);
    }

    public class HTTPRequestProcessor implements Runnable {

        private InputEventAdapterListener inputEventAdapterListener;
        private String payload;
        private int tenantId;

        public HTTPRequestProcessor(InputEventAdapterListener inputEventAdapterListener,
                                    String payload, int tenantId) {
            this.inputEventAdapterListener = inputEventAdapterListener;
            this.payload = payload;
            this.tenantId = tenantId;
        }

        public void run() {
            try {
                PrivilegedCarbonContext.startTenantFlow();
                PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(tenantId);

                if (log.isDebugEnabled()) {
                    log.debug("Event received in HTTP Event Adapter - " + payload);
                }

                if (payload.trim() != null) {
                    inputEventAdapterListener.onEvent(payload);
                } else {
                    log.warn("Dropping the empty/null event received through http adapter");
                }
            } catch (Exception e) {
                log.error("Error while parsing http request for processing: " + e.getMessage(), e);
            } finally {
                PrivilegedCarbonContext.endTenantFlow();
            }
        }

    }

}
