/**
 * @description Sends a Docusign composite envelope via direct REST API call when the
 *              primary recipient (Docusign Recipient #1) does not have an email address
 *              and requires SMS delivery instead.
 *
 *              The dfsle Apex Toolkit does not support the Docusign
 *              additionalNotifications/secondaryDeliveryMethod property needed for
 *              SMS-only recipients, so this service bypasses the toolkit and calls
 *              the Docusign REST API directly.
 *
 *              Prerequisites:
 *              - Named Credential "DocusignAPI" must be configured in the org.
 *              - Docusign account must have SMS delivery enabled.
 *              - Docusign_Configuration__c custom setting must have Account_Id__c populated.
 *
 * @author Paul Huliganga
 * @date 2026-02-25
 */
global with sharing class DocusignSmsEnvelopeService {

    // Named Credential for Docusign REST API callouts
    @TestVisible
    private static final String NAMED_CREDENTIAL = 'callout:DocusignAPI';

    // Endpoint path template — {0} is replaced with the accountId
    @TestVisible
    private static final String ENVELOPES_PATH = '/accounts/{0}/envelopes';

    // Dummy email used for SMS-only recipients. Docusign requires an email address
    // on every recipient even when delivery is via SMS. This placeholder satisfies
    // that requirement without routing any actual email.
    @TestVisible
    private static final String SMS_PLACEHOLDER_EMAIL = 'placeholder_email@docusign.com';

    // Country code assumed when the caller supplies a bare 10-digit US number.
    // The flow enforces E.164 format (e.g. +15551234567) so this is a safety fallback.
    private static final String DEFAULT_COUNTRY_CODE = '1';

    /**
     * @description Builds and sends a composite Docusign envelope with SMS delivery for
     *              the primary recipient. Mirrors the logic in DocusignCompositeEnvelopeBuilder
     *              but constructs the JSON payload manually so that additionalNotifications
     *              can be included on the recipient object.
     *
     * @param recordId            The Client_Case__c record ID (used to look up recipients)
     * @param sortedIds           Template IDs in sorted order (may contain duplicates for multi-copy)
     * @param docNames            Document labels in same order as sortedIds
     * @param displayNames        Deduplicated display labels for email subject / body
     * @param envelopeSubject     Email subject line (already formatted and truncated)
     * @param envelopeBody        Email body text (already formatted and deduplicated)
     * @param smsPhone            Mobile phone number for SMS delivery (E.164 preferred, e.g. +15551234567)
     * @return Docusign envelope ID string on success
     * @throws IllegalArgumentException when recipients or configuration data are missing
     * @throws CalloutException on HTTP errors
     */
    global static String sendEnvelope(
        String recordId,
        List<String> sortedIds,
        List<String> docNames,
        List<String> displayNames,
        String envelopeSubject,
        String envelopeBody,
        String smsPhone
    ) {
        // ─── Fetch Docusign account ID from custom setting ───────────────────────────
        Docusign_Configuration__c config = Docusign_Configuration__c.getInstance();
        if (config == null || String.isBlank(config.Account_Id__c)) {
            throw new IllegalArgumentException(
                'Docusign_Configuration__c is not configured. '
                + 'Please set Account_Id__c in the Docusign Configuration custom setting.'
            );
        }
        String accountId = config.Account_Id__c;

        // ─── Resolve recipients from Client_Case__c ───────────────────────────────────
        RecipientInfo serviceCoordinator = null;
        RecipientInfo docusignRecipient  = null;

        String query = 'SELECT Id, Service_Coordinator__c, Docusign_Recipient_1__c '
            + 'FROM Client_Case__c WHERE Id = :recordId LIMIT 1';
        Client_Case__c caseRecord = Database.query(query);

        Id scId  = (Id) caseRecord.get('Service_Coordinator__c');
        Id drId  = (Id) caseRecord.get('Docusign_Recipient_1__c');

        if (scId != null) {
            serviceCoordinator = resolveRecipient(scId, 'Service Coordinator', 1, null, false);
        }
        if (drId != null) {
            // SMS phone provided — substitute placeholder email for the Docusign Recipient
            docusignRecipient = resolveRecipient(drId, 'Docusign Recipient #1', 2, smsPhone, true);
        }

        if (serviceCoordinator == null && docusignRecipient == null) {
            throw new IllegalArgumentException(
                'No recipients found on the Client Case record. '
                + 'Please ensure Service Coordinator and Docusign Recipient #1 are populated.'
            );
        }

        // ─── Build JSON body ─────────────────────────────────────────────────────────
        String jsonBody = buildEnvelopeJson(
            sortedIds,
            docNames,
            envelopeSubject,
            envelopeBody,
            serviceCoordinator,
            docusignRecipient,
            recordId
        );

        // ─── Call the Docusign REST API ───────────────────────────────────────────────
        String endpoint = NAMED_CREDENTIAL
            + String.format(ENVELOPES_PATH, new List<Object>{ accountId });

        HttpRequest httpReq = new HttpRequest();
        httpReq.setEndpoint(endpoint);
        httpReq.setMethod('POST');
        httpReq.setHeader('Content-Type', 'application/json');
        httpReq.setHeader('Accept', 'application/json');
        httpReq.setBody(jsonBody);

        Http http = new Http();
        HttpResponse httpResp = http.send(httpReq);

        Integer statusCode = httpResp.getStatusCode();
        String responseBody = httpResp.getBody();

        if (statusCode == 200 || statusCode == 201) {
            // Parse envelopeId from response
            Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
            Object envelopeIdObj = responseMap.get('envelopeId');
            if (envelopeIdObj == null) {
                throw new CalloutException(
                    'Docusign API returned success but no envelopeId in response: ' + responseBody
                );
            }
            return String.valueOf(envelopeIdObj);
        } else {
            // Try to extract a meaningful error message from the response JSON
            String errorDetail = parseDocusignError(responseBody);
            throw new CalloutException(
                'Docusign API returned HTTP ' + statusCode + ': ' + errorDetail
            );
        }
    }

    // ─────────────────────────────────────────────────────────────────────────────────
    // PRIVATE HELPERS
    // ─────────────────────────────────────────────────────────────────────────────────

    /**
     * @description Resolves a recipient's name and email from a Contact or User record.
     *              When applySmsPlaceholder is true and the record has no email,
     *              the SMS_PLACEHOLDER_EMAIL constant is used instead.
     */
    private static RecipientInfo resolveRecipient(
        Id recipientId,
        String roleName,
        Integer routingOrder,
        String smsPhone,
        Boolean applySmsPlaceholder
    ) {
        String objectType = recipientId.getSObjectType().getDescribe().getName();
        String name;
        String email;

        if (objectType == 'Contact') {
            Contact c = [SELECT Id, Name, Email FROM Contact WHERE Id = :recipientId LIMIT 1];
            name  = c.Name;
            email = c.Email;
        } else if (objectType == 'User') {
            User u = [SELECT Id, Name, Email FROM User WHERE Id = :recipientId LIMIT 1];
            name  = u.Name;
            email = u.Email;
        } else {
            throw new IllegalArgumentException(
                'Unsupported recipient type: ' + objectType + '. Expected Contact or User.'
            );
        }

        if (String.isBlank(email)) {
            if (applySmsPlaceholder) {
                email = SMS_PLACEHOLDER_EMAIL;
            } else {
                throw new IllegalArgumentException(
                    'No email found for ' + roleName + ' (' + name + '). '
                    + 'Please ensure the recipient has a valid email address.'
                );
            }
        }

        RecipientInfo info = new RecipientInfo();
        info.name         = name;
        info.email        = email;
        info.roleName     = roleName;
        info.routingOrder = routingOrder;
        info.smsPhone     = applySmsPlaceholder ? smsPhone : null;
        return info;
    }

    /**
     * @description Constructs the full envelope JSON payload for the Docusign REST API.
     *              Uses compositeTemplates so that all selected templates are merged into
     *              a single envelope, matching what the dfsle Toolkit does internally.
     *
     *              Docusign compositeTemplates reference:
     *              https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/
     */
    @TestVisible
    private static String buildEnvelopeJson(
        List<String> sortedIds,
        List<String> docNames,
        String envelopeSubject,
        String envelopeBody,
        RecipientInfo serviceCoordinator,
        RecipientInfo docusignRecipient,
        String sourceRecordId
    ) {
        // Build the recipients object (shared across all compositeTemplates)
        // Each recipient gets a unique recipientId (sequential string integer).
        List<Object> signers = new List<Object>();
        Integer recipientIdCounter = 1;

        if (serviceCoordinator != null) {
            signers.add(buildSignerMap(serviceCoordinator, String.valueOf(recipientIdCounter++)));
        }
        if (docusignRecipient != null) {
            signers.add(buildSignerMap(docusignRecipient, String.valueOf(recipientIdCounter++)));
        }

        Map<String, Object> recipientsMap = new Map<String, Object>{
            'signers' => signers
        };

        // Build compositeTemplates array — one entry per document (template ID + label)
        List<Object> compositeTemplates = new List<Object>();
        for (Integer i = 0; i < sortedIds.size(); i++) {
            String templateId = sortedIds[i];
            String docLabel   = docNames[i];

            Map<String, Object> serverTemplate = new Map<String, Object>{
                'sequence'   => '1',
                'templateId' => templateId
            };

            // Inline template overrides — set the document label and wire up role assignments
            List<Object> inlineRecipientSigners = new List<Object>();
            Integer irCounter = 1;
            if (serviceCoordinator != null) {
                inlineRecipientSigners.add(new Map<String, Object>{
                    'recipientId' => String.valueOf(irCounter++),
                    'roleName'    => serviceCoordinator.roleName,
                    'name'        => serviceCoordinator.name,
                    'email'       => serviceCoordinator.email,
                    'routingOrder' => String.valueOf(serviceCoordinator.routingOrder)
                });
            }
            if (docusignRecipient != null) {
                Map<String, Object> drSigner = new Map<String, Object>{
                    'recipientId' => String.valueOf(irCounter++),
                    'roleName'    => docusignRecipient.roleName,
                    'name'        => docusignRecipient.name,
                    'email'       => docusignRecipient.email,
                    'routingOrder' => String.valueOf(docusignRecipient.routingOrder)
                };
                // Add SMS additionalNotifications for this recipient
                if (String.isNotBlank(docusignRecipient.smsPhone)) {
                    String[] phoneParts = parsePhone(docusignRecipient.smsPhone);
                    drSigner.put('additionalNotifications', new List<Object>{
                        new Map<String, Object>{
                            'secondaryDeliveryMethod' => 'SMS',
                            'phoneNumber' => new Map<String, Object>{
                                'countryCode' => phoneParts[0],
                                'number'      => phoneParts[1]
                            }
                        }
                    });
                }
                inlineRecipientSigners.add(drSigner);
            }

            Map<String, Object> inlineTemplate = new Map<String, Object>{
                'sequence'   => '2',
                'recipients' => new Map<String, Object>{
                    'signers' => inlineRecipientSigners
                },
                'document' => new Map<String, Object>{
                    'name'           => docLabel,
                    'documentId'     => String.valueOf(i + 1),
                    'transformPdfFields' => 'true'
                }
            };

            compositeTemplates.add(new Map<String, Object>{
                'serverTemplates'  => new List<Object>{ serverTemplate },
                'inlineTemplates'  => new List<Object>{ inlineTemplate },
                'recipients'       => recipientsMap
            });
        }

        Map<String, Object> envelopeDefinition = new Map<String, Object>{
            'status'             => 'sent',
            'emailSubject'       => envelopeSubject,
            'emailBlurb'         => envelopeBody,
            'compositeTemplates' => compositeTemplates
        };

        return JSON.serialize(envelopeDefinition);
    }

    /**
     * @description Builds the signer map for a recipient in the envelope-level recipients object.
     *              Adds additionalNotifications for SMS recipients.
     */
    private static Map<String, Object> buildSignerMap(RecipientInfo info, String recipientId) {
        Map<String, Object> signer = new Map<String, Object>{
            'recipientId'  => recipientId,
            'name'         => info.name,
            'email'        => info.email,
            'roleName'     => info.roleName,
            'routingOrder' => String.valueOf(info.routingOrder)
        };

        if (String.isNotBlank(info.smsPhone)) {
            String[] phoneParts = parsePhone(info.smsPhone);
            signer.put('additionalNotifications', new List<Object>{
                new Map<String, Object>{
                    'secondaryDeliveryMethod' => 'SMS',
                    'phoneNumber' => new Map<String, Object>{
                        'countryCode' => phoneParts[0],
                        'number'      => phoneParts[1]
                    }
                }
            });
        }

        return signer;
    }

    /**
     * @description Parses a phone number into [countryCode, nationalNumber].
     *              Accepts E.164 format (+15551234567) or a bare 10-digit number.
     *              Returns [DEFAULT_COUNTRY_CODE, strippedNumber] for bare numbers.
     *
     * @param phone Raw phone string from the flow input
     * @return String array: index 0 = country code, index 1 = national number (digits only)
     */
    @TestVisible
    private static String[] parsePhone(String phone) {
        if (String.isBlank(phone)) {
            return new String[]{ DEFAULT_COUNTRY_CODE, '' };
        }

        String stripped = phone.replaceAll('[^\\d+]', ''); // keep digits and leading +

        if (stripped.startsWith('+')) {
            // E.164: determine country code length heuristically
            // We only support 1-digit (+1) and 2-digit (+XX) country codes for now
            String digits = stripped.substring(1); // remove leading +
            if (digits.length() == 11 && digits.startsWith('1')) {
                // +1XXXXXXXXXX  (North American Numbering Plan)
                return new String[]{ '1', digits.substring(1) };
            } else if (digits.length() == 12) {
                // +XXXXXXXXXXXX  (2-digit country code + 10 digit number)
                return new String[]{ digits.left(2), digits.substring(2) };
            } else {
                // Fall back: treat first digit(s) as country code not supported; use full digits
                return new String[]{ DEFAULT_COUNTRY_CODE, digits };
            }
        }

        // Bare digits — assume DEFAULT_COUNTRY_CODE
        return new String[]{ DEFAULT_COUNTRY_CODE, stripped };
    }

    /**
     * @description Attempts to extract a human-readable error message from a Docusign
     *              REST API error response JSON.
     * @param responseBody Raw HTTP response body
     * @return Error string with errorCode and message if available, otherwise raw body
     */
    private static String parseDocusignError(String responseBody) {
        if (String.isBlank(responseBody)) {
            return '(no response body)';
        }
        try {
            Map<String, Object> errorMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
            String errorCode = errorMap.containsKey('errorCode') ? String.valueOf(errorMap.get('errorCode')) : null;
            String message   = errorMap.containsKey('message')   ? String.valueOf(errorMap.get('message'))   : null;
            if (String.isNotBlank(errorCode) || String.isNotBlank(message)) {
                return (errorCode != null ? '[' + errorCode + '] ' : '') + (message != null ? message : '');
            }
        } catch (Exception ex) {
            // Non-JSON response — return raw body below
        }
        return responseBody.left(500); // cap at 500 chars to avoid log spam
    }

    // ─────────────────────────────────────────────────────────────────────────────────
    // INNER CLASSES
    // ─────────────────────────────────────────────────────────────────────────────────

    /**
     * @description Lightweight holder for resolved recipient data.
     */
    private class RecipientInfo {
        String  name;
        String  email;
        String  roleName;
        Integer routingOrder;
        String  smsPhone;     // null for email recipients; phone string for SMS recipients
    }
}
