package org.wso2.carbon.device.mgt.ios.util;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey;
import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.mgt.common.Device;
import org.wso2.carbon.device.mgt.common.DeviceIdentifier;
import org.wso2.carbon.device.mgt.common.DeviceManagementConstants;
import org.wso2.carbon.device.mgt.common.DeviceManagementException;
import org.wso2.carbon.device.mgt.common.app.mgt.Application;
import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException;
import org.wso2.carbon.device.mgt.common.operation.mgt.Operation;
import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException;
import org.wso2.carbon.device.mgt.ios.api.utils.util.OperationMapping;
import org.wso2.carbon.device.mgt.ios.apns.exception.APNSException;
import org.wso2.carbon.device.mgt.ios.core.exception.IOSEnrollmentException;
import org.wso2.carbon.device.mgt.ios.core.service.IOSEnrollmentService;
import org.wso2.carbon.device.mgt.ios.core.util.AppConfigurations;
import org.wso2.carbon.device.mgt.ios.payload.beans.PayloadOperation;
import org.wso2.carbon.device.mgt.ios.payload.dto.PayloadOperationWrapper;
import org.wso2.carbon.device.mgt.ios.payload.exception.PListException;
import org.wso2.carbon.device.mgt.ios.payload.exception.ProfileConfigurationException;
import org.wso2.carbon.device.mgt.ios.payload.operation.InstallProfileOperation;
import org.wso2.carbon.device.mgt.ios.payload.operation.PolicyOperations;
import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo;
import org.wso2.carbon.policy.mgt.core.util.PolicyManagementConstants;
import org.wso2.carbon.user.api.UserStoreException;

import javax.ws.rs.core.MediaType;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

public class OperationUtils {

    private static final String POLICY_ID_PREFIX = "policy_";
    private static final String INSTALL_AGENT_PROTOCOL = "https://";
    private static final String INSTALL_AGENT_CONTEXT =
            "/api/appm/store/v1.1/apps/mobile/schedule-install";
    private static final int BUFFER_SIZE = 1024;
    private static final Log log = LogFactory.getLog(OperationUtils.class);
    private static final String AUTHORIZATION = "Authorization";
    private static final String AUTHORIZATION_HEADER_VALUE = "Bearer ";
    private static final String ENCODING = "UTF-8";
    public static final String IOT_CORE_HOST = "iot.core.host";
    public static final String IOT_CORE_PORT = "iot.core.https.port";

    public Operation getPendingOperation(String deviceId)
            throws DeviceManagementException, OperationManagementException {

        DeviceIdentifier deviceIdentifier = new DeviceIdentifier();
        deviceIdentifier.setId(deviceId);
        deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_IOS);

        Operation operation = IOSServiceUtils.getDeviceManagementService().
                getNextPendingOperation(deviceIdentifier);

        return operation;
    }

    public void updateOperation(Operation operation, DeviceIdentifier deviceId)
            throws DeviceManagementException, OperationManagementException {

        IOSServiceUtils.getDeviceManagementService().updateOperation(deviceId, operation);
    }

    public void updateOperationWithErrorStatus(String commandUUID, String operationError,
                                               DeviceIdentifier deviceIdentifier) {

        if(commandUUID == null || commandUUID.isEmpty() || !StringUtils.isNumeric(commandUUID)) {
            return;
        }

        try {
            Operation operation = new Operation();
            operation.setId(Integer.parseInt(commandUUID));
            operation.setStatus(Operation.Status.ERROR);
            operation.setOperationResponse(operationError);

            updateOperation(operation, deviceIdentifier);
        } catch (DeviceManagementException e) {
            log.error("DeviceManagementException occurred when saving operation error status", e);
        } catch (OperationManagementException e) {
            log.error("OperationManagementException occurred when saving operation error status", e);
        }

    }

    public void updateApplicationList(List<Application> applications, DeviceIdentifier deviceId)
            throws ApplicationManagementException {

        IOSServiceUtils.getApplicationManagementService().
                updateApplicationListInstalledInDevice(deviceId, applications);
    }

    public String generateOperationPayload(Device device, Operation operation, X509Certificate certificate)
            throws ProfileConfigurationException, PListException, APNSException, IOSEnrollmentException {

        OperationMapping operationMapping = OperationMapping.getEnumByString(operation.getCode());

        if (operationMapping == null) {
            throw new ProfileConfigurationException(
                    "Cannot map operation code: '" + operation.getCode() + "' into an operation.");
        }

        OperationMapper operationMapper = new OperationMapper();
        String profileOperationPayload;

        String operationID = Integer.toString(operation.getId());

        if (operationMapping == OperationMapping.MONITOR) {
            operationMapping = OperationMapping.PROFILE_LIST;
        }

        String commandOperationPayload = operationMapper.mapToCommandOperations(device, operationMapping, operation);

        if(commandOperationPayload != null) {
            return commandOperationPayload;
        }

        InstallProfileOperation installProfileOperation = new InstallProfileOperation();
        IOSEnrollmentService enrollmentService = IOSServiceUtils.getEnrollmentService();

        if(PolicyManagementConstants.POLICY_BUNDLE.equals(operation.getCode())) {

            List<Operation> operations =  (ArrayList<Operation>)operation.getPayLoad();

            PayloadOperationWrapper policyPayloadOperationWrapper = new PayloadOperationWrapper();
            policyPayloadOperationWrapper.setPayloadProperties(
                    new PolicyOperations().generatePayloadProperties(PolicyManagementConstants.POLICY_BUNDLE));
            List<Object> payloads = new ArrayList<>();

            int policyCounter = 0;

            for (Operation policyOperation : operations) {

                OperationMapping policyOperationMapping = OperationMapping.getEnumByString(policyOperation.getCode());

                if (policyOperationMapping == null) {
                    log.warn("Cannot map operation code: '" + policyOperation.getCode()
                            + "' into an operation. Skipping the operation from policy payload generation.");
                    continue;
                }

                String policyOperationID = POLICY_ID_PREFIX + policyCounter++;
                PayloadOperationWrapper payloadOperationWrapper = operationMapper.mapToProfileOperation(device,
                        policyOperationMapping, (String)policyOperation.getPayLoad(), policyOperationID);

                payloads.addAll(payloadOperationWrapper.getPayload());
            }

            policyPayloadOperationWrapper.setPayload(payloads);

            profileOperationPayload = PayloadOperation.generateOperationPayload(
                    policyPayloadOperationWrapper, operationID);

        } else {
            PayloadOperationWrapper payloadOperationWrapper = operationMapper.mapToProfileOperation(device,
                    operationMapping, (String) operation.getPayLoad(), operationID);

            profileOperationPayload = PayloadOperation.generateOperationPayload(payloadOperationWrapper, operationID);

        }

        try {
            byte[] signedData = enrollmentService.getSignedData(profileOperationPayload.
                    getBytes(AppConfigurations.UTF_8));

            return installProfileOperation.getInstallProfilePayload(operationID, signedData);
        } catch (UnsupportedEncodingException e) {
            throw new ProfileConfigurationException("Unsupported encoding. UTF-8 character set is not supported.");
        }
    }

    public InputStream[] cloneInputStream(InputStream inputStream) throws IOSEnrollmentException {

        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            byteArrayOutputStream = new ByteArrayOutputStream();

            byte[] buffer = new byte[BUFFER_SIZE];
            int len;
            while ((len = inputStream.read(buffer)) > -1 ) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            byteArrayOutputStream.flush();

            InputStream stream1 = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            InputStream stream2 = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            InputStream stream3 = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

            return new InputStream[]{stream1, stream2, stream3};
        } catch (IOException e) {
            throw new IOSEnrollmentException("Error occurred while cloning input stream ", e);
        } finally {
            if (byteArrayOutputStream != null) {
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {

                }
            }
        }
    }

    public boolean isOneWayOperation(Operation operation) {
        //Further modify this method if there exist any one way operations which does not return any operation response.
        return (OperationMapping.NOTIFICATION.getCode().equals(operation.getCode()) || OperationMapping.WIPE_DATA
                .getCode().equals(operation.getCode()));
    }

    public Operation getOperation(DeviceIdentifier deviceIdentifier, int operationId)
            throws DeviceManagementException, OperationManagementException {
        return IOSServiceUtils.getDeviceManagementService().getOperation(deviceIdentifier.getType(), operationId);
    }

    /**
     * Method can be used to invoke the appstore APIs to install an APP on a device
     *
     * @param deviceIdentifier The device Id of the device the app must be installed.
     * @param username         The username of the user invoking the app install.
     * @param tenantDomain     tenant domain of the user.
     * @param appId            app Id that must be installed on the device.
     * @throws DeviceManagementException
     * @throws OperationManagementException
     */
    public void addAgentInstall(String deviceIdentifier, String username, String
            tenantDomain, String appId) {
        try {
            ApiApplicationKey apiApplicationKey = OAuthUtils.getClientCredentials(tenantDomain);
            AccessTokenInfo tokenInfo = OAuthUtils.getOAuthCredentials(apiApplicationKey,
                    PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration()
                            .getAdminUserName() + "@" + tenantDomain, true);

            // App management API does not support proper JSON and requires the JSON to be an
            // escaped String.
            String payload = "{\"appId\": \"" + appId + "\", " +
                    "\"scheduleTime\":\"2013-12-25T15:25:30-05:00\",\"deviceIds\": " +
                    "[\"{\\\"id\\\":\\\"" + deviceIdentifier + "\\\"," +
                    "\\\"type\\\":\\\"ios\\\"}\"]}";
            StringRequestEntity requestEntity = new StringRequestEntity(payload, MediaType.APPLICATION_JSON, ENCODING);
            HttpClient httpClient = new HttpClient();
            String url = INSTALL_AGENT_PROTOCOL + System.getProperty(IOT_CORE_HOST) + ":" +
                    System.getProperty(IOT_CORE_PORT) + INSTALL_AGENT_CONTEXT;
            PostMethod request = new PostMethod(url);
            request.addRequestHeader(AUTHORIZATION, AUTHORIZATION_HEADER_VALUE +
                    tokenInfo.getAccessToken());
            request.setRequestEntity(requestEntity);
            httpClient.executeMethod(request);
        } catch (APIManagerException e) {
            log.error("Error while getting access token for DEP device id: " +
                    deviceIdentifier, e);
        } catch (UserStoreException e) {
            log.error("Error while getting accessing the user store for user " +
                    username, e);
        } catch (HttpException e) {
            log.error("Error while calling the app store to install app id: " +
                    appId + " on device with id: " + deviceIdentifier, e);
        } catch (IOException e) {
            log.error("Error while installing the app  with id: " +
                    appId + " on device with id: " + deviceIdentifier, e);
        }
    }

}
