/**
 * @description Combines multiple Docusign templates into a single composite envelope
 *              using the dfsle Apex Toolkit (Docusign for Salesforce managed package).
 *              Recipients are resolved from Client_Case__c lookup fields.
 * @author Paul Huliganga
 * @date 2026-02-25
 */
global with sharing class DocusignCompositeEnvelopeBuilder {
    
    // ============================================================
    // CONFIGURATION: Update these constants if field/role names change
    // ============================================================
    
    // API names of the lookup fields on Client_Case__c that point to recipient records
    // These are the "Select Lookup Field" values from the Docusign template recipient config
    private static final String FIELD_SERVICE_COORDINATOR = 'Service_Coordinator__c';
    private static final String FIELD_DOCUSIGN_RECIPIENT  = 'Docusign_Recipient_1__c';
    
    // Role names must match EXACTLY what's configured in the Docusign templates
    private static final String ROLE_SERVICE_COORDINATOR  = 'Service Coordinator';
    private static final String ROLE_DOCUSIGN_RECIPIENT   = 'Docusign Recipient #1';

    // ============================================================
    // MULTI-COPY TEMPLATE: Update this if the template name changes.
    // This is matched against the dfsle__EnvelopeConfiguration__c Name
    // field using a case-insensitive contains check.
    // Both English and Spanish versions share this base name.
    // ============================================================
    @TestVisible
    private static final String MULTI_COPY_TEMPLATE_NAME = 'Authorization to Release Information';

    // ============================================================
    // SMS DELIVERY: Placeholder email used when the primary recipient
    // (Docusign Recipient #1) has no email address and SMS delivery is
    // requested via recipientSmsPhone.  Docusign requires an email on
    // every recipient even when dfsle.Recipient.withSmsDelivery() is used;
    // this placeholder satisfies that requirement without routing any
    // actual email — delivery occurs entirely via SMS.
    // ============================================================
    @TestVisible
    private static final String SMS_PLACEHOLDER_EMAIL = 'placeholder_email@docusign.com';
    
    @InvocableMethod(
        label='Send Composite Docusign Envelope' 
        description='Combines multiple Docusign templates into a single envelope using dfsle Apex Toolkit'
        category='Docusign'
    )
    public static List<DocusignEnvelopeResult> sendCompositeEnvelope(List<DocusignEnvelopeRequest> requests) {
        List<DocusignEnvelopeResult> results = new List<DocusignEnvelopeResult>();
        
        if (requests == null || requests.isEmpty()) {
            return buildErrorResult('No request provided');
        }
        
        DocusignEnvelopeRequest req = requests[0];
        DocusignEnvelopeResult result = new DocusignEnvelopeResult();
        
        try {
            // Validate request
            DocusignEnvelopeRequestHandler.validateRequest(req);
            
            // Create empty envelope linked to the source record
            dfsle.Envelope myEnvelope = dfsle.EnvelopeService.getEmptyEnvelope(
                new dfsle.Entity(req.recordId)
            );
            
            // Expand multi-copy templates before deduplication.
            // If the user selected the Authorization to Release Information template and
            // requested more than 1 copy, insert additional copies of its template ID into
            // the list now so the deduplication step handles all IDs uniformly.
            List<String> expandedTemplateIds = new List<String>(req.templateIds);
            Integer copies = (req.authReleaseFormCopies != null && req.authReleaseFormCopies > 1)
                ? Math.min(req.authReleaseFormCopies, 3)
                : 1;
            if (copies > 1) {
                // Find which template ID(s) correspond to the multi-copy template
                List<String> multiCopyIds = new List<String>();
                for (dfsle__EnvelopeConfiguration__c config : [
                    SELECT dfsle__DocuSignId__c
                    FROM dfsle__EnvelopeConfiguration__c
                    WHERE dfsle__DocuSignId__c IN :req.templateIds
                    AND Name LIKE :('%' + MULTI_COPY_TEMPLATE_NAME + '%')
                ]) {
                    multiCopyIds.add(config.dfsle__DocuSignId__c);
                }
                // Add (copies - 1) additional entries for each matched template ID
                for (String multiId : multiCopyIds) {
                    for (Integer i = 1; i < copies; i++) {
                        expandedTemplateIds.add(multiId);
                    }
                }
            }
            
            // Build document list from templates.
            // NOTE: We intentionally do NOT deduplicate here so that multiple copies of
            // the same template ID produce distinct documents in the envelope.
            // We sort by label after resolving names instead.
            List<String> sortedTemplateIds = new List<String>(expandedTemplateIds);
            sortedTemplateIds.sort();
            
            // Query template names for document labels (shows in Docusign Status)
            // Uses Short_Name__c if populated, otherwise falls back to Name (with language suffix stripped)
            Map<String, String> templateNames = new Map<String, String>();
            Map<String, String> templateShortNames = new Map<String, String>();
            for (dfsle__EnvelopeConfiguration__c config : [
                SELECT dfsle__DocuSignId__c, Name, Short_Name__c
                FROM dfsle__EnvelopeConfiguration__c
                WHERE dfsle__DocuSignId__c IN :sortedTemplateIds
            ]) {
                templateNames.put(config.dfsle__DocuSignId__c, config.Name);
                if (String.isNotBlank(config.Short_Name__c)) {
                    templateShortNames.put(config.dfsle__DocuSignId__c, config.Short_Name__c);
                }
            }
            
            List<dfsle.Document> documents = new List<dfsle.Document>();
            List<String> docNames = new List<String>();
            // Use a list of label+id pairs to correctly handle duplicate template IDs
            // (e.g. multiple copies of Authorization to Release Information)
            List<String[]> labelIdPairs = new List<String[]>();
            Map<String, Integer> labelCounters = new Map<String, Integer>();
            for (String templateId : sortedTemplateIds) {
                String label;
                if (templateShortNames.containsKey(templateId)) {
                    label = templateShortNames.get(templateId);
                } else if (templateNames.containsKey(templateId)) {
                    label = stripLanguageSuffix(templateNames.get(templateId));
                } else {
                    label = templateId;
                }
                // If the same label appears more than once, append " (Copy N)" to distinguish
                if (labelCounters.containsKey(label)) {
                    Integer count = labelCounters.get(label) + 1;
                    labelCounters.put(label, count);
                    label = label + ' (Copy ' + count + ')';
                } else {
                    labelCounters.put(label, 1);
                }
                labelIdPairs.add(new String[]{ label, templateId });
                docNames.add(label);
            }
            // Sort by label for consistent ordering
            docNames.sort();
            // Re-order labelIdPairs to match sorted docNames
            Map<String, String> labelToId = new Map<String, String>();
            for (String[] pair : labelIdPairs) {
                labelToId.put(pair[0], pair[1]);
            }
            List<String> sortedIds = new List<String>();
            for (String label : docNames) {
                sortedIds.add(labelToId.get(label));
            }
            for (Integer i = 0; i < sortedIds.size(); i++) {
                String templateId = sortedIds[i];
                String label = docNames[i];
                documents.add(
                    dfsle.Document.fromTemplate(
                        dfsle.UUID.parse(templateId),
                        label
                    )
                );
            }
            myEnvelope = myEnvelope.withDocuments(documents);
            
            // Build a deduplicated display list for the email subject and body.
            // Where a template appears more than once (multi-copy), show the base label
            // once with a " [x N]" count suffix, e.g. "Authorization to Release Information [x 3]".
            // This keeps the subject and body clean while the envelope still contains all copies.
            Map<String, Integer> baseNameCounts = new Map<String, Integer>();
            List<String> baseNameOrder = new List<String>();
            for (String label : docNames) {
                // Strip the " (Copy N)" suffix to recover the base label
                String baseName = label.replaceAll(' \\(Copy \\d+\\)$', '');
                if (!baseNameCounts.containsKey(baseName)) {
                    baseNameCounts.put(baseName, 0);
                    baseNameOrder.add(baseName);
                }
                baseNameCounts.put(baseName, baseNameCounts.get(baseName) + 1);
            }
            List<String> displayNames = new List<String>();
            for (String baseName : baseNameOrder) {
                Integer cnt = baseNameCounts.get(baseName);
                displayNames.add(cnt > 1 ? baseName + ' (' + cnt + ')' : baseName);
            }

            // Set combined template names as the envelope document name
            // (shows in Docusign Status "Document Name" column)
            String combinedName = String.join(displayNames, ', ');
            if (combinedName.length() > 255) {
                combinedName = combinedName.left(252) + '...';
            }
            // Use combined name as the first document label so it appears in Status
            if (!documents.isEmpty()) {
                documents[0] = dfsle.Document.fromTemplate(
                    dfsle.UUID.parse(sortedIds[0]),
                    combinedName
                );
                myEnvelope = myEnvelope.withDocuments(documents);
            }
            
            // Resolve recipients from Client_Case__c lookup fields
            List<dfsle.Recipient> recipients = resolveRecipients(req.recordId, req.recipientSmsPhone);
            myEnvelope = myEnvelope.withRecipients(recipients);
            
            // Set envelope subject to combined display names (deduplicated, with copy counts).
            // Query for EmailMessage__c — use a deduplicated set of template IDs so each
            // template's body text is included only once even when multi-copy is in effect.
            Set<String> uniqueTemplateIds = new Set<String>(sortedIds);
            Map<String, String> templateBodies = new Map<String, String>();
            for (dfsle__EnvelopeConfiguration__c config : [
                SELECT dfsle__DocuSignId__c, dfsle__EmailMessage__c
                FROM dfsle__EnvelopeConfiguration__c
                WHERE dfsle__DocuSignId__c IN :uniqueTemplateIds
            ]) {
                if (String.isNotBlank(config.dfsle__EmailMessage__c)) {
                    templateBodies.put(config.dfsle__DocuSignId__c, config.dfsle__EmailMessage__c);
                }
            }
            // Build body using one entry per unique template ID (preserving sorted order)
            Set<String> bodyIdsAdded = new Set<String>();
            List<String> bodyParts = new List<String>();
            for (String templateId : sortedIds) {
                if (templateBodies.containsKey(templateId) && !bodyIdsAdded.contains(templateId)) {
                    bodyParts.add(templateBodies.get(templateId));
                    bodyIdsAdded.add(templateId);
                }
            }
            String envelopeSubject = combinedName;
            // Truncate subject to 100 characters maximum as required by Docusign
            if (envelopeSubject.length() > 100) {
                envelopeSubject = envelopeSubject.left(97) + '...';
            }
            String envelopeBody = bodyParts.isEmpty() ? '' : String.join(bodyParts, '\n\n');
            myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody);
            
            // Send the envelope
            myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true);
            
            // Success
            result.envelopeId = String.valueOf(myEnvelope.docuSignId);
            result.success = true;
            result.errorMessage = null;
            
            logResult(sortedTemplateIds.size(), result.envelopeId, 'Success (' + String.join(displayNames, ', ') + ')', null);
            
        } catch (Exception e) {
            result.success = false;
            result.errorMessage = e.getMessage();
            result.envelopeId = null;
            
            logResult(
                req.templateIds != null ? req.templateIds.size() : 0,
                null, 'Error',
                e.getMessage() + '\n' + e.getStackTraceString()
            );
            
            if (e instanceof System.LimitException) {
                throw e;
            }
        }
        
        results.add(result);
        return results;
    }
    
    /**
     * @description Resolves recipients from Client_Case__c lookup fields.
     *              Queries the case record and related contacts to get name/email.
     * @param recordId The Client_Case__c record ID
     * @param smsPhone Optional SMS phone for the primary recipient. When provided,
     *                 the Docusign Recipient #1 is configured for SMS delivery via
     *                 dfsle.Recipient.withSmsDelivery() and a placeholder email is
     *                 substituted if the recipient has no email address.
     * @return List of dfsle.Recipient objects with role mappings
     */
    private static List<dfsle.Recipient> resolveRecipients(String recordId, String smsPhone) {
        // Query the Client_Case__c record with recipient lookup fields
        // NOTE: Adjust field API names if they differ in your org
        String query = 'SELECT Id, '
            + FIELD_SERVICE_COORDINATOR + ', '
            + FIELD_DOCUSIGN_RECIPIENT
            + ' FROM Client_Case__c WHERE Id = :recordId LIMIT 1';
        
        Client_Case__c caseRecord = Database.query(query);
        
        List<dfsle.Recipient> recipients = new List<dfsle.Recipient>();
        Integer routingOrder = 1;
        
        // Recipient 1: Service Coordinator (always email delivery)
        Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
        if (serviceCoordinatorId != null) {
            recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId, null));
        }
        
        // Recipient 2: Docusign Recipient #1 (SMS delivery when smsPhone is provided)
        Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT);
        if (docusignRecipientId != null) {
            recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId, smsPhone));
        }
        
        if (recipients.isEmpty()) {
            throw new IllegalArgumentException('No recipients found on the Client Case record. '
                + 'Please ensure Service Coordinator and Docusign Recipient #1 are populated.');
        }
        
        return recipients;
    }
    
    /**
     * @description Builds a dfsle.Recipient from a Contact/User lookup ID.
     *              When smsPhone is provided the recipient is configured for SMS
     *              delivery via dfsle.Recipient.withSmsDelivery(). If the recipient
     *              has no email address and SMS delivery is requested, SMS_PLACEHOLDER_EMAIL
     *              is substituted — Docusign requires an email field on every recipient
     *              even when delivery is via SMS.
     * @param recipientId    The Contact or User record ID
     * @param roleName       The Docusign template role name (must match exactly)
     * @param routingOrder   Signing order
     * @param sourceRecordId The source Client_Case__c record ID
     * @param smsPhone       Optional phone number for SMS delivery. Null for email delivery.
     * @return dfsle.Recipient configured for the role
     */
    private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId, String smsPhone) {
        // Determine if this is a Contact or User
        String objectType = recipientId.getSObjectType().getDescribe().getName();
        
        String recipientName;
        String recipientEmail;
        
        if (objectType == 'Contact') {
            Contact c = [SELECT Id, Name, Email FROM Contact WHERE Id = :recipientId LIMIT 1];
            recipientName = c.Name;
            recipientEmail = c.Email;
        } else if (objectType == 'User') {
            User u = [SELECT Id, Name, Email FROM User WHERE Id = :recipientId LIMIT 1];
            recipientName = u.Name;
            recipientEmail = u.Email;
        } else {
            throw new IllegalArgumentException('Unsupported recipient type: ' + objectType 
                + '. Expected Contact or User.');
        }
        
        if (String.isBlank(recipientEmail)) {
            if (String.isNotBlank(smsPhone)) {
                // SMS delivery requested — substitute placeholder email so the dfsle
                // Toolkit can create the recipient. Actual delivery is via SMS only.
                recipientEmail = SMS_PLACEHOLDER_EMAIL;
            } else {
                throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
                    + 'Please ensure the recipient has a valid email address.');
            }
        }
        
        dfsle.Recipient recipient = dfsle.Recipient.fromSource(
            recipientName,
            recipientEmail,
            null,                                    // phone (not used — SMS delivery set below)
            roleName,                                // must match template role exactly
            new dfsle.Entity(sourceRecordId)          // source record for merge fields
        );

        // Enable SMS delivery when a phone number is provided
        if (String.isNotBlank(smsPhone)) {
            recipient = recipient.withSmsDelivery(smsPhone);
        }

        return recipient;
    }
    
    private static void logResult(Integer templateCount, String envelopeId, String status, String errorMessage) {
        System.debug(LoggingLevel.INFO, '=== Docusign Composite Envelope ===');
        System.debug(LoggingLevel.INFO, 'Templates: ' + templateCount);
        System.debug(LoggingLevel.INFO, 'Envelope ID: ' + envelopeId);
        System.debug(LoggingLevel.INFO, 'Status: ' + status);
        if (String.isNotBlank(errorMessage)) {
            System.debug(LoggingLevel.ERROR, 'Error: ' + errorMessage);
        }
    }
    
    /**
     * @description Strips language suffixes like " - English" or " - Spanish" from template names
     * @param name Template name
     * @return Cleaned template name
     */
    @TestVisible
    private static String stripLanguageSuffix(String name) {
        if (String.isBlank(name)) return name;
        // Remove common language suffixes (case-insensitive)
        String cleaned = name;
        for (String suffix : new List<String>{
            ' - English', ' - Spanish', ' - French',
            ' - Anglais', ' - Espagnol', ' - Français'
        }) {
            if (cleaned.endsWithIgnoreCase(suffix)) {
                cleaned = cleaned.left(cleaned.length() - suffix.length());
                break;
            }
        }
        return cleaned.trim();
    }
    
    private static List<DocusignEnvelopeResult> buildErrorResult(String errorMessage) {
        DocusignEnvelopeResult result = new DocusignEnvelopeResult();
        result.success = false;
        result.errorMessage = errorMessage;
        result.envelopeId = null;
        return new List<DocusignEnvelopeResult>{ result };
    }
}
