/**
 * @description Service class for Docusign REST API interactions
 * @author Paul Huliganga
 * @date 2026-02-23
 */
public with sharing class DocusignAPIService {
    
    private static final Integer TIMEOUT_MS = 120000; // 120 seconds
    private static final Integer MAX_RETRIES = 2;
    
    /**
     * @description Creates a composite envelope in Docusign
     * @param envelopeJSON JSON string containing envelope definition
     * @param creds Docusign credentials object
     * @return Envelope ID (GUID)
     * @throws CalloutException if API call fails
     */
    public static String createCompositeEnvelope(
        String envelopeJSON,
        DocusignCredentials creds
    ) {
        Long startTime = System.currentTimeMillis();
        
        // Build HTTP request
        HttpRequest req = buildCreateEnvelopeRequest(envelopeJSON, creds);
        
        // Make callout with retry logic
        HttpResponse res = executeWithRetry(req, MAX_RETRIES);
        
        Long duration = System.currentTimeMillis() - startTime;
        
        // Log request/response (sanitized)
        logAPICall(req, res, duration);
        
        // Parse response
        if (res.getStatusCode() == 201) {
            return parseEnvelopeId(res);
        } else {
            handleAPIError(res);
            return null; // Won't reach here, handleAPIError throws
        }
    }
    
    /**
     * @description Builds HTTP request for creating envelope
     * @param envelopeJSON JSON body
     * @param creds Credentials
     * @return Configured HttpRequest
     */
    @TestVisible
    private static HttpRequest buildCreateEnvelopeRequest(
        String envelopeJSON,
        DocusignCredentials creds
    ) {
        HttpRequest req = new HttpRequest();
        
        // Use Named Credential if available, otherwise build URL manually
        String endpoint = buildEnvelopeEndpoint(creds);
        req.setEndpoint(endpoint);
        
        req.setMethod('POST');
        req.setHeader('Authorization', 'Bearer ' + creds.getAccessToken());
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(envelopeJSON);
        req.setTimeout(TIMEOUT_MS);
        
        return req;
    }
    
    /**
     * @description Builds envelope endpoint URL
     * @param creds Docusign credentials
     * @return Full endpoint URL
     */
    private static String buildEnvelopeEndpoint(DocusignCredentials creds) {
        // Check if using Named Credential (starts with 'callout:')
        if (creds.getBaseUrl().startsWith('callout:')) {
            return creds.getBaseUrl() + '/accounts/' + creds.getAccountId() + '/envelopes';
        } else {
            return creds.getBaseUrl() + '/restapi/v2.1/accounts/' + creds.getAccountId() + '/envelopes';
        }
    }
    
    /**
     * @description Executes HTTP request with retry logic
     * @param req HTTP request
     * @param maxRetries Maximum number of retry attempts
     * @return HTTP response
     * @throws CalloutException if all retries fail
     */
    @TestVisible
    private static HttpResponse executeWithRetry(HttpRequest req, Integer maxRetries) {
        Http http = new Http();
        HttpResponse res;
        Integer attempt = 0;
        
        while (attempt <= maxRetries) {
            attempt++;
            
            try {
                res = http.send(req);
                
                // Success or non-retryable error
                if (res.getStatusCode() == 201 || !isRetryableError(res.getStatusCode())) {
                    return res;
                }
                
                // Rate limit - wait before retry
                if (res.getStatusCode() == 429 && attempt <= maxRetries) {
                    Integer waitMs = calculateBackoff(attempt);
                    System.debug('Rate limited, waiting ' + waitMs + 'ms before retry ' + attempt);
                    // Note: Apex doesn't have Thread.sleep, but in production this would be handled by Platform Events
                    // For now, just retry immediately
                }
                
            } catch (System.CalloutException e) {
                // Timeout or connection error
                if (attempt > maxRetries) {
                    throw new CalloutException('Docusign API callout failed after ' + maxRetries + ' retries: ' + e.getMessage());
                }
                System.debug('Callout failed (attempt ' + attempt + '): ' + e.getMessage());
            }
        }
        
        // All retries exhausted
        throw new CalloutException('Docusign API callout failed after ' + maxRetries + ' retries. Last status: ' + res.getStatusCode());
    }
    
    /**
     * @description Determines if an HTTP status code is retryable
     * @param statusCode HTTP status code
     * @return True if error is transient and should be retried
     */
    private static Boolean isRetryableError(Integer statusCode) {
        // 401 (token expired), 429 (rate limit), 500-599 (server errors)
        return statusCode == 401 || statusCode == 429 || (statusCode >= 500 && statusCode < 600);
    }
    
    /**
     * @description Calculates exponential backoff delay
     * @param attempt Retry attempt number
     * @return Delay in milliseconds
     */
    private static Integer calculateBackoff(Integer attempt) {
        // Exponential backoff: 2^attempt * 1000ms (1s, 2s, 4s, ...)
        return (Integer) Math.pow(2, attempt) * 1000;
    }
    
    /**
     * @description Parses envelope ID from successful response
     * @param res HTTP response
     * @return Envelope ID (GUID)
     */
    @TestVisible
    private static String parseEnvelopeId(HttpResponse res) {
        try {
            Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            String envelopeId = (String) responseMap.get('envelopeId');
            
            if (String.isBlank(envelopeId)) {
                throw new CalloutException('Envelope ID not found in Docusign response');
            }
            
            return envelopeId;
        } catch (Exception e) {
            throw new CalloutException('Failed to parse Docusign response: ' + e.getMessage());
        }
    }
    
    /**
     * @description Handles API error responses
     * @param res HTTP response with error
     * @throws CalloutException with detailed error message
     */
    @TestVisible
    private static void handleAPIError(HttpResponse res) {
        String errorMessage = 'Docusign API error [' + res.getStatusCode() + ']';
        
        try {
            Map<String, Object> errorBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            
            String errorCode = (String) errorBody.get('errorCode');
            String message = (String) errorBody.get('message');
            
            if (String.isNotBlank(errorCode)) {
                errorMessage += ' ' + errorCode;
            }
            if (String.isNotBlank(message)) {
                errorMessage += ': ' + message;
            }
        } catch (Exception e) {
            // Could not parse error body, use raw response
            errorMessage += ': ' + res.getBody();
        }
        
        // Map common errors to user-friendly messages
        errorMessage = enhanceErrorMessage(res.getStatusCode(), errorMessage);
        
        throw new CalloutException(errorMessage);
    }
    
    /**
     * @description Enhances error messages with user-friendly guidance
     * @param statusCode HTTP status code
     * @param originalMessage Original error message
     * @return Enhanced error message
     */
    private static String enhanceErrorMessage(Integer statusCode, String originalMessage) {
        switch on statusCode {
            when 400 {
                return originalMessage + ' - Check template IDs and request parameters.';
            }
            when 401 {
                return originalMessage + ' - Authentication failed. Check API credentials and access token.';
            }
            when 403 {
                return originalMessage + ' - User lacks permission to create envelopes.';
            }
            when 404 {
                return originalMessage + ' - One or more templates not found in Docusign account.';
            }
            when 429 {
                return originalMessage + ' - API rate limit exceeded. Please try again later.';
            }
            when 500, 503 {
                return originalMessage + ' - Docusign server error. Please try again.';
            }
            when else {
                return originalMessage;
            }
        }
    }
    
    /**
     * @description Logs API call details (sanitized, no credentials)
     * @param req HTTP request
     * @param res HTTP response
     * @param durationMs Duration in milliseconds
     */
    private static void logAPICall(HttpRequest req, HttpResponse res, Long durationMs) {
        System.debug(LoggingLevel.INFO, '=== Docusign API Call ===');
        System.debug(LoggingLevel.INFO, 'Method: ' + req.getMethod());
        System.debug(LoggingLevel.INFO, 'Endpoint: ' + sanitizeEndpoint(req.getEndpoint()));
        System.debug(LoggingLevel.INFO, 'Request Body Length: ' + req.getBody().length() + ' bytes');
        System.debug(LoggingLevel.INFO, 'Response Status: ' + res.getStatusCode() + ' ' + res.getStatus());
        System.debug(LoggingLevel.INFO, 'Response Body Length: ' + res.getBody().length() + ' bytes');
        System.debug(LoggingLevel.INFO, 'Duration: ' + durationMs + 'ms');
        
        // Only log full bodies in DEBUG mode (not in production)
        if (Test.isRunningTest()) {
            System.debug(LoggingLevel.FINEST, 'Request Body: ' + req.getBody());
            System.debug(LoggingLevel.FINEST, 'Response Body: ' + res.getBody());
        }
    }
    
    /**
     * @description Sanitizes endpoint URL for logging (removes account ID)
     * @param endpoint Full endpoint URL
     * @return Sanitized URL
     */
    private static String sanitizeEndpoint(String endpoint) {
        // Replace account ID with placeholder for security
        return endpoint.replaceAll('/accounts/[a-f0-9\\-]+/', '/accounts/{accountId}/');
    }
    
    /**
     * @description Custom exception for API errors
     */
    public class CalloutException extends Exception {}
}
