/*
 *   Copyright (c) 2017, 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.device.mgt.jaxrs.service.impl.admin;

import org.apache.axis2.AxisFault;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.Stub;
import org.apache.axis2.java.security.SSLProtocolSocketFactory;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.wso2.carbon.application.mgt.stub.upload.CarbonAppUploaderStub;
import org.wso2.carbon.application.mgt.stub.upload.types.carbon.UploadedFileItem;
import org.wso2.carbon.base.ServerConfiguration;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.core.util.Utils;
import org.wso2.carbon.device.mgt.jaxrs.service.api.admin.DeviceAnalyticsArtifactUploaderAdminService;
import org.wso2.carbon.device.mgt.jaxrs.util.DeviceMgtAPIUtils;
import org.wso2.carbon.identity.jwt.client.extension.JWTClient;
import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.utils.CarbonUtils;
import org.wso2.carbon.event.receiver.stub.EventReceiverAdminServiceStub;
import org.wso2.carbon.event.stream.stub.EventStreamAdminServiceStub;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;

import javax.activation.DataHandler;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import java.io.*;
import java.nio.file.Files;
import java.rmi.RemoteException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;

@Path("/admin/publish-artifact")
public class DeviceAnalyticsArtifactUploaderAdminServiceImpl implements DeviceAnalyticsArtifactUploaderAdminService {

    /**
     * required soap header for authorization
     */
    private static final String AUTHORIZATION_HEADER = "Authorization";

    /**
     * required soap header value for mutualSSL
     */
    private static final String AUTHORIZATION_HEADER_VALUE = "Bearer";

    private static final String KEY_STORE_TYPE = "JKS";
    /**
     * Default truststore type of the client
     */
    private static final String TRUST_STORE_TYPE = "JKS";
    /**
     * Default keymanager type of the client
     */
    private static final String KEY_MANAGER_TYPE = "SunX509"; //Default Key Manager Type
    /**
     * Default trustmanager type of the client
     */
    private static final String TRUST_MANAGER_TYPE = "SunX509"; //Default Trust Manager Type

    private static final String SSLV3 = "SSLv3";



    private KeyStore keyStore;
    private KeyStore trustStore;
    private char[] keyStorePassword;
    private SSLContext sslContext;

    private String tenantDomain;

    private static final Log log = LogFactory.getLog(DeviceAnalyticsArtifactUploaderAdminServiceImpl.class);
    private static final String DEFAULT_RESOURCE_LOCATION = "/resources/devicetypes";
    private static final String CAR_FILE_LOCATION = CarbonUtils.getCarbonHome() + File.separator + "repository" +
    File.separator + "resources" + File.separator + "devicetypes";
    private static final String DAS_PORT = "${iot.analytics.https.port}";
    private static final String DAS_HOST_NAME = "${iot.analytics.host}";
    private static final String DEFAULT_HTTP_PROTOCOL = "https";
    private static final String IOT_MGT_PORT = "${iot.manager.https.port}";
    private static final String IOT_MGT_HOST_NAME = "${iot.manager.host}";
    private static final String DAS_URL = DEFAULT_HTTP_PROTOCOL + "://" + DAS_HOST_NAME
            + ":" + DAS_PORT + "/services/CarbonAppUploader/";
    private static final String DAS_EVENT_RECEIVER_EP = DEFAULT_HTTP_PROTOCOL + "://" + DAS_HOST_NAME
            + ":" + DAS_PORT + "/services/EventReceiverAdminService/";
    private static final String DAS_EVENT_STREAM_EP = DEFAULT_HTTP_PROTOCOL + "://" + DAS_HOST_NAME
            + ":" + DAS_PORT + "/services/EventStreamAdminService/";

    private static final String IOT_MGT_URL = DEFAULT_HTTP_PROTOCOL + "://" + IOT_MGT_HOST_NAME
            + ":" + IOT_MGT_PORT + "/services/CarbonAppUploader/";
    private static final String MEDIA_TYPE_XML = "application/xml";
    private static final String DEVICE_MANAGEMENT_TYPE = "device_management";
    private static final String TENANT_DOMAIN_PROPERTY = "\\$\\{tenant-domain\\}";


    @Override
    @POST
    @Path("/deploy/{type}")
    public Response doPublish(@PathParam("type") String type) {
        try {
            //Getting the tenant Domain
            tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
            String username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername();
            String tenantAdminUser = username + "@" + tenantDomain;

            String keyStorePassword = ServerConfiguration.getInstance().getFirstProperty("Security.KeyStore.Password");
            String trustStorePassword = ServerConfiguration.getInstance().getFirstProperty(
                    "Security.TrustStore.Password");
            String keyStoreLocation = ServerConfiguration.getInstance().getFirstProperty("Security.KeyStore.Location");
            String trustStoreLocation = ServerConfiguration.getInstance().getFirstProperty(
                    "Security.TrustStore.Location");

            //Call to load the keystore.
            loadKeyStore(keyStoreLocation, keyStorePassword);
            //Call to load the TrustStore.
            loadTrustStore(trustStoreLocation, trustStorePassword);
            //Create the SSL context with the loaded TrustStore/keystore.
            initSSLConnection();
            JWTClient jwtClient = DeviceMgtAPIUtils.getJWTClientManagerService().getJWTClient();

            String authValue =  AUTHORIZATION_HEADER_VALUE + " " + new String(Base64.encodeBase64(
                    jwtClient.getJwtToken(tenantAdminUser).getBytes()));

            List<Header> list = new ArrayList<>();
            Header httpHeader = new Header();
            httpHeader.setName(AUTHORIZATION_HEADER);
            httpHeader.setValue(authValue);
            list.add(httpHeader);//"https"

            List<String> streamFileList = getStreamsList(type);
            List<String> receiverFileList = getReceiversList(type);

            if (!tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) {
                if (streamFileList != null) {
                    publishDynamicEventStream(type, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME, streamFileList);
                }
                if (receiverFileList != null) {
                    publishDynamicEventReceivers(type, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME, receiverFileList);
                }
            }
            if (streamFileList != null) {
                publishDynamicEventStream(type, tenantDomain, streamFileList);
            }
            if (deployAnalyticsCapp(type, list)){
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity("\"Error, Artifact does not exist.\"").build();
            }
            if (receiverFileList != null) {
                publishDynamicEventReceivers(type, tenantDomain, receiverFileList);
            }
            return Response.status(Response.Status.CREATED).entity("\"OK. \\n Successfully uploaded the artifacts.\"")
                    .build();
        } catch (AxisFault e) {
            log.error("failed to publish event definitions for tenantDomain:" + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (RemoteException e) {
            log.error("Failed to connect with the remote services:" + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (JWTClientException e) {
            log.error("Failed to generate jwt token for tenantDomain:" + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (UserStoreException e) {
            log.error("Failed to connect with the user store, tenantDomain: " + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (CertificateException | UnrecoverableKeyException | KeyStoreException |
                KeyManagementException | IOException | NoSuchAlgorithmException e) {
            log.error("Failed to access keystore for, tenantDomain: " + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (RegistryException e) {
            log.error("Failed to load tenant, tenantDomain: " + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        } catch (ParseException e) {
            log.error("Invalid stream definition for device type" + type + " for tenant, tenantDomain: " + tenantDomain, e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }
    }

    private boolean deployAnalyticsCapp(@PathParam("type") String type, List<Header> list) throws IOException, RegistryException {
        CarbonAppUploaderStub carbonAppUploaderStub = null;
        try {
            File directory = new File(CAR_FILE_LOCATION + File.separator + type);
            if (directory.isDirectory() && directory.exists()) {
                UploadedFileItem[] uploadedFileItems = loadCappFromFileSystem(type);
                if (uploadedFileItems.length > 0) {
                    if (DEVICE_MANAGEMENT_TYPE.equals(type.toLowerCase())) {
                        carbonAppUploaderStub = new CarbonAppUploaderStub(Utils.replaceSystemProperty(
                                IOT_MGT_URL));
                        Options appUploaderOptions = carbonAppUploaderStub._getServiceClient().getOptions();
                        if (appUploaderOptions == null) {
                            appUploaderOptions = new Options();
                        }
                        appUploaderOptions.setProperty(HTTPConstants.HTTP_HEADERS, list);
                        appUploaderOptions.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER
                                , new Protocol(DEFAULT_HTTP_PROTOCOL,
                                               (ProtocolSocketFactory) new SSLProtocolSocketFactory
                                                       (sslContext), Integer.parseInt(Utils.replaceSystemProperty(
                                IOT_MGT_PORT))));

                        carbonAppUploaderStub._getServiceClient().setOptions(appUploaderOptions);
                        carbonAppUploaderStub.uploadApp(uploadedFileItems);
                    } else {
                        carbonAppUploaderStub = new CarbonAppUploaderStub(Utils.replaceSystemProperty(DAS_URL));
                        Options appUploaderOptions = carbonAppUploaderStub._getServiceClient().getOptions();
                        if (appUploaderOptions == null) {
                            appUploaderOptions = new Options();
                        }
                        appUploaderOptions.setProperty(HTTPConstants.HTTP_HEADERS, list);
                        appUploaderOptions.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER
                                , new Protocol(DEFAULT_HTTP_PROTOCOL
                                , (ProtocolSocketFactory) new SSLProtocolSocketFactory(sslContext)
                                , Integer.parseInt(Utils.replaceSystemProperty(DAS_PORT))));

                        carbonAppUploaderStub._getServiceClient().setOptions(appUploaderOptions);
                        carbonAppUploaderStub.uploadApp(uploadedFileItems);
                    }
                }
            } else {
                return true;
            }
            return false;
        } finally {
            cleanup(carbonAppUploaderStub);
        }
    }

    private void publishDynamicEventReceivers(String deviceType, String tenantDomain, List<String> receiversList)
            throws IOException, UserStoreException, JWTClientException {

        PrivilegedCarbonContext.startTenantFlow();
        PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tenantDomain, true);
        EventReceiverAdminServiceStub receiverAdminServiceStub = null;
        try {
            receiverAdminServiceStub = new EventReceiverAdminServiceStub
                    (Utils.replaceSystemProperty(DAS_EVENT_RECEIVER_EP));
            Options eventReciverOptions = receiverAdminServiceStub._getServiceClient().getOptions();
            if (eventReciverOptions == null) {
                eventReciverOptions = new Options();
            }
            String username;
            if(!tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) {
               username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()
                        .getRealmConfiguration().getAdminUserName()+"@"+tenantDomain;
            }else {
                username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()
                        .getRealmConfiguration().getAdminUserName();
            }


            JWTClient jwtClient = DeviceMgtAPIUtils.getJWTClientManagerService().getJWTClient();

            String authValue =  AUTHORIZATION_HEADER_VALUE + " " + new String(Base64.encodeBase64(
                    jwtClient.getJwtToken(username).getBytes()));

            List<Header> list = new ArrayList<>();
            Header httpHeader = new Header();
            httpHeader.setName(AUTHORIZATION_HEADER);
            httpHeader.setValue(authValue);
            list.add(httpHeader);//"https"

            eventReciverOptions.setProperty(HTTPConstants.HTTP_HEADERS, list);
            eventReciverOptions.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER
                    , new Protocol(DEFAULT_HTTP_PROTOCOL
                    , (ProtocolSocketFactory) new SSLProtocolSocketFactory(sslContext)
                    , Integer.parseInt(Utils.replaceSystemProperty(DAS_PORT))));

            receiverAdminServiceStub._getServiceClient().setOptions(eventReciverOptions);
            for (String receiverContent:receiversList) {
                receiverAdminServiceStub.deployEventReceiverConfiguration(receiverContent);
            }
        } finally {
            cleanup(receiverAdminServiceStub);
            PrivilegedCarbonContext.endTenantFlow();
        }
    }

    private void publishDynamicEventStream(String deviceType, String tenantDomain, List<String> streamList)
            throws IOException, UserStoreException, JWTClientException, ParseException {

        PrivilegedCarbonContext.startTenantFlow();
        PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tenantDomain, true);
        EventStreamAdminServiceStub eventStreamAdminServiceStub = null;
        try {
            eventStreamAdminServiceStub = new EventStreamAdminServiceStub
                    (Utils.replaceSystemProperty(DAS_EVENT_STREAM_EP));
            Options eventReciverOptions = eventStreamAdminServiceStub._getServiceClient().getOptions();
            if (eventReciverOptions == null) {
                eventReciverOptions = new Options();
            }
            String username;
            if(!tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) {
                username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()
                        .getRealmConfiguration().getAdminUserName()+"@"+tenantDomain;
            }else {
                username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()
                        .getRealmConfiguration().getAdminUserName();
            }


            JWTClient jwtClient = DeviceMgtAPIUtils.getJWTClientManagerService().getJWTClient();

            String authValue =  AUTHORIZATION_HEADER_VALUE + " " + new String(Base64.encodeBase64(
                    jwtClient.getJwtToken(username).getBytes()));

            List<Header> list = new ArrayList<>();
            Header httpHeader = new Header();
            httpHeader.setName(AUTHORIZATION_HEADER);
            httpHeader.setValue(authValue);
            list.add(httpHeader);//"https"

            eventReciverOptions.setProperty(HTTPConstants.HTTP_HEADERS, list);
            eventReciverOptions.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER
                    , new Protocol(DEFAULT_HTTP_PROTOCOL
                    , (ProtocolSocketFactory) new SSLProtocolSocketFactory(sslContext)
                    , Integer.parseInt(Utils.replaceSystemProperty(DAS_PORT))));

            eventStreamAdminServiceStub._getServiceClient().setOptions(eventReciverOptions);
            for (String streamContent:streamList) {
                JSONParser jsonParser = new JSONParser();
                JSONObject steamJson = (JSONObject)jsonParser.parse(streamContent);
                String name = (String) steamJson.get("name");
                String version = (String) steamJson.get("version");
                String streamId = name +":"+version;
                if (eventStreamAdminServiceStub.getStreamDefinitionDto(streamId) == null) {
                    eventStreamAdminServiceStub.addEventStreamDefinitionAsString(streamContent);
                }
            }
        } finally {
            cleanup(eventStreamAdminServiceStub);
            PrivilegedCarbonContext.endTenantFlow();
        }
    }


    private List<String> getReceiversList(String deviceType) throws IOException {
        File directory = new File(CAR_FILE_LOCATION + File.separator + deviceType+File.separator+"receiver");
        if (!directory.exists()) {
            return null;
        }
        File[] receiverFiles = directory.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".xml");
            }
        });
       List<String> receiverList = new ArrayList<>();
        for (File receiverFile:receiverFiles) {
            String receiverContentTemplate =new String(Files.readAllBytes(receiverFile.toPath()));
            final String receiverContent = receiverContentTemplate.replaceAll(TENANT_DOMAIN_PROPERTY, tenantDomain.toLowerCase());
            receiverList.add(receiverContent);
        }

        return receiverList;
    }

    private List<String> getStreamsList(String deviceType) throws IOException {
        File directory = new File(CAR_FILE_LOCATION + File.separator + deviceType+File.separator+"streams");
        if (!directory.exists()) {
            return null;
        }
        File[] receiverFiles = directory.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".json");
            }
        });
        List<String> streamList = new ArrayList<>();
        for (File StreamFile:receiverFiles) {
            String streamContent =new String(Files.readAllBytes(StreamFile.toPath()));
            streamList.add(streamContent);
        }
        return streamList;
    }

    private UploadedFileItem[] loadCappFromFileSystem(String deviceType) throws IOException {

        File directory = new File(CAR_FILE_LOCATION + File.separator + deviceType);
        File[] carFiles = directory.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".car");
            }
        });
        List<UploadedFileItem> uploadedFileItemLis = new ArrayList<>();
        if (carFiles != null) {

            for (File carFile : carFiles) {
                UploadedFileItem uploadedFileItem = new UploadedFileItem();
                DataHandler param = new DataHandler(carFile.toURI().toURL());
                uploadedFileItem.setDataHandler(param);
                uploadedFileItem.setFileName(carFile.getName());
                uploadedFileItem.setFileType("jar");
                uploadedFileItemLis.add(uploadedFileItem);
            }
        }
        UploadedFileItem[] fileItems = new UploadedFileItem[uploadedFileItemLis.size()];
        fileItems = uploadedFileItemLis.toArray(fileItems);
        return fileItems;
    }

    /**
     * Loads the keystore.
     *
     * @param keyStorePath - the path of the keystore
     * @param ksPassword   - the keystore password
     */
    private void loadKeyStore(String keyStorePath, String ksPassword)
            throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
        InputStream fis = null;
        try {
            keyStorePassword = ksPassword.toCharArray();
            keyStore = KeyStore.getInstance(KEY_STORE_TYPE);
            fis = new FileInputStream(keyStorePath);
            keyStore.load(fis, keyStorePassword);
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    /**
     * Loads the trustore
     *
     * @param trustStorePath - the trustore path in the filesystem.
     * @param tsPassword     - the truststore password
     */
    private void loadTrustStore(String trustStorePath, String tsPassword)
            throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {

        InputStream fis = null;
        try {
            trustStore = KeyStore.getInstance(TRUST_STORE_TYPE);
            fis = new FileInputStream(trustStorePath);
            trustStore.load(fis, tsPassword.toCharArray());
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    /**
     * Initializes the SSL Context
     */
    private void initSSLConnection() throws NoSuchAlgorithmException, UnrecoverableKeyException,
                                                  KeyStoreException, KeyManagementException {
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KEY_MANAGER_TYPE);
        keyManagerFactory.init(keyStore, keyStorePassword);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TRUST_MANAGER_TYPE);
        trustManagerFactory.init(trustStore);

        // Create and initialize SSLContext for HTTPS communication
        sslContext = SSLContext.getInstance(SSLV3);
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
        SSLContext.setDefault(sslContext);
    }

    private void cleanup(Stub stub) {
        if (stub != null) {
            try {
                stub.cleanup();
            } catch (AxisFault axisFault) {
                //do nothing
            }
        }
    }

}

