package org.wso2.carbon.device.mgt.ios.dep.utils;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpStatus;
import org.json.JSONObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.wso2.carbon.device.mgt.common.DeviceManagementException;
import org.wso2.carbon.device.mgt.ios.dep.DEPConstants;
import org.wso2.carbon.device.mgt.ios.dep.beans.ServerToken;
import org.wso2.carbon.device.mgt.ios.dep.exception.AppleDEPConnectorException;
import org.wso2.carbon.device.mgt.ios.dep.exception.DEPServerTokenException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

import static java.net.URLEncoder.encode;

/**
 * This class represents  methods through which unique session authorization token is generated
 * where the generated token is used to access all the Apple DEP endpoints.
 * This supports the OAuth 1.0a protocol for accessing protected resources.
 */
public class SessionTokenUtils {

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

    /**
     * Generates the session authorization token.
     * @return String generated session authorization token.
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     * @throws DEPServerTokenException if the server token (.p7m) is expired.
     * @throws DeviceManagementException if iOS DEP configuration is not able to retrieve.
     */
    public static String generateSessionToken() throws AppleDEPConnectorException,
                                                       DEPServerTokenException,
                                                       DeviceManagementException {
        EndpointUtils.checkTokenExpiration();
        String sessionToken = createParams();
        JSONObject jsonObject = new JSONObject(sessionToken);
        return (String) jsonObject.get("auth_session_token");
    }

    /**
     * Creates key value pairs and encodes the session authorization URL and params with
     * UTF-8 encoding method.
     * @return String which contains the session authorization token.
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     * @throws DeviceManagementException if iOS DEP configuration is not able to retrieve.
     */
    public static String createParams() throws AppleDEPConnectorException, DeviceManagementException {
        ServerToken token = TokenUtils.retrieveDEPConfiguration();
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("oauth_consumer_key", token.getConsumerKey()));
        params.add(new BasicNameValuePair("oauth_nonce", "dep" + (int) (Math.random() *
                                                                DEPConstants.NONCE_MULTIPLIER)));
        params.add(new BasicNameValuePair("oauth_signature_method", DEPConstants.SIGNATURE_METHOD));
        params.add(new BasicNameValuePair("oauth_timestamp", "" + (System.currentTimeMillis() /
                                                                DEPConstants.TIMESTAMP_DIVIDER)));
        params.add(new BasicNameValuePair("oauth_token", token.getAccessToken()));
        params.add(new BasicNameValuePair("oauth_version", DEPConstants.OAUTH_VERSION));
        String signatureBase = null;
        try {
            signatureBase = getSignatureBase(encode(DEPConstants.AUTH_SESSION_URL,
                                    DEPConstants.ENC_UTF_8), encode(URLEncodedUtils.format(params,
                                    DEPConstants.ENC_UTF_8), DEPConstants.ENC_UTF_8));
        } catch (UnsupportedEncodingException e) {
            throw new AppleDEPConnectorException("Unsupported encoding error occurred while " +
                                                 "encoding auth session URL and params.", e);
        }
        String signature = generateSignature(signatureBase);
        params.add(new BasicNameValuePair("oauth_signature", signature));
        String requestHeader = createRequestHeader(params);
        byte[] tokenBytes = executeRequest(requestHeader);
        return new String(tokenBytes);
    }

    /**
     * Created an encoded URL.
     * @param url encoded session authorization token requesting URL.
     * @param params encoded key value pairs.
     * @return encoded URL.
     */
    public static String getSignatureBase(String url, String params) {
        StringBuilder base = new StringBuilder();
        base.append(DEPConstants.HTTP_GET).append("&").append(url).append("&").append(params);
        return base.toString();
    }

    /**
     * This method encodes the Consumer Secret and Access Secret to UTF-8 encoding method and
     * then hash with HMAC-SHA1 protocol.
     * @param signatureBase encoded URL generated through "getSignatureBase" method.
     * @return HMAC-SHA1 signature.
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     * @throws DeviceManagementException if iOS DEP configuration is not able to retrieve.
     */
    public static String generateSignature(String signatureBase) throws AppleDEPConnectorException,
                                                                        DeviceManagementException {
        ServerToken token = TokenUtils.retrieveDEPConfiguration();
        byte[] byteHMAC = null;
        try {
            Mac mac = Mac.getInstance(DEPConstants.HMAC_SHA1);
            String signingKey = encode(token.getConsumerSecret(), DEPConstants.ENC_UTF_8) + '&' +
                                    encode(token.getAccessSecret(), DEPConstants.ENC_UTF_8);
            SecretKeySpec spec = new SecretKeySpec(signingKey.getBytes(), DEPConstants.HMAC_SHA1);
            mac.init(spec);
            byteHMAC = mac.doFinal(signatureBase.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new AppleDEPConnectorException("HMAC-SHA1 algorithm not found.", e);
        } catch (InvalidKeyException e) {
            throw new AppleDEPConnectorException("Invalid secret key spec when initializing", e);
        } catch (UnsupportedEncodingException e) {
            throw new AppleDEPConnectorException("Error occurred while encoding auth session URL", e);
        }
        return new String(new Base64().encode(byteHMAC));
    }

    /**
     * Creates a header which is to be used in the request of the session authorization token.
     * @param params key value pairs.
     * @return request header
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     */
    public static String createRequestHeader(List<NameValuePair> params)
            throws AppleDEPConnectorException {
        String requestHeader = "OAuth ";
        for (NameValuePair pair : params) {
            if (("oauth_signature").equals(pair.getName())) {
                String encodedSignature = null;
                try {
                    encodedSignature = encode(pair.getValue(), DEPConstants.ENC_UTF_8);
                } catch (UnsupportedEncodingException e) {
                    throw new AppleDEPConnectorException("Unsupported encoding when encoding " +
                                                         "signature", e);
                }
                requestHeader += pair.getName() + "=\"" + encodedSignature + "\",";
            } else {
                requestHeader += pair.getName() + "=\"" + pair.getValue() + "\",";
            }
        }
        return requestHeader;
    }

    /**
     * Executes the session authorization request by calling Apple DEP endpoint.
     * @param requestHeader request header.
     * @return byte[] containing response message.
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     */
    public static byte[] executeRequest(String requestHeader) throws AppleDEPConnectorException {
        HttpURLConnection httpConnection = null;
        try {
            httpConnection = WebTransportUtils.getHttpConnection(DEPConstants.AUTH_SESSION_URL);
            httpConnection.setRequestMethod(DEPConstants.HTTP_GET);
            httpConnection.setRequestProperty("Authorization", requestHeader);
            byte[] tokenBytes = WebTransportUtils.getResponseBytes(httpConnection);
            handleWebException(httpConnection, tokenBytes);
            return tokenBytes;
        } catch (ProtocolException e) {
            throw new AppleDEPConnectorException("Protocol specific error occurred when trying " +
                                                 "to set method to " + DEPConstants.HTTP_GET +
                                                 " for:" + DEPConstants.AUTH_SESSION_URL, e);
        } finally {
            if (httpConnection != null) {
                httpConnection.disconnect();
            }
        }
    }

    /**
     * Handles session authorization specific exceptions.
     * @param connection http connection request.
     * @param tokenBytes containing response message.
     * @throws AppleDEPConnectorException if the method encounters any Apple DEP related issue.
     */
    public static void handleWebException(HttpURLConnection connection, byte[] tokenBytes)
            throws AppleDEPConnectorException {
        WebTransportUtils.checkHttpConnection(connection);
        String errorMessage = "";
        String responseMessage = new String(tokenBytes);
        int responseCode = WebTransportUtils.getHttpResponseCode(connection);
        if (responseCode == HttpStatus.SC_OK) {
            return;
        }
        if (responseCode == HttpStatus.SC_BAD_REQUEST) {
            switch (responseMessage) {
                case "parameter_absentBad Request":
                    errorMessage = "the required parameters missing";
                    break;
                case "parameter_rejectedBad Request":
                    errorMessage = "invalid or duplicated parameters";
                    break;
                case "signature_method_rejectedBad Request":
                    errorMessage = "invalid signature method";
                    break;
                case "oauth_problem_adviceBad Request":
                    errorMessage = "the required authorization parameters missing";
                    break;
                default:
                    errorMessage = new String(tokenBytes).replace("Bad Request", "");
                    break;
            }
        } else if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
            switch (responseMessage) {
                case "nonce_usedUnauthorized":
                    errorMessage = "the usage of already used nonce";
                    break;
                case "signature_invalidUnauthorized":
                    errorMessage = "invalid signature";
                    break;
                default:
                    errorMessage = new String(tokenBytes).replace("Unauthorized", "");
                    break;
            }
        } else if (responseCode == HttpStatus.SC_FORBIDDEN) {
            switch (responseMessage) {
                case "ACCESS_DENIED":
                    errorMessage = "the MDM server not having access";
                    break;
                case "T_C_NOT_SIGNED":
                    errorMessage = "the organization not accepting the latest Terms & Conditions";
                    break;
                default:
                    errorMessage = "forbidden";
                    break;
            }
        } else {
            errorMessage = responseMessage;
        }
        throw new AppleDEPConnectorException("The request to " + connection.getURL() + " failed " +
                                             "due to " + errorMessage + " while generating Apple" +
                                             " DEP session authorization token.");
    }
}
