/***********************************************************************************************************
 * Name         : WelcomeEmailBatchHelper
 * Purpose      : Helper Class to execute the callouts to MC, in order to send welcome emails to the fans.
 **********************************************************************************************************/
public with sharing class WelcomeEmailBatchHelper {
    public static final String SEPARATOR = '|';
    public static final String NOT_AUTHORIZED_ERROR = 'Not Authorized';
    public static final String INTERNAL_SERVER_ERROR = 'Internal Server Error';
    public static final String TSDK_NOT_FOUND_ERROR = 'Triggered send definition key not found';
    public static final String NON_EXISTING_MID_ERROR = 'Non existing MID';

    /**
     * @description segregates the SObjects into unique combination groups of TSDK+MID
     * @param SObject List of sObjects that will be grouped by TSDK and MID
     * @return void
     */
    public static Map<String, List<SObject>> groupByTSDKandMID(List<SObject> scope) {
        Map<String, List<SObject>> midTsdkMap = new Map<String, List<SObject>>();

        for (SObject sObj : scope) {
            String tsdk = (String) sObj.get('triggered_send_definition_key__c');
            String mid = sObj instanceof SubscriptionFanTriggerSendMapping__c
                ? (String) sObj.get('MC_MID__c')
                : (String) sObj.get('Parent_MID__c');
            String key = tsdk + SEPARATOR + mid;

            if (midTsdkMap.containsKey(key)) {
                midTsdkMap.get(key).add(sObj);
            } else {
                midTsdkMap.put(key, new List<SObject>{ sObj });
            }
        }
        return midTsdkMap;
    }
    /**
     * @description create and send a Http request to MC in order to send welcome emails OR DoubleOptIn emails,
     * along with updating the Welcome_Email_Delivered__c field.
     * @param Map<String, List<SObject>> combinations of TSDK+MID related to a specific SObject list
     * @return List<SObject> SObjects to update
     */
    public static List<SObject> createTriggeredSend(Map<String, List<SObject>> midTsdkMap) {
        List<SObject> sObjectsToUpdate = new List<SObject>();
        Boolean isSubFan = midTsdkMap.values()[0][0] instanceof SubscriptionFanTriggerSendMapping__c ? true : false;
        Map<String, String> mapMidToken = getAllTokens(midTsdkMap.keySet());
        for (String key : midTsdkMap.keySet()) {
            try {
                String tsdk = key.substringBefore(SEPARATOR);
                String mid = key.substringAfter(SEPARATOR);
                List<SObject> listGroupedByTSDK = midTsdkMap.get(key);
                if (mapMidToken.get(mid) == null) {
                    handleResponseError(listGroupedByTSDK, sObjectsToUpdate, isSubFan, NON_EXISTING_MID_ERROR);
                    continue;
                }
                String bodyJson = isSubFan
                    ? createBodyForSubFan(listGroupedByTSDK)
                    : createBodyForDoTemp(listGroupedByTSDK);
                Http http = new Http();
                HttpRequest req = new HttpRequest();
                HttpResponse response = new HttpResponse();
                String endpoint = ConfigUtils.getConfigString(ConfigUtils.MC_WELCOME_EMAIL_ENDPOINT);
                String tsdkEncoded = EncodingUtil.urlEncode(tsdk, 'UTF-8').replace('+', '%20');
                endpoint = endpoint.replace('{TSDK}', tsdkEncoded);
                req.setMethod('POST');
                req.setHeader('Authorization', 'Bearer ' + mapMidToken.get(mid));
                req.setHeader('Content-Type', 'application/json');
                req.setBody(bodyJson);
                req.setTimeout(120000);
                req.setEndpoint(endpoint);
                response = http.send(req);

                if (response.getStatusCode() == 202) {
                    WelcomeEmailResponse deserializedResponseJson = (WelcomeEmailResponse) JSON.deserialize(
                        response.getBody(),
                        WelcomeEmailResponse.class
                    );
                    handleResponse(listGroupedByTSDK, sObjectsToUpdate, isSubFan, deserializedResponseJson);
                } else if (response.getStatusCode() == 404) {
                    handleResponseError(listGroupedByTSDK, sObjectsToUpdate, isSubFan, TSDK_NOT_FOUND_ERROR);
                } else {
                    ErrorMessages deserializedResponseJson = (ErrorMessages) JSON.deserialize(
                        response.getBody(),
                        ErrorMessages.class
                    );
                    if (
                        deserializedResponseJson.message != INTERNAL_SERVER_ERROR &&
                        deserializedResponseJson.message != NOT_AUTHORIZED_ERROR
                    ) {
                        handleResponseError(
                            listGroupedByTSDK,
                            sObjectsToUpdate,
                            isSubFan,
                            deserializedResponseJson.message
                        );
                    }
                }
            } catch (Exception ex) {
                SMELogger.logError(ex);
            }
        }
        return sObjectsToUpdate;
    }

    /**
     * @description saves all the neccessary auth tokens into a map of mid + token
     * @param Set<String> set with all the combinations of tsdk+mid
     * @return Map<String, String> map saving all the tokens for a specific MID
     */
    public static Map<String, String> getAllTokens(Set<String> keys) {
        Map<String, String> mapMidToken = new Map<String, String>();
        Set<String> mids = new Set<String>();
        for (String key : keys) {
            String mid = key.substringAfter(SEPARATOR);
            mids.add(mid);
        }
        for (String mid : mids) {
            MarketingCloudHelper.MarketingCloudAuthResponse authResp = MarketingCloudHelper.getTokenForWelcomeEmail(
                mid
            );
            mapMidToken.put(mid, authResp.access_token);
        }
        return mapMidToken;
    }

    /**
     * @description creates a body json based on a list of doTemps grouped by TDSK
     * @param DO_Temp_Form_Response__c List with a unique combination of TSDK+MID
     * @return String JSON body for a unique API call
     */
    public static String createBodyForDoTemp(List<SObject> listGroupedByTSDK) {
        List<DO_Temp_Form_Response__c> convertedList = new List<DO_Temp_Form_Response__c>();
        List<MessageDefinitionSendsBody> listTriggeredSendItem = new List<MessageDefinitionSendsBody>();

        for (SObject sObj : listGroupedByTSDK) {
            DO_Temp_Form_Response__c doTemp = (DO_Temp_Form_Response__c) sObj;
            convertedList.add(doTemp);
        }

        for (DO_Temp_Form_Response__c doTemp : convertedList) {
            To toObj = new To();
            toObj.Address = doTemp.Email__c;
            toObj.SubscriberKey = doTemp.Id;

            MessageDefinitionSendsBody MessageDefinitionSendsBody = new MessageDefinitionSendsBody();
            MessageDefinitionSendsBody.To = toObj;

            listTriggeredSendItem.add(MessageDefinitionSendsBody);
        }
        return JSON.serialize(listTriggeredSendItem);
    }

    /**
     * @description creates a body json based on a list of subFans grouped by TDSK
     * @param SubscriptionFanTriggerSendMapping__c List with a unique combination of TSDK+MID
     * @return String JSON body for a unique API call
     */
    public static String createBodyForSubFan(List<SObject> listGroupedByTSDK) {
        List<SubscriptionFanTriggerSendMapping__c> convertedList = new List<SubscriptionFanTriggerSendMapping__c>();
        List<MessageDefinitionSendsBody> listTriggeredSendItem = new List<MessageDefinitionSendsBody>();

        for (SObject sObj : listGroupedByTSDK) {
            SubscriptionFanTriggerSendMapping__c subFan = (SubscriptionFanTriggerSendMapping__c) sObj;
            convertedList.add(subFan);
        }

        for (SubscriptionFanTriggerSendMapping__c subFan : convertedList) {
            String subscriberKey = getValue(subFan.Fan_ID__c);
            String fanEmail = getValue(subFan.Fan_ID__r.Email__c);
            String firstName = getValue(subFan.Fan_ID__r.First_Name__c);
            String lastName = getValue(subFan.Fan_ID__r.Last_Name__c);
            String postalCode = getValue(subFan.Fan_ID__r.Postal_Code__c);
            String encryptedId = getValue(subFan.Fan_ID__r.Encrypted_ID__c);
            String formId = getValue(subFan.Form_id__c);
            String tla = getValue(subFan.Form_id__r.TLA_ID__c);
            String subscriptionId = getValue(subFan.Subscription_ID__c);
            String mailingListId;
            String promoId;
            String territoryId = getValue(subFan.Subscription_ID__r.Mailing_List_ID__r.Tla_ID__r.Territory_ID__c);
            String labelName = getValue(subFan.Form_ID__r.Tla_ID__r.Label_ID__r.Name);
            String artistName = getValue(subFan.Form_ID__r.TLA_ID__r.Artist_ID__r.Artist__c);
            if (String.isBlank(subscriptionId)) {
                subscriptionId = 'No Subscription Id';
                mailingListId = 'No Mailing List Id';
                promoId = 'No Promo Id';
            } else {
                mailingListId = getValue(subFan.Subscription_ID__r.Mailing_List_ID__c);
                promoId = String.isBlank(subFan.Subscription_ID__r.Promo_Id__r.Name)
                    ? 'No Promo Id'
                    : subFan.Subscription_ID__r.Promo_Id__r.Name;
            }

            To toObj = new To();
            toObj.Address = fanEmail;
            toObj.SubscriberKey = subscriberKey;
            toObj.ContactAttributes = new ContactAttributes(
                new Map<String, String>{
                    'First Name' => firstName,
                    'Last Name' => lastName,
                    'Postal Code' => postalCode,
                    'Encrypted ID' => encryptedId,
                    'Form ID' => formId,
                    'TLA' => tla,
                    'Mailing_List_Id__c' => mailingListId,
                    'X18_Digit_ID__c' => subscriptionId,
                    'Promo Id' => promoId,
                    'Form_ID__c' => formId,
                    'Territory_ID__c' => territoryId,
                    'Label Name' => labelName,
                    'Mailing List ID: TLA ID: Artist ID: Artist' => artistName,
                    'StartDate' => String.valueOf(system.today())
                }
            );
            MessageDefinitionSendsBody MessageDefinitionSendsBody = new MessageDefinitionSendsBody();
            MessageDefinitionSendsBody.To = toObj;

            listTriggeredSendItem.add(MessageDefinitionSendsBody);
        }

        return JSON.serialize(listTriggeredSendItem);
    }
    public class MessageDefinitionSendsBody {
        public To To { get; set; }
    }
    public class To {
        public String Address { get; set; }
        public String SubscriberKey { get; set; }
        public ContactAttributes ContactAttributes { get; set; }
    }

    public class ContactAttributes {
        public Map<String, String> SubscriberAttributes { get; set; }

        public ContactAttributes(Map<String, String> SubscriberAttributes) {
            this.SubscriberAttributes = SubscriberAttributes;
        }
    }

    /**
     * @description Returns the value provided when it is not null. Otherwise, it returns an empty String.
     * @param String field
     * @return String
     */
    private static String getValue(String value) {
        return String.isBlank(value) ? '' : value;
    }

    public class WelcomeEmailResponse {
        public String requestId;
        public Boolean batchHasErrors;
        public List<ResponseMessages> responses;
    }
    public class ResponseMessages {
        public String recipientSendId;
        public Boolean hasErrors;
        public List<String> messages;
    }
    public class ErrorMessages {
        public String documentation;
        public Integer errorcode;
        public String message;
    }

     /**
     * @description handler method for 404 and else responses
     * @param List<SObject> listGroupedByTSDK -> list with the records that were in the callout execution
     * @param List<SObject> sObjectsToUpdate -> list with the records that will be updated
     * @param Boolean isSubFan -> true if the record is a subscriptionFanTriggerSendMapping
     * @param String errorMessage -> error message that will be updated in the  MC_Error_Message__c field
     * @return void
     */
    public static void handleResponseError(
        List<SObject> listGroupedByTSDK,
        List<SObject> sObjectsToUpdate,
        Boolean isSubFan,
        String errorMessage
    ) {
        for (SObject sObjToUpdate : listGroupedByTSDK) {
            sObjToUpdate.put('MC_Error_Message__c', errorMessage);
            if (isSubFan) {
                sObjToUpdate.put('Welcome_Email_Triggered__c', true);
            }
            sObjectsToUpdate.add(sObjToUpdate);
        }
    }

    /**
     * @description handler method for 202 response
     * @param List<SObject> listGroupedByTSDK -> list with the records that were in the callout execution
     * @param List<SObject> sObjectsToUpdate -> list with the records that will be updated
     * @param Boolean isSubFan -> true if the record is a subscriptionFanTriggerSendMapping
     * @param WelcomeEmailResponse deserializedResponseJson -> response 
     * @return void
     */
    public static void handleResponse(
        List<SObject> listGroupedByTSDK,
        List<SObject> sObjectsToUpdate,
        Boolean isSubFan,
        WelcomeEmailResponse deserializedResponseJson
    ) {
        Integer count = 0;
        for (SObject sObjToUpdate : listGroupedByTSDK) {
            if (deserializedResponseJson.responses[count].hasErrors) {
                sObjToUpdate.put('MC_Error_Message__c', deserializedResponseJson.responses[count].messages[0] ?? 'Error - no result message');
            } else {
                sObjToUpdate.put('Welcome_Email_Delivered__c', true);
                sObjToUpdate.put('MC_Error_Message__c', null);
                sObjToUpdate.put('RecipientSendId__c', deserializedResponseJson.responses[count].recipientSendId);
            }

            if (isSubFan) {
                sObjToUpdate.put('Welcome_Email_Triggered__c', true);
            }

            sObjectsToUpdate.add(sObjToUpdate);
            count++;
        }
    }

    /**
     * @description sends an email with the failed welcome email sends
     * @param Map<String, List<SObject>> String error and related objects 
     * @return void
     */
    public static void sendErrorEmailNotification(Map<String, List<SObject>> errorMap, Boolean isSubFanObj) {
        String csvName = isSubFanObj ? 'failedSubFans.csv' : 'failedDoTemps.csv';
        for (String key : errorMap.keySet()) {
            String subject;

            if (key == TSDK_NOT_FOUND_ERROR) {
                subject = ConfigUtils.WE_FAILURE_EMAIL_SUBJECT_TSDK;
            } else if (key == NON_EXISTING_MID_ERROR) {
                subject = ConfigUtils.WE_FAILURE_EMAIL_SUBJECT_MID;
            } else {
                subject = ConfigUtils.WE_FAILURE_EMAIL_SUBJECT_NEW_ERROR;
            }

            BatchJobUtils.sendEmail(
                ConfigUtils.WE_FAILURE_EMAIL_RECIPIENTS,
                ConfigUtils.WE_FAILURE_EMAIL_SENDER,
                subject,
                ConfigUtils.WE_FAILURE_EMAIL_BODIES,
                csvName,
                errorMap.get(key)
            );
        }
    }

    /**
     * @description populates a map of String error and related records that failed
     * @param Map<String, List<SObject>> String error and related objects 
     * @param List<SObject> failed records
     * @param Boolean isSubFan -> true if the record is a subscriptionFanTriggerSendMapping
     * @return void
     */
    public static void mapErrors(Map<String, List<SObject>> errorMap, List<SObject> sObjectList, Boolean isSubFan) {
        for (SObject obj : sobjectList) {
            String mcErrorMessage = (String) obj.get('MC_Error_Message__c');
            Boolean welcomeEmailDelivered = (Boolean) obj.get('Welcome_Email_Delivered__c');
            Boolean welcomeEmailTriggered = isSubFan ? (Boolean) obj.get('Welcome_Email_Triggered__c') : null;

            if (
                (isSubFan && !welcomeEmailDelivered && welcomeEmailTriggered && mcErrorMessage != null) ||
                (!isSubFan && !welcomeEmailDelivered && mcErrorMessage != null)
            ) {
                if (errorMap.containsKey(mcErrorMessage)) {
                    errorMap.get(mcErrorMessage).add(filterFields(obj, isSubFan));
                } else {
                    List<SObject> errorSObjectList = new List<SObject>{ filterFields(obj, isSubFan) };
                    errorMap.put(mcErrorMessage, errorSObjectList);
                }
            }
        }
    }

    /**
     * @description filters the fields that will be displayed in the CSV
     * @param SObject obj -> object to be filtered
     * @param Boolean isSubFan -> true if the record is a subscriptionFanTriggerSendMapping
     * @return SObject filtered SObject
     */
    public static SObject filterFields(SObject obj, Boolean isSubFan) {
        if (!isSubFan) {
            return new DO_Temp_Form_Response__c(
                Id = obj.Id,
                Parent_MID__c = (String) obj.get('Parent_MID__c'),
                triggered_send_definition_key__c = (String) obj.get('triggered_send_definition_key__c'),
                Form_ID__c = (String) obj.get('Form_ID__c'), 
                MC_Error_Message__c = (String) obj.get('MC_Error_Message__c')
            );
        } else {
            return new SubscriptionFanTriggerSendMapping__c(
                Id = obj.Id,
                MC_MID__c = (String) obj.get('MC_MID__c'), 
                Triggered_Send_Definition_Key__c = (String) obj.get('Triggered_Send_Definition_Key__c'), 
                Form_ID__c = (String) obj.get('Form_ID__c'),
                MC_Error_Message__c = (String) obj.get('MC_Error_Message__c')
            );
        }
    }
}