/***********************************************************************************************************
 * Name         : ContentDocumentTriggerHelper
 * Purpose      : Implementation for methods called by ContentDocumentTriggerHandler
 **********************************************************************************************************/
public with sharing class ContentDocumentTriggerHelper {
    @TestVisible
    private static Integer numOfpublishedEvents;

    /**
     * @description Entry point for ContentDocument before delete handling.
     *   Queries ContentDocumentLink once, buckets the ContentDocuments being deleted by parent type
     *   (Opportunity vs AdvancePayment__c), then delegates each bucket to:
     *   - handleAWALOpps (AWAL Opp logic: addError + update HasAttachments__c)
     *   - handlePayments (AdvancePayment__c logic: addError + update HasAttachedFile__c)
     * @param oldMap Map<Id, ContentDocument> containing the ContentDocument records being deleted (Trigger.oldMap).
     */
    public static void handleBeforeDeleteByLinkedEntities(Map<Id, ContentDocument> oldMap) {
        List<ContentDocumentLink> cdls = [
            SELECT ContentDocumentId, LinkedEntityId
            FROM ContentDocumentLink
            WHERE ContentDocumentId IN :oldMap.keySet() AND LinkedEntityId != NULL
        ];
        if (cdls.isEmpty()) {
            return;
        }

        Map<Id, ContentDocument> oppCdMap = new Map<Id, ContentDocument>();
        Map<Id, ContentDocument> paymentCdMap = new Map<Id, ContentDocument>();

        List<ContentDocumentLink> oppCdls = new List<ContentDocumentLink>();
        List<ContentDocumentLink> paymentCdls = new List<ContentDocumentLink>();

        for (ContentDocumentLink cdl : cdls) {
            if (cdl.LinkedEntityId == null || !oldMap.containsKey(cdl.ContentDocumentId)) {
                continue;
            }

            if (cdl.LinkedEntityId.getSObjectType() == Opportunity.SObjectType) {
                oppCdls.add(cdl);
                oppCdMap.put(cdl.ContentDocumentId, oldMap.get(cdl.ContentDocumentId));
            } else if (cdl.LinkedEntityId.getSObjectType() == AdvancePayment__c.SObjectType) {
                paymentCdls.add(cdl);
                paymentCdMap.put(cdl.ContentDocumentId, oldMap.get(cdl.ContentDocumentId));
            }
        }

        if (!oppCdMap.isEmpty()) {
            handleAWALOpps(oppCdMap, oppCdls);
        }

        if (!paymentCdMap.isEmpty()) {
            handlePayments(paymentCdMap, paymentCdls);
        }
    }

    /**
     * @description Handles ContentDocument deletions for AWAL Opportunities:
     *   1) Blocks deletion (addError) when it would leave an AWAL Opportunity that requires the P&L tool
     *       (IsPAndLToolAttached__c = 'Yes') without any attached files.
     *   2) Updates Opportunity.HasAttachments__c to false when, after the deletable docs are removed,
     *       the Opportunity would have no remaining ContentDocumentLinks.
     * @param cdMap Map<Id, ContentDocument> containing ONLY the ContentDocuments being deleted that are linked to Opportunity.
     * @param oppCdls List<ContentDocumentLink> already queried, containing ONLY links for the docs being deleted to Opportunity.
     */
    public static void handleAWALOpps(Map<Id, ContentDocument> cdMap, List<ContentDocumentLink> oppCdls) {
        Map<Id, Set<Id>> oppIdToContentDocIds = new Map<Id, Set<Id>>();
        for (ContentDocumentLink cdl : oppCdls) {
            if (!oppIdToContentDocIds.containsKey(cdl.LinkedEntityId)) {
                oppIdToContentDocIds.put(cdl.LinkedEntityId, new Set<Id>());
            }
            oppIdToContentDocIds.get(cdl.LinkedEntityId).add(cdl.ContentDocumentId);
        }

        Map<Id, Opportunity> awalOppsById = new Map<Id, Opportunity>(
            [
                SELECT Id, IsPAndLToolAttached__c, HasAttachments__c
                FROM Opportunity
                WHERE Id IN :oppIdToContentDocIds.keySet() AND RecordTypeId = :OpportunityTriggerHelper.AWAL_RECTYPE_ID
            ]
        );

        if (awalOppsById.isEmpty()) {
            return;
        }

        Map<Id, Integer> remainingLinksAfterDeletion = new Map<Id, Integer>();
        for (AggregateResult ar : [
            SELECT LinkedEntityId oppId, COUNT(Id) total
            FROM ContentDocumentLink
            WHERE LinkedEntityId IN :awalOppsById.keySet() AND ContentDocumentId NOT IN :cdMap.keySet()
            GROUP BY LinkedEntityId
        ]) {
            remainingLinksAfterDeletion.put((Id) ar.get('oppId'), (Integer) ar.get('total'));
        }

        Set<Id> blockedDocIds = new Set<Id>();

        for (Opportunity opp : awalOppsById.values()) {
            Integer remaining = (remainingLinksAfterDeletion.get(opp.Id) ?? 0);
            if (opp.IsPAndLToolAttached__c != AWAL_Constants.OPPORTUNITY_YES_OPTION || remaining > 0) {
                continue;
            }

            Set<Id> relatedDocIds = oppIdToContentDocIds.get(opp.Id);

            for (Id docId : relatedDocIds) {
                if (cdMap.containsKey(docId) && !blockedDocIds.contains(docId)) {
                    cdMap.get(docId).addError(AWAL_Constants.ERROR_MSG_PL_FILE_DELETE_NOT_ALLOWED);
                    blockedDocIds.add(docId);
                }
            }
        }

        Set<Id> deletableDocIds = new Set<Id>(cdMap.keySet());
        deletableDocIds.removeAll(blockedDocIds);

        if (deletableDocIds.isEmpty()) {
            return;
        }

        List<Opportunity> oppsToUpdate = new List<Opportunity>();

        for (Opportunity opp : awalOppsById.values()) {
            Set<Id> relatedDocIds = oppIdToContentDocIds.get(opp.Id);

            Integer blockedDeletesCountForOpp = 0;

            for (Id docId : relatedDocIds) {
                if (blockedDocIds.contains(docId)) {
                    blockedDeletesCountForOpp++;
                }
            }

            Integer finalRemainingLinksAfterThisDelete =
                (remainingLinksAfterDeletion.get(opp.Id) ?? 0) + blockedDeletesCountForOpp;

            if (finalRemainingLinksAfterThisDelete == 0) {
                oppsToUpdate.add(new Opportunity(Id = opp.Id, HasAttachments__c = false));
            }
        }

        if (!oppsToUpdate.isEmpty()) {
            TriggerControl.disableTrigger(OpportunityTriggerHandler.SELF);
            update oppsToUpdate;
            TriggerControl.enableTrigger(OpportunityTriggerHandler.SELF);
        }
    }

    /**
     * @description Handles ContentDocument deletions for AdvancePayment__c:
     *    1) Blocks deletion (addError) when the payment Status__c does not allow file deletion.
     *    2) Updates AdvancePayment__c.HasAttachedFile__c to false when, after the deletable docs are removed,
     *        the payment would have no remaining ContentDocumentLinks (only for allowed statuses).
     * @param oldMap Map<Id, ContentDocument> containing ONLY the ContentDocuments being deleted that are linked to AdvancePayment__c.
     * @param paymentCdls List<ContentDocumentLink> already queried, containing ONLY links for the docs being deleted to AdvancePayment__c.
     */
    public static void handlePayments(Map<Id, ContentDocument> cdMap, List<ContentDocumentLink> paymentCdls) {
        Map<Id, Set<Id>> paymentIdToDocIds = new Map<Id, Set<Id>>();
        for (ContentDocumentLink cdl : paymentCdls) {
            if (!paymentIdToDocIds.containsKey(cdl.LinkedEntityId)) {
                paymentIdToDocIds.put(cdl.LinkedEntityId, new Set<Id>());
            }
            paymentIdToDocIds.get(cdl.LinkedEntityId).add(cdl.ContentDocumentId);
        }

        if (paymentIdToDocIds.isEmpty()) {
            return;
        }

        Map<Id, AdvancePayment__c> paymentsById = new Map<Id, AdvancePayment__c>(
            [
                SELECT Id, Status__c, HasAttachedFile__c
                FROM AdvancePayment__c
                WHERE Id IN :paymentIdToDocIds.keySet()
            ]
        );

        Set<Id> invalidPaymentIds = new Set<Id>();
        Set<Id> allowedPaymentsWithFlagTrue = new Set<Id>();

        for (AdvancePayment__c p : paymentsById.values()) {
            if (AP_Constants.AP_ALLOWED_FILE_DELETION_STATUS.contains(p.Status__c)) {
                if (p.HasAttachedFile__c) {
                    allowedPaymentsWithFlagTrue.add(p.Id);
                }
            } else {
                invalidPaymentIds.add(p.Id);
            }
        }

        Set<Id> blockedDocIds = new Set<Id>();
        for (Id paymentId : invalidPaymentIds) {
            Set<Id> relatedDocIds = paymentIdToDocIds.get(paymentId);

            for (Id docId : relatedDocIds) {
                if (cdMap.containsKey(docId) && !blockedDocIds.contains(docId)) {
                    cdMap.get(docId).addError(AP_Constants.ERROR_MESSAGE_FILE_DELETE_INVALID_STATUS);
                    blockedDocIds.add(docId);
                }
            }
        }

        Set<Id> deletableDocIds = new Set<Id>(cdMap.keySet());
        deletableDocIds.removeAll(blockedDocIds);

        if (deletableDocIds.isEmpty()) {
            return;
        }

        Set<Id> paymentsToCheckForUpdate = new Set<Id>();

        for (Id paymentId : paymentIdToDocIds.keySet()) {
            if (!allowedPaymentsWithFlagTrue.contains(paymentId)) {
                continue;
            }

            Set<Id> relatedDocIds = paymentIdToDocIds.get(paymentId);

            for (Id docId : relatedDocIds) {
                if (deletableDocIds.contains(docId)) {
                    paymentsToCheckForUpdate.add(paymentId);
                    break;
                }
            }
        }

        if (paymentsToCheckForUpdate.isEmpty()) {
            return;
        }

        Map<Id, Integer> paymentToRemainingLinks = new Map<Id, Integer>();
        for (AggregateResult ar : [
            SELECT LinkedEntityId paymentId, COUNT(Id) total
            FROM ContentDocumentLink
            WHERE LinkedEntityId IN :paymentsToCheckForUpdate AND ContentDocumentId NOT IN :deletableDocIds
            GROUP BY LinkedEntityId
        ]) {
            paymentToRemainingLinks.put((Id) ar.get('paymentId'), (Integer) ar.get('total'));
        }

        List<AdvancePayment__c> paymentsToUpdate = new List<AdvancePayment__c>();
        for (Id paymentId : paymentsToCheckForUpdate) {
            Integer remaining = (paymentToRemainingLinks.get(paymentId) ?? 0);
            if (remaining == 0) {
                paymentsToUpdate.add(new AdvancePayment__c(Id = paymentId, HasAttachedFile__c = false));
            }
        }

        if (!paymentsToUpdate.isEmpty()) {
            TriggerControl.disableTrigger(AdvancePaymentTriggerHandler.SELF);
            update paymentsToUpdate;
            TriggerControl.enableTrigger(AdvancePaymentTriggerHandler.SELF);
        }
    }

    /**
     * @description Publishes File_Delete_Event__e platform events for each non-User parent record that is linked
     *   (via ContentDocumentLink) to any ContentDocument being deleted.
     *   One event is published per unique parent Id.
     * @param triggerOld List<ContentDocument> provided by the trigger context (Trigger.old).
     */
    public static void publishDeleteEvent(List<ContentDocument> triggerOld) {
        Set<Id> linkedEntityIds = new Set<Id>();

        for (ContentDocument cd : [
            SELECT Id, (SELECT Id, LinkedEntityId FROM ContentDocumentLinks)
            FROM ContentDocument
            WHERE Id IN :triggerOld
        ]) {
            for (ContentDocumentLink cdl : cd.ContentDocumentLinks) {
                linkedEntityIds.add(cdl.LinkedEntityId);
            }
        }

        List<File_Delete_Event__e> fileDeleteEvents = new List<File_Delete_Event__e>();
        for (Id parentId : linkedEntityIds) {
            if (parentId.getSObjectType() != User.SObjectType) {
                fileDeleteEvents.add(new File_Delete_Event__e(Parent_ID__c = parentId));
            }
        }

        EventBus.publish(fileDeleteEvents);
        numOfpublishedEvents = fileDeleteEvents.size();
    }
}