/***********************************************************************************************************
 * Name         : NodeJsHelper
 * Purpose      : Utility class containing logic to communicate with the NodeJs app
 **********************************************************************************************************/
public with sharing class NodeJsHelper {
    private static String auth0Token;

    /**
     * @description Retrieves the auth0 token from the Auth0__c record
     * @return String   auth0 Token
     */
    @TestVisible
    private static String getAuth0Token() {
        if (auth0Token == null) {
            auth0Token = [SELECT Token__c FROM Auth0__c WHERE Name = :UtilConstants.AUTH0_NODEJS LIMIT 1].Token__c;
        }
        return auth0Token;
    }

    /**
     * @description Performs the export tracker query in Snowflake.
     *  To prevent Heap Size errors, the result is a String instead of a wrapper instance
     * @param  Export__c    export record
     * @return String       decrypted response
     */
    public static String performExportTrackerQuery(Export__c exp) {
        return performQueryToString(SnowflakeUtils.transformExportQuerytoSQL(exp));
    }

    /**
     * @description Performs a query in Snowflake to retrieve all the FRs for the
     *  fans provided
     * @param  List<String> Fan Ids
     * @return List<SnowflakeFormResponseWrapper>   Snowflake FR Wrappers
     */
    public static List<SnowflakeFormResponseWrapper> performFormResponsesQuery(List<String> fanIds) {
        String query = SnowflakeUtils.createQueryForVendors(fanIds);
        DecryptedResponseWrapper decryptedResponse = performQuery(query);
        String serializedRows = JSON.serialize(decryptedResponse.rows);
        List<SnowflakeFormResponseWrapper> rows = (List<SnowflakeFormResponseWrapper>) JSON.deserialize(
            serializedRows,
            List<SnowflakeFormResponseWrapper>.class
        );
        return rows;
    }

    /**
     * @description Performs the provided query in Snowflake and decrypts the response
     *  into a DecryptedResponseWrapper instance. If the query is likely to return a huge
     *  dataset, performQueryToString should be used instead and the result should be handled
     *  asynchronously.
     * @param  String   query to execute
     * @return DecryptedResponseWrapper decrypted response
     */
    public static DecryptedResponseWrapper performQuery(String query) {
        String result = performQueryToString(query);
        return stringToDecryptedResponse(result);
    }

    /**
     * @description Performs the provided query in Snowflake and decrypts the response
     *  into a String
     * @param  String   query to execute
     * @return String   decrypted response
     */
    public static String performQueryToString(String query) {
        String encryptedBody = encryptBody('{"query": "' + query + '"}');
        HttpResponse response = makeAPIcall(
            encryptedBody,
            ConfigUtils.getConfigString(ConfigUtils.NODEJS_ENDPOINT_QUERY)
        );
        EncryptedResponseWrapper responseWrapper = (EncryptedResponseWrapper) JSON.deserialize(
            response.getBody(),
            EncryptedResponseWrapper.class
        );
        return decryptResponseToString(responseWrapper);
    }

    /**
     * @description Performs the Http Request
     * @param   String   body
     * @param   String  endpoint
     * @return HttpResponse response
     */
    public static HttpResponse makeAPIcall(String body, String endpoint) {
        HttpRequest request = new HttpRequest();
        request.setEndpoint(endpoint);
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json');
        request.setHeader('Authorization', 'Bearer ' + getAuth0Token());
        request.setTimeout(120000);
        request.setBody(body);

        HttpResponse response = new Http().send(request);
        if (response.getStatusCode() != 200) {
            throw new NodeJsException(response.getBody());
        }
        return response;
    }

    /**
     * @description Encrypts the request body
     * @param  String   body to encrypt
     * @return String   encrypted body
     */
    @TestVisible
    private static String encryptBody(String body) {
        Blob key = EncodingUtil.base64Decode(ConfigUtils.getConfigString(ConfigUtils.NODEJS_AES_KEY));
        Blob iv = Crypto.generateAesKey(128);
        Blob encrypted = Crypto.encrypt('AES256', key, iv, Blob.valueOf(body));

        return '{"iv": "' +
            EncodingUtil.base64Encode(iv) +
            '", "data": "' +
            EncodingUtil.base64Encode(encrypted) +
            '"}';
    }

    /**
     * @description Decrypts an EncryptedResponseWrapper instance
     * @param  EncryptedResponseWrapper     encrypted response
     * @return DecryptedResponseWrapper     decrypted response
     */
    private static DecryptedResponseWrapper decryptResponse(EncryptedResponseWrapper responseWrapper) {
        String decryptedResponse = decryptResponseToString(responseWrapper);
        return stringToDecryptedResponse(decryptedResponse);
    }

    /**
     * @description Creates a decryptedResponseWrapper instance from a
     *  decrypted response String
     * @param  String   decrypted response String
     * @return DecryptedResponseWrapper     decryptedResponseWrapper instance
     */
    @TestVisible
    private static DecryptedResponseWrapper stringToDecryptedResponse(String decryptedResponse) {
        Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(decryptedResponse);
        decryptedResponse = null;
        List<Map<String, Object>> rows = new List<Map<String, Object>>();
        for (Object objectRow : (List<Object>) data.get('rows')) {
            Map<String, Object> deserializedRow = (Map<String, Object>) objectRow;
            rows.add(deserializedRow);
        }
        return new DecryptedResponseWrapper(Integer.valueOf(data.get('count')), rows);
    }

    /**
     * @description Decrypts an EncryptedResponseWrapper into a String
     * @param  EncryptedResponseWrapper     encrypted response
     * @return String     decrypted String
     */
    private static String decryptResponseToString(EncryptedResponseWrapper responseWrapper) {
        Blob key = EncodingUtil.base64Decode(ConfigUtils.getConfigString(ConfigUtils.NODEJS_AES_KEY));
        Blob iv = EncodingUtil.base64Decode(responseWrapper.iv);

        List<String> result = new List<String>();
        for (String data : responseWrapper.data) {
            Blob decryptedBatch = Crypto.decrypt('AES256', key, iv, EncodingUtil.base64Decode(data));
            result.add(decryptedBatch.toString());
        }
        responseWrapper = null;
        return String.join(result, '');
    }

    public class EncryptedResponseWrapper {
        public String iv;
        public List<String> data;

        public EncryptedResponseWrapper(String iv, List<String> data) {
            this.iv = iv;
            this.data = data;
        }
    }

    public class DecryptedResponseWrapper {
        public Integer count;
        public List<Map<String, Object>> rows;

        public DecryptedResponseWrapper(Integer count, List<Map<String, Object>> rows) {
            this.count = count;
            this.rows = rows;
        }
    }

    public class NodeJsException extends Exception {
    }
}
