public class CLMDocGenCallout {

    // S1 demo environment
    private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
    private static final String CLM_NAMED_CRED_S1 = 'callout:CLMs1NamedCreds';
    private static final String CLM_DOWNLOAD_NAMED_CRED_S1 = 'callout:CLMs1Download';

    // UAT demo environment
    private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
    private static final String CLM_NAMED_CRED_UAT = 'callout:CLMuatNamedCreds';
    private static final String CLM_DOWNLOAD_NAMED_CRED_UAT = 'callout:CLMuatDownload';

    public static final Integer HTTP_TIMEOUT = 30000;

    /** Resolve the correct CLM Named Credential base. env: 'UAT' or 'S1'. */
    private static String clmNamedCredential(String env) {
        return env == 'S1' ? CLM_NAMED_CRED_S1 : CLM_NAMED_CRED_UAT;
    }

    private static String clmDownloadNamedCredential(String env) {
        return env == 'S1' ? CLM_DOWNLOAD_NAMED_CRED_S1 : CLM_DOWNLOAD_NAMED_CRED_UAT;
    }

    @TestVisible
    static String defaultAccountId(String env) {
        return env == 'S1' ? CLM_ACCOUNT_ID_S1 : CLM_ACCOUNT_ID_UAT;
    }

    @TestVisible
    static String extractAccountId(String resourceOrHref) {
        if (String.isBlank(resourceOrHref)) {
            return null;
        }

        Integer v2Index = resourceOrHref.indexOf('/v2/');
        if (v2Index < 0) {
            return null;
        }

        String afterV2 = resourceOrHref.substring(v2Index + 4);
        List<String> parts = afterV2.split('/');
        return parts.isEmpty() ? null : parts[0];
    }

    @TestVisible
    static String normalizeResourcePath(String resourceOrHref, String env) {
        return normalizeResourcePathWithAccountId(resourceOrHref, defaultAccountId(env));
    }

    @TestVisible
    static String normalizeResourcePathWithAccountId(String resourceOrHref, String fallbackAccountId) {
        if (String.isBlank(resourceOrHref)) {
            throw new IllegalArgumentException('resourceOrHref is required');
        }

        if (resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://')) {
            Integer pathStart = resourceOrHref.indexOf('/', resourceOrHref.indexOf('//') + 2);
            if (pathStart < 0) {
                throw new IllegalArgumentException('Unable to determine resource path from href: ' + resourceOrHref);
            }
            return resourceOrHref.substring(pathStart);
        }

        if (resourceOrHref.startsWith('/v2/')) {
            return resourceOrHref;
        }

        if (resourceOrHref.startsWith('v2/')) {
            return '/' + resourceOrHref;
        }

        if (resourceOrHref.startsWith('/')) {
            return '/v2/' + fallbackAccountId + resourceOrHref;
        }

        return '/v2/' + fallbackAccountId + '/' + resourceOrHref;
    }

    public static String buildEndpointForResource(String resourceOrHref, String env) {
        return clmNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
    }

    public static String buildEndpointForResource(String resourceOrHref, String accountId, String apiNamedCredential) {
        return 'callout:' + apiNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
    }

    public static String buildDownloadEndpointForResource(String resourceOrHref, String env) {
        return clmDownloadNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
    }

    public static String buildDownloadEndpointForResource(String resourceOrHref, String accountId, String downloadNamedCredential) {
        return 'callout:' + downloadNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
    }

    /** Defaults to UAT environment. */
    public static CLMDocGenResponse generateDocument(
        String caseId,
        String templateDocHref,
        String destinationFolderHref,
        String destinationDocName
    ) {
        return generateDocument(caseId, templateDocHref, destinationFolderHref, destinationDocName, 'UAT');
    }

    /**
     * Generate a merged document via CLM documentxmlmergetasks (no user interaction).
     * @param caseId                Appraiser_Case__c record Id
     * @param templateDocHref       Full CLM Href URL of the template .docx document
     * @param destinationFolderHref Full CLM Href URL of the destination folder
     * @param destinationDocName    Filename for the generated document, e.g. "Review_AC-00001.docx"
     * @param env                   'UAT' (apiuatna11.springcm.com) or 'S1' (api.s1.us.clm.demo.docusign.net)
     */
    public static CLMDocGenResponse generateDocument(
        String caseId,
        String templateDocHref,
        String destinationFolderHref,
        String destinationDocName,
        String env
    ) {
        return generateDocument(
            caseId,
            templateDocHref,
            destinationFolderHref,
            destinationDocName,
            env,
            defaultAccountId(env),
            clmNamedCredential(env).substringAfter('callout:')
        );
    }

    public static CLMDocGenResponse generateDocument(
        String caseId,
        String templateDocHref,
        String destinationFolderHref,
        String destinationDocName,
        String env,
        String configuredAccountId,
        String apiNamedCredential
    ) {
        Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
        String accountId = firstNonBlank(
            extractAccountId(templateDocHref),
            extractAccountId(destinationFolderHref),
            configuredAccountId,
            defaultAccountId(env)
        );
        Map<String, Object> requestBody = buildRequestBodyMap(
            casePayload,
            templateDocHref,
            destinationFolderHref,
            destinationDocName
        );

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(HTTP_TIMEOUT);
        req.setBody(JSON.serialize(requestBody));

        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            return parseTaskResponse(res);
        } catch (Exception ex) {
            return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
        }
    }

    public static String buildDataXmlForCase(String caseId) {
        return buildDataXml(AppraiserCasePayloadBuilder.buildPayload(caseId));
    }

    public static String buildRequestBodyJson(
        String caseId,
        String templateDocHref,
        String destinationFolderHref,
        String destinationDocName
    ) {
        return JSON.serializePretty(
            buildRequestBodyMap(
                AppraiserCasePayloadBuilder.buildPayload(caseId),
                templateDocHref,
                destinationFolderHref,
                destinationDocName
            )
        );
    }

    public static String buildDocumentXmlMergeTasksUrl(
        String templateDocHref,
        String destinationFolderHref,
        String env,
        String configuredAccountId
    ) {
        String accountId = firstNonBlank(
            extractAccountId(templateDocHref),
            extractAccountId(destinationFolderHref),
            configuredAccountId,
            defaultAccountId(env)
        );
        String baseUrl = firstNonBlank(
            extractBaseUrl(templateDocHref),
            extractBaseUrl(destinationFolderHref),
            null,
            defaultBaseUrl(env)
        );
        return baseUrl + '/v2/' + accountId + '/documentxmlmergetasks';
    }

    /** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
    public static CLMDocGenResponse getTaskStatus(String taskId) {
        return getTaskStatus(taskId, 'UAT');
    }

    public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
        return getTaskStatus(taskId, env, defaultAccountId(env), clmNamedCredential(env).substringAfter('callout:'));
    }

    public static CLMDocGenResponse getTaskStatus(String taskId, String env, String configuredAccountId, String apiNamedCredential) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
        req.setMethod('GET');
        req.setTimeout(HTTP_TIMEOUT);
        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            return parseTaskResponse(res);
        } catch (Exception ex) {
            return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
        }
    }

    /** Probe any CLM resource path for debugging. env: 'UAT' or 'S1'. */
    public static String probe(String resource) {
        return probe(resource, 'UAT');
    }

    public static String probe(String resource, String env) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(buildEndpointForResource(resource, env));
        req.setMethod('GET');
        req.setTimeout(HTTP_TIMEOUT);
        HttpResponse res = new Http().send(req);
        return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
    }

    public static DownloadedDocument downloadDocument(String resourceOrHref, String env) {
        return downloadDocument(
            resourceOrHref,
            env,
            defaultAccountId(env),
            clmDownloadNamedCredential(env).substringAfter('callout:')
        );
    }

    public static DownloadedDocument downloadDocument(String resourceOrHref, String env, String configuredAccountId, String downloadNamedCredential) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(buildDownloadEndpointForResource(resourceOrHref, configuredAccountId, downloadNamedCredential));
        req.setMethod('GET');
        req.setTimeout(HTTP_TIMEOUT);

        HttpResponse res = new Http().send(req);
        Integer statusCode = res.getStatusCode();
        if (statusCode < 200 || statusCode >= 300) {
            throw new CalloutException('CLM download error (HTTP ' + statusCode + '): ' + res.getBody());
        }

        DownloadedDocument document = new DownloadedDocument();
        document.body = res.getBodyAsBlob();
        document.contentType = res.getHeader('Content-Type');
        document.fileName = extractFileName(res, resourceOrHref, document.contentType);
        return document;
    }

    /**
     * Build the DataXML string from the case payload.
     * Flat fields become direct child elements of <TemplateFieldData>.
     * DeficiencyList items expand into numbered elements:
     *   Deficiency_1_Number, Deficiency_1_Description, Deficiency_1_Resolution, etc.
     */
    private static String buildDataXml(Map<String, Object> payload) {
        String xml = '<TemplateFieldData>';

        // Emit flat fields first
        for (String key : payload.keySet()) {
            if (key == 'DeficiencyList') continue;
            xml += '<' + key + '>' + escapeXml(safeValue(payload.get(key))) + '</' + key + '>';
        }

        // Emit DeficiencyList as a nested list so templates can iterate dynamically
        List<Object> deficiencies = (List<Object>) payload.get('DeficiencyList');
        if (deficiencies != null && !deficiencies.isEmpty()) {
            xml += '<DeficiencyList>';
            for (Integer i = 0; i < deficiencies.size(); i++) {
                Map<String, Object> d = (Map<String, Object>) deficiencies[i];
                xml += '<Deficiency>';
                xml += '<Number>'      + escapeXml(safeValue(d.get('deficiencyNumber'))) + '</Number>';
                xml += '<Description>' + escapeXml(safeValue(d.get('description')))      + '</Description>';
                xml += '<Resolution>'  + escapeXml(safeValue(d.get('resolution')))       + '</Resolution>';
                xml += '<Reference>'   + escapeXml(safeValue(d.get('reference')))        + '</Reference>';
                xml += '</Deficiency>';
            }
            xml += '</DeficiencyList>';
            xml += '<DeficiencyCount>' + deficiencies.size() + '</DeficiencyCount>';
        }

        xml += '</TemplateFieldData>';
        return xml;
    }

    public static String prettyPrintXml(String xml) {
        if (String.isBlank(xml)) {
            return xml;
        }

        String normalized = xml
            .replace('><', '>\n<')
            .replace('\r\n', '\n')
            .replace('\r', '\n');

        List<String> lines = normalized.split('\n');
        List<String> formatted = new List<String>();
        Integer indent = 0;

        for (String rawLine : lines) {
            String line = rawLine == null ? '' : rawLine.trim();
            if (line == '') {
                continue;
            }

            Boolean isClosing = line.startsWith('</');
            Boolean isDeclaration = line.startsWith('<?') || line.startsWith('<!');
            Boolean isSelfClosing = line.endsWith('/>') || (line.contains('</') && line.indexOf('</') > 0);

            if (isClosing && indent > 0) {
                indent--;
            }

            formatted.add(repeatIndent(indent) + line);

            if (!isClosing && !isSelfClosing && !isDeclaration) {
                indent++;
            }
        }

        return String.join(formatted, '\n');
    }

    private static Map<String, Object> buildRequestBodyMap(
        Map<String, Object> casePayload,
        String templateDocHref,
        String destinationFolderHref,
        String destinationDocName
    ) {
        return new Map<String, Object>{
            'TemplateDocument' => new Map<String, Object>{
                'Href' => templateDocHref
            },
            'DataXML' => buildDataXml(casePayload),
            'DestinationDocumentName' => destinationDocName,
            'DestinationFolder' => new Map<String, Object>{
                'Href' => destinationFolderHref
            }
        };
    }

    private static String extractBaseUrl(String resourceOrHref) {
        if (String.isBlank(resourceOrHref)) {
            return null;
        }
        if (!(resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://'))) {
            return null;
        }

        Integer schemeIndex = resourceOrHref.indexOf('//');
        Integer pathStart = resourceOrHref.indexOf('/', schemeIndex + 2);
        return pathStart > 0 ? resourceOrHref.substring(0, pathStart) : resourceOrHref;
    }

    private static String defaultBaseUrl(String env) {
        return env == 'S1'
            ? 'https://api.s1.us.clm.demo.docusign.net'
            : 'https://apiuatna11.springcm.com';
    }

    private static String repeatIndent(Integer indent) {
        String value = '';
        for (Integer i = 0; i < indent; i++) {
            value += '  ';
        }
        return value;
    }

    private static String safeValue(Object val) {
        return val != null ? String.valueOf(val) : '';
    }

    private static String escapeXml(String s) {
        if (s == null) return '';
        return s.replace('&', '&amp;')
                .replace('<', '&lt;')
                .replace('>', '&gt;')
                .replace('"', '&quot;')
                .replace('\'', '&apos;');
    }

    private static CLMDocGenResponse parseTaskResponse(HttpResponse res) {
        Integer statusCode = res.getStatusCode();
        String body = res.getBody();
        if (statusCode >= 200 && statusCode < 300) {
            Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
            String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
            String status = firstString(m, new List<String>{ 'Status', 'State' });
            String taskId = href != null ? href.substringAfterLast('/') : null;
            String generatedDocumentUrl = findFirstDocumentHref(m);
            String generatedDocumentId = generatedDocumentUrl != null ? generatedDocumentUrl.substringAfterLast('/') : null;
            String message = 'Task status: ' + (String.isNotBlank(status) ? status : 'Unknown');
            return new CLMDocGenResponse(true, message, href, taskId, status, generatedDocumentUrl, generatedDocumentId, body);
        } else {
            return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null, null, null, null, body);
        }
    }

    private static String firstNonBlank(String firstValue, String secondValue, String thirdValue, String fallbackValue) {
        if (String.isNotBlank(firstValue)) {
            return firstValue;
        }
        if (String.isNotBlank(secondValue)) {
            return secondValue;
        }
        if (String.isNotBlank(thirdValue)) {
            return thirdValue;
        }
        return fallbackValue;
    }

    private static String firstString(Map<String, Object> source, List<String> keys) {
        for (String key : keys) {
            Object value = source.get(key);
            if (value != null) {
                String asString = String.valueOf(value);
                if (String.isNotBlank(asString)) {
                    return asString;
                }
            }
        }
        return null;
    }

    private static String findFirstDocumentHref(Object node) {
        if (node instanceof Map<String, Object>) {
            Map<String, Object> mapNode = (Map<String, Object>) node;
            String href = firstString(mapNode, new List<String>{ 'Href', 'Uri', 'Location' });
            if (String.isNotBlank(href) && href.contains('/documents/') && !href.contains('/documentxmlmergetasks/')) {
                return href;
            }

            for (Object value : mapNode.values()) {
                String nestedHref = findFirstDocumentHref(value);
                if (String.isNotBlank(nestedHref)) {
                    return nestedHref;
                }
            }
        } else if (node instanceof List<Object>) {
            for (Object item : (List<Object>) node) {
                String nestedHref = findFirstDocumentHref(item);
                if (String.isNotBlank(nestedHref)) {
                    return nestedHref;
                }
            }
        }

        return null;
    }

    private static String extractFileName(HttpResponse res, String resourceOrHref, String contentType) {
        String disposition = res.getHeader('Content-Disposition');
        if (String.isNotBlank(disposition)) {
            Integer marker = disposition.toLowerCase().indexOf('filename=');
            if (marker >= 0) {
                String candidate = disposition.substring(marker + 9).trim();
                if (candidate.startsWith('"') && candidate.endsWith('"') && candidate.length() >= 2) {
                    candidate = candidate.substring(1, candidate.length() - 1);
                }
                if (String.isNotBlank(candidate)) {
                    return candidate;
                }
            }
        }

        String baseName = String.isNotBlank(resourceOrHref) ? resourceOrHref.substringAfterLast('/') : 'generated-document';
        String extension = inferExtension(contentType);
        if (String.isNotBlank(extension) && !baseName.toLowerCase().endsWith(extension)) {
            return baseName + extension;
        }
        return baseName;
    }

    private static String inferExtension(String contentType) {
        if (String.isBlank(contentType)) {
            return '.docx';
        }

        String normalizedType = contentType.toLowerCase();
        if (normalizedType.contains('pdf')) {
            return '.pdf';
        }
        if (normalizedType.contains('wordprocessingml') || normalizedType.contains('officedocument')) {
            return '.docx';
        }
        if (normalizedType.contains('msword')) {
            return '.doc';
        }
        if (normalizedType.contains('json')) {
            return '.json';
        }
        return '';
    }

    public class CLMDocGenResponse {
        @AuraEnabled
        public Boolean success;
        @AuraEnabled
        public String message;
        @AuraEnabled
        public String documentUrl;
        @AuraEnabled
        public String documentId;
        @AuraEnabled
        public String taskStatus;
        @AuraEnabled
        public String generatedDocumentUrl;
        @AuraEnabled
        public String generatedDocumentId;
        @AuraEnabled
        public String taskDetailsJson;

        public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
            this(success, message, documentUrl, documentId, null, null, null, null);
        }

        public CLMDocGenResponse(
            Boolean success,
            String message,
            String documentUrl,
            String documentId,
            String taskStatus,
            String generatedDocumentUrl,
            String generatedDocumentId,
            String taskDetailsJson
        ) {
            this.success = success;
            this.message = message;
            this.documentUrl = documentUrl;
            this.documentId = documentId;
            this.taskStatus = taskStatus;
            this.generatedDocumentUrl = generatedDocumentUrl;
            this.generatedDocumentId = generatedDocumentId;
            this.taskDetailsJson = taskDetailsJson;
        }
    }

    public class DownloadedDocument {
        public Blob body;
        public String fileName;
        public String contentType;
    }
}
