/***********************************************************************************************************
 * Name         : CA_OpportunityTriggerHelper
 * Purpose      : Helper methods for CA_OpportunityTrigger
 **********************************************************************************************************/
public with sharing class CA_OpportunityTriggerHelper {
    /**
     * @description Creates or Deletes Projected Peformance records
     * @param List<Opportunity> Trigger.new opps
     * @param Map<Id,Opportunity> Trigger.oldMap opps
     */
    public static void createOrDeleteProjecedPerformance(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> oppsToCreatePPs = new List<Opportunity>();
        List<Projected_Performance__c> ppsToDelete = new List<Projected_Performance__c>();
        final List<String> BUCKETS_WITH_PP = new List<String>{
            CA_Constants.OPPORTUNITY_BUCKET_NEW_DEAL,
            CA_Constants.OPPORTUNITY_BUCKET_AMENDMENT
        };
        FormulaUtils.recalcFormulasPreservingAddressCodes(newList);
        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap?.get(opp.Id);

            Boolean needsPP =
                BUCKETS_WITH_PP.contains(opp.DealTypeBucket__c) && opp.AdvanceRequired__c == Constants.YES;
            Boolean hadPP = oldOpp != null && oldOpp.Projected_Performance__c != null;

            if (needsPP != hadPP) {
                if (needsPP) {
                    oppsToCreatePPs.add(opp);
                } else {
                    ppsToDelete.add(new Projected_Performance__c(Id = opp.Projected_Performance__c));
                }
            }
        }

        if (!oppsToCreatePPs.isEmpty()) {
            List<Projected_Performance__c> ppsToCreate = new List<Projected_Performance__c>();
            for (Integer i = 0; i < oppsToCreatePPs.size(); i++) {
                ppsToCreate.add(new Projected_Performance__c());
            }
            insert ppsToCreate;
            for (Integer i = 0; i < oppsToCreatePPs.size(); i++) {
                oppsToCreatePPs[i].Projected_Performance__c = ppsToCreate[i].Id;
            }
        }

        delete ppsToDelete;
    }

    /**
     * @description Validates required fields by Stage
     * @param List<Opportunity> Trigger.new opps
     */
    public static void validateRequiredFieldsByStage(List<Opportunity> newList) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.CA)) {
            return;
        }

        Set<String> dealTypeBuckets = new Set<String>();
        for (Opportunity opp : newList) {
            dealTypeBuckets.add(opp.DealTypeBucket__c);
        }

        Map<String, List<CA_Required_Field__c>> reqFieldsByDealTypeBucket = new Map<String, List<CA_Required_Field__c>>();
        for (CA_Required_Field__c reqField : [
            SELECT Name, Stage__c, Deal_Type_Bucket__c, Deal_Type__c, Additional_Criteria__c, Record_Type__c
            FROM CA_Required_Field__c
            WHERE Deal_Type_Bucket__C IN :dealTypeBuckets AND Active__c = TRUE
            ORDER BY Name ASC, Priority__c DESC
        ]) {
            if (!reqFieldsByDealTypeBucket.containsKey(reqField.Deal_Type_Bucket__C)) {
                reqFieldsByDealTypeBucket.put(reqField.Deal_Type_Bucket__C, new List<CA_Required_Field__c>());
            }
            reqFieldsByDealTypeBucket.get(reqField.Deal_Type_Bucket__C).add(reqField);
        }

        for (Opportunity opp : newList) {
            for (
                CA_Required_Field__c reqField : reqFieldsByDealTypeBucket.get(opp.DealTypeBucket__c) ??
                    new List<CA_Required_Field__c>()
            ) {
                Integer currentStageIndex = CA_Constants.OPPORTUNITY_STAGE_LIST.indexOf(opp.StageName);
                List<String> dealTypeList = reqField.Deal_Type__c.split(';');
                List<String> recordTypeList = reqField.Record_Type__c.split(';');
                if (
                    currentStageIndex >= CA_Constants.OPPORTUNITY_STAGE_LIST.indexOf(reqField.Stage__c) &&
                    dealTypeList.contains(opp.CA_Deal_Type__c) &&
                    recordTypeList.contains(OpportunityTriggerHelper.CA_RECORD_TYPES_BY_ID_MAP.get(opp.RecordTypeId))
                ) {
                    Boolean criteriaMet = true;
                    if (String.isNotBlank(reqField.Additional_Criteria__c)) {
                        List<String> criteriaList = reqField.Additional_Criteria__c.split(';');

                        for (String criteria : criteriaList) {
                            criteriaMet = evaluateReqFieldCriterion(criteria.trim(), opp);
                            if (!criteriaMet) {
                                break;
                            }
                        }
                    }
                    if (criteriaMet && opp.get(reqField.Name) == null) {
                        opp.addError(
                            reqField.Name,
                            getMissingRequiredFieldsByStageError(opp.CA_Deal_Type__c, reqField.Stage__c)
                        );
                    }
                }
            }
        }
    }

    /**
     * Evaluates a single Additional_Criteria__c criterion against an Opportunity.
     * Supported formats:
     *   FIELD=VALUE    — equality; comma-separated values treated as IN (any match = true)
     *   FIELD<>VALUE   — not-equal; comma-separated values treated as NOT IN (all must differ)
     */
    @TestVisible
    private static Boolean evaluateReqFieldCriterion(String criterion, Opportunity opp) {
        if (criterion.contains('<>')) {
            List<String> parts = criterion.split('<>');
            String actualValue = String.valueOf(opp.get(parts[0].trim()));
            for (String criterionValue : parts[1].split(',')) {
                if (actualValue == criterionValue.trim()) {
                    return false;
                }
            }
            return true;
        }

        List<String> parts = criterion.split('=');
        String actualValue = String.valueOf(opp.get(parts[0].trim()));
        for (String criterionValue : parts[1].split(',')) {
            if (actualValue == criterionValue.trim()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @description Concatenates the error message for missing required fields
     *  based on the deal type and the stage
     * @param String    deal type
     * @param String    stage
     */
    @TestVisible
    private static String getMissingRequiredFieldsByStageError(String dealType, String stage) {
        List<Object> parameters = new List<Object>{ dealType, stage };
        return String.format(CA_Constants.VR_ERROR_MSG_REQUIRED_FIELDS_BY_STAGE, parameters);
    }

    /**
     * @description Prevents users from updating the Stage and Advance Required? to Yes simultaneously.
     *  That way the Advance Tab required fields will be displayed and enforced before moving the Opp to the new stage.
     * @param List<Opportunity> Trigger.new opps
     * @param Map<Id,Opportunity> Trigger.oldMap opps
     */
    public static void preventAdvanceRequiredAndStageChange(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.CA)) {
            return;
        }
        for (Opportunity opp : newList) {
            Integer newStageIndex = CA_Constants.OPPORTUNITY_STAGE_LIST.indexOf(opp.StageName);
            if (
                (opp.DealTypeBucket__c == CA_Constants.OPPORTUNITY_BUCKET_NEW_DEAL ||
                opp.DealTypeBucket__c == CA_Constants.OPPORTUNITY_BUCKET_AMENDMENT) &&
                TriggerUtils.fieldHasChanged(oldMap.get(opp.Id), opp, 'StageName') &&
                TriggerUtils.fieldHasChanged(oldMap.get(opp.Id), opp, 'AdvanceRequired__c') &&
                opp.AdvanceRequired__c == 'Yes' &&
                newStageIndex >=
                CA_Constants.OPPORTUNITY_STAGE_LIST.indexOf(CA_Constants.OPPORTUNITY_STAGE_REQUEST_ADVANCE_MODEL)
            ) {
                opp.addError(CA_Constants.VR_ERROR_MSG_ADV_REQUIRED_AND_STAGE);
            }
        }
    }

    /**
     * @description Enqueues a job to send an email to the Relationship Manager (RM) when
     *  the CA_RelationshipTeamLeadMember__c field changes on an Opportunity.
     * @param newList List of Opportunity records from Trigger.new
     * @param oldMap Map of Opportunity Id to Opportunity records from Trigger.oldMap
     */
    public static void sendEmailToRM(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();
        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap.get(opp.Id);

            Boolean changedRmLeadMember =
                TriggerUtils.fieldHasChanged(oldOpp, opp, CA_Constants.RELATIONSHIP_TEAM_LEAD_FIELD) &&
                opp.CA_RelationshipTeamLead__c != null;

            Boolean stageActiveInOA =
                TriggerUtils.fieldHasChanged(oldOpp, opp, 'StageName') &&
                opp.StageName == CA_Constants.OPPORTUNITY_STAGE_ACTIVE_IN_OA &&
                opp.CA_RelationshipTeamLead__c != null;

            Boolean isNewDeal = opp.DealTypeBucket__c == CA_Constants.OPPORTUNITY_BUCKET_NEW_DEAL;

            if (isNewDeal && (changedRmLeadMember || stageActiveInOA)) {
                filteredOpps.add(opp);
            }
        }
        if (!filteredOpps.isEmpty()) {
            System.enqueueJob(new CA_RmEmailQueueable(filteredOpps));
        }
    }

    /**
     * @description After Insert trigger handler that initializes Service Tracker records for
     * newly created opportunities.
     *
     * Process:
     * 1. Groups opportunities by account
     * 2. For each account, finds the most recent completed opportunity's services
     * 3. Clones those services and associates them with the new opportunity
     * 4. Creates missing records for any other active Master Right not present in the clone set
     *
     * @param opps List of newly inserted opportunities
     */
    public static void loadServiceTracker(List<Opportunity> opps) {
        Map<Id, List<Opportunity>> oppsByAccount = new Map<Id, List<Opportunity>>();

        for (Opportunity opp : opps) {
            if (opp.AccountId != null) {
                if (!oppsByAccount.containsKey(opp.AccountId)) {
                    oppsByAccount.put(opp.AccountId, new List<Opportunity>());
                }
                oppsByAccount.get(opp.AccountId).add(opp);
            }
        }

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

        Map<Id, Map<Id, Opportunity_Right__c>> servicesByAccount = new Map<Id, Map<Id, Opportunity_Right__c>>();
        Set<Id> accountIds = oppsByAccount.keySet();

        String query =
            'SELECT ' +
            ' Master_Right__r.Name, ' +
            String.join(CA_ServiceTrackerHelper.getRightFields(), ',') +
            ' FROM Opportunity_Right__c' +
            ' WHERE Account_Id__c IN :accountIds' +
            ' AND Master_Right__r.Active__c = TRUE' +
            ' AND Opportunity__c NOT IN :opps' +
            ' AND Opportunity__r.StageName = \'' +
            CA_Constants.OPPORTUNITY_STAGE_COMPLETE +
            '\'' +
            ' ORDER BY Opportunity__r.LastStageChangeDate DESC NULLS LAST';
        List<Opportunity_Right__c> existingServices = Database.query(query);

        if (!existingServices.isEmpty()) {
            for (Opportunity_Right__c service : existingServices) {
                if (!servicesByAccount.containsKey(service.Account_Id__c)) {
                    servicesByAccount.put(service.Account_Id__c, new Map<Id, Opportunity_Right__c>());
                }

                if (!servicesByAccount.get(service.Account_Id__c).containsKey(service.Master_Right__c)) {
                    servicesByAccount.get(service.Account_Id__c).put(service.Master_Right__c, service);
                }
            }

            List<Master_Right__c> activeMasterRights = [
                SELECT Id
                FROM Master_Right__c
                WHERE Active__c = TRUE
                ORDER BY Order__c
            ];

            List<Opportunity_Right__c> servicesToInsert = new List<Opportunity_Right__c>();
            Map<Id, Opportunity> oppsToUpdate = new Map<Id, Opportunity>();

            for (Id accountId : oppsByAccount.keySet()) {
                Map<Id, Opportunity_Right__c> accountServicesByMasterRight = servicesByAccount.get(accountId);

                List<Opportunity_Right__c> accountServices = accountServicesByMasterRight != null
                    ? accountServicesByMasterRight.values()
                    : new List<Opportunity_Right__c>();

                Set<Id> clonedMasterRights = accountServicesByMasterRight != null
                    ? new Set<Id>(accountServicesByMasterRight.keySet())
                    : new Set<Id>();

                for (Opportunity opp : oppsByAccount.get(accountId)) {
                    for (Opportunity_Right__c originalService : accountServices) {
                        Opportunity_Right__c newService = originalService.clone(false, true, false, false);
                        newService.Opportunity__c = opp.Id;
                        newService.Approval_History__c = null;
                        newService.Approval_Snapshot__c = null;
                        servicesToInsert.add(newService);

                        if (
                            originalService.Master_Right__r.Name == CA_ServiceTrackerHelper.ROYALTY_SHARE_SERVICE &&
                            originalService.Acquired__c
                        ) {
                            oppsToUpdate.put(opp.Id, new Opportunity(Id = opp.Id, CA_RoyaltyShare__c = true));
                        }
                    }

                    for (Master_Right__c masterRight : activeMasterRights) {
                        if (!clonedMasterRights.contains(masterRight.Id)) {
                            servicesToInsert.add(
                                new Opportunity_Right__c(
                                    Opportunity__c = opp.Id,
                                    Master_Right__c = masterRight.Id,
                                    Service_Requested__c = false,
                                    Approval_Status__c = null
                                )
                            );
                        }
                    }
                }
            }

            try {
                if (!servicesToInsert.isEmpty()) {
                    insert servicesToInsert;
                }

                if (!oppsToUpdate.isEmpty()) {
                    TriggerControl.disableTrigger(OpportunityTriggerHandler.SELF);
                    update oppsToUpdate.values();
                    TriggerControl.enableTrigger(OpportunityTriggerHandler.SELF);
                }
            } catch (Exception ex) {
                ErrorLogger.logError(ex);
            }
        }
    }

    /**
     * Clears persisted values from unavailable services when Opportunities transition to Complete.
     * This keeps values during negotiation (to allow country rollback) but ensures completed records
     * do not carry unavailable service data to Account Service Tracker and future Opportunities.
     * Only services acquired by the current Opportunity are cleared.
     *
     * @param newList Trigger.new opportunities
     * @param oldMap Trigger.oldMap opportunities
     */
    public static void clearUnavailableServicesOnOppComplete(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        Map<Id, String> countryRegionByOppId = new Map<Id, String>();

        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            if (
                opp.StageName == CA_Constants.OPPORTUNITY_STAGE_COMPLETE &&
                TriggerUtils.fieldHasChanged(oldOpp, opp, 'StageName')
            ) {
                String countryRegion = opp.CA_Main_Focus_Territory__c ?? opp.CA_Company_Individual_Country__c;
                countryRegionByOppId.put(opp.Id, countryRegion);
            }
        }

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

        List<Opportunity_Right__c> services = [
            SELECT
                Id,
                Opportunity__c,
                Acquired_Opportunity__c,
                Master_Right__r.Name,
                Master_Right__r.Availability__c,
                Master_Right__r.Availability_Type__c
            FROM Opportunity_Right__c
            WHERE Opportunity__c IN :countryRegionByOppId.keySet() AND Master_Right__r.Active__c = TRUE
        ];

        List<Opportunity_Right__c> servicesToUpdate = new List<Opportunity_Right__c>();
        Set<Id> royaltyShareOppIdsToUpdate = new Set<Id>();
        for (Opportunity_Right__c service : services) {
            String countryRegion = countryRegionByOppId.get(service.Opportunity__c);
            if (
                service.Acquired_Opportunity__c == service.Opportunity__c &&
                !isServiceAvailableForCountry(service, countryRegion)
            ) {
                Opportunity_Right__c serviceToUpdate = new Opportunity_Right__c(Id = service.Id);
                clearUnavailableServiceValues(serviceToUpdate);
                servicesToUpdate.add(serviceToUpdate);
                if (service.Master_Right__r.Name == CA_Constants.MASTER_RIGHT_ROYALTY_SHARE) {
                    royaltyShareOppIdsToUpdate.add(service.Opportunity__c);
                }
            }
        }

        if (!servicesToUpdate.isEmpty()) {
            update servicesToUpdate;
        }

        if (!royaltyShareOppIdsToUpdate.isEmpty()) {
            List<Opportunity> oppsToUpdate = new List<Opportunity>();
            for (Opportunity opp : newList) {
                if (royaltyShareOppIdsToUpdate.contains(opp.Id) && opp.CA_RoyaltyShare__c) {
                    oppsToUpdate.add(new Opportunity(Id = opp.Id, CA_RoyaltyShare__c = false));
                }
            }

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

    /**
     * Returns whether a service is available for the given country/region
     * based on Master Right availability and availability type.
     *
     * @param service Opportunity Right record with Master Right availability metadata
     * @param countryRegion Country/region to evaluate
     * @return true when service is considered available, otherwise false
     */
    public static Boolean isServiceAvailableForCountry(Opportunity_Right__c service, String countryRegion) {
        if (String.isBlank(countryRegion) || String.isBlank(service.Master_Right__r.Availability__c)) {
            return true;
        }

        List<String> availabilityList = service.Master_Right__r.Availability__c.split(';');
        String availabilityType = service.Master_Right__r.Availability_Type__c ??
            CA_Constants.SERVICE_AVAILABILITY_TYPE_AVAILABLE_IN;

        Boolean isListed = availabilityList.contains(countryRegion);
        return availabilityType == CA_Constants.SERVICE_AVAILABILITY_TYPE_AVAILABLE_IN ? isListed : !isListed;
    }

    /**
     * Clears updateable persisted fields on an unavailable service while preserving
     * core identifying and relationship fields.
     *
     * @param serviceToUpdate Minimal Opportunity Right instance used for update
     */
    private static void clearUnavailableServiceValues(Opportunity_Right__c serviceToUpdate) {
        Map<String, Schema.DisplayType> fieldsToClear = getServiceFieldsToClearOnUnavailable();
        for (String fieldApiName : fieldsToClear.keySet()) {
            if (fieldsToClear.get(fieldApiName) == Schema.DisplayType.BOOLEAN) {
                serviceToUpdate.put(fieldApiName, false);
            } else {
                serviceToUpdate.put(fieldApiName, null);
            }
        }
    }

    /**
     * Builds the list of updateable fields that can be reset when a service
     * becomes unavailable. Boolean fields are reset to false and nillable fields
     * are set to null.
     *
     * @return Map of field API name to display type for fields that should be cleared
     */
    private static Map<String, Schema.DisplayType> getServiceFieldsToClearOnUnavailable() {
        Map<String, Schema.DisplayType> fieldsToClearOnUnavailable = new Map<String, Schema.DisplayType>();
        Map<String, Schema.SObjectField> serviceFieldsMap = Opportunity_Right__c.SObjectType.getDescribe()
            .fields.getMap();

        for (String fieldApiName : serviceFieldsMap.keySet()) {
            Schema.DescribeFieldResult fieldDesc = serviceFieldsMap.get(fieldApiName).getDescribe();
            if (
                CA_Constants.SERVICE_FIELDS_TO_PRESERVE_ON_UNAVAILABLE_CLEAR.contains(fieldApiName) ||
                fieldDesc.isCalculated() ||
                !fieldDesc.isUpdateable()
            ) {
                continue;
            }

            if (fieldDesc.getType() == Schema.DisplayType.BOOLEAN || fieldDesc.isNillable()) {
                fieldsToClearOnUnavailable.put(fieldApiName, fieldDesc.getType());
            }
        }

        return fieldsToClearOnUnavailable;
    }

    /**
     * Validates that accounts can only have one active Opportunity
     * (StageName not in Complete/Inactive).
     * Adds an error to opportunities if their related account already has another active opportunity.
     *
     * @param newList List of Opportunity records being inserted or updated
     * @param oldMap Trigger.oldMap
     */
    public static void validateOneActiveOpportunityPerAccount(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();
        Set<Id> oppIdsToExclude = new Set<Id>();
        List<String> nonActiveStages = new List<String>{
            CA_Constants.OPPORTUNITY_STAGE_COMPLETE,
            CA_Constants.OPPORTUNITY_STAGE_INACTIVE
        };

        if (oldMap == null) {
            filteredOpps.addAll(newList);
        } else {
            for (Opportunity opp : newList) {
                if (
                    TriggerUtils.fieldHasChanged(oldMap.get(opp.Id), opp, 'StageName') &&
                    !nonActiveStages.contains(opp.StageName)
                ) {
                    filteredOpps.add(opp);
                    oppIdsToExclude.add(opp.Id);
                }
            }
        }

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

        Set<Id> accountIds = new Set<Id>();
        for (Opportunity opp : filteredOpps) {
            if (opp.AccountId != null) {
                accountIds.add(opp.AccountId);
            }
        }

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

        Map<Id, Integer> activeOppCountByAccount = new Map<Id, Integer>();
        String query =
            'SELECT AccountId, COUNT(Id) oppCount ' +
            'FROM Opportunity ' +
            'WHERE AccountId IN :accountIds ' +
            'AND StageName NOT IN :nonActiveStages ' +
            (oldMap == null ? '' : 'AND Id NOT IN :oppIdsToExclude ') +
            'GROUP BY AccountId';
        List<AggregateResult> results = Database.query(query);

        for (AggregateResult result : results) {
            activeOppCountByAccount.put((Id) result.get('AccountId'), (Integer) result.get('oppCount'));
        }

        for (Opportunity opp : filteredOpps) {
            if (
                opp.AccountId != null &&
                !nonActiveStages.contains(opp.StageName) &&
                activeOppCountByAccount.containsKey(opp.AccountId)
            ) {
                opp.addError('AccountId', CA_Constants.VR_ERROR_MSG_MORE_THAN_ONE_ACTIVE_OPP_BY_ACCOUNT);
            }
        }
    }

    /**
     * @description Prevents users from assigning an already in use CA_MA_Label_Id__c
     *  to another Account
     *
     * @param newList List<Opportunity> Trigger.new list (after update)
     * @param oldMap Map<Id,Opportunity> Trigger.oldMap (before update)
     */
    public static void checkLabelId(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        Set<String> labelIds = new Set<String>();
        List<Opportunity> filteredOpps = new List<Opportunity>();
        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap != null ? oldMap.get(opp.Id) : null;
            if (TriggerUtils.fieldHasChanged(oldOpp, opp, 'CA_MA_Label_Id__C') && opp.CA_MA_Label_Id__c != null) {
                filteredOpps.add(opp);
                labelIds.add(opp.CA_MA_Label_Id__C);
            }
        }

        if (!labelIds.isEmpty()) {
            Map<String, Account> labelAccountMap = new Map<String, Account>();
            for (Account acc : [
                SELECT Id, Name, CA_MA_Label_Id__c
                FROM Account
                WHERE CA_MA_Label_Id__c IN :labelIds AND RecordType.DeveloperName = :CA_Constants.RECORD_TYPE_CA
            ]) {
                labelAccountMap.put(acc.CA_MA_Label_Id__c, acc);
            }

            for (Opportunity opp : filteredOpps) {
                Account acc = labelAccountMap.get(opp.CA_MA_Label_Id__C);
                if (acc != null && acc.Id != opp.AccountId) {
                    String errorMessage = String.format(
                        CA_Constants.VR_ERROR_MSG_LABEL_ID_MUST_BE_UNIQUE,
                        new List<Object>{ acc.Name }
                    );
                    opp.addError('CA_MA_Label_Id__C', errorMessage);
                }
            }
        }
    }

    /**
     * @description Creates Opportunity_History__c records for Content Acquisition Opportunities
     *  when specific fields are edited after the Opportunity has been completed.
     * @param newList List<Opportunity> - Trigger.new
     * @param oldMap Map<Id, Opportunity> - Trigger.oldMap
     */
    public static void logFieldHistory(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();
        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            if (opp.StageName == oldOpp.StageName && opp.StageName == CA_Constants.OPPORTUNITY_STAGE_COMPLETE) {
                filteredOpps.add(opp);
            }
        }

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

        Set<String> trackedOppFields = new Set<String>(CA_Constants.ACCOUNT_OPP_MAPPING.values());
        Map<String, Schema.SObjectField> oppFieldsMap = Schema.SObjectType.Opportunity.fields.getMap();
        Map<String, String> fieldsToTrackMap = new Map<String, String>();

        for (String fieldName : trackedOppFields) {
            if (oppFieldsMap.containsKey(fieldName)) {
                fieldsToTrackMap.put(fieldName, oppFieldsMap.get(fieldName).getDescribe().getLabel());
            }
        }

        List<SObject> historyRecordsToInsert = new List<SObject>();
        for (Opportunity opp : filteredOpps) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            historyRecordsToInsert.addAll(HistoryTracker.createHistoryRecords(oldOpp, opp, fieldsToTrackMap));
        }

        if (!historyRecordsToInsert.isEmpty()) {
            insert historyRecordsToInsert;
        }
    }

    /**
     * @description Synchronizes data between Account and Opportunity records based on field mappings.
     *  On insert, copies Account field values to corresponding Opportunity fields.
     *  On update, when an Opportunity reaches 'Complete' stage, copies Opportunity field values
     *  back to the related Account fields.
     *
     * @param newList List<Opportunity> - The new Opportunity records being inserted or updated
     * @param oldMap Map<Id, Opportunity> - Map of old Opportunity records (null for insert operations)
     */
    public static void syncDataBetweenAccountAndOpp(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        Set<Id> accountIds = new Set<Id>();
        List<Opportunity> filteredOpps = new List<Opportunity>();
        if (oldMap == null) {
            for (Opportunity opp : newList) {
                if (opp.AccountId != null) {
                    accountIds.add(opp.AccountId);
                    filteredOpps.add(opp);
                }
            }
        } else {
            for (Opportunity opp : newList) {
                if (opp.AccountId != null && opp.StageName == CA_Constants.OPPORTUNITY_STAGE_COMPLETE) {
                    accountIds.add(opp.AccountId);
                    filteredOpps.add(opp);
                }
            }
        }

        if (!filteredOpps.isEmpty()) {
            Set<String> acctFields = CA_Constants.ACCOUNT_OPP_MAPPING.keySet();
            String selectFields = 'Id,' + String.join(new List<String>(acctFields), ',');

            String query = 'SELECT ' + selectFields + ' FROM Account WHERE Id IN :accountIds';
            List<Account> accounts = (List<Account>) new QueryWithoutSharing().query(query, accountIds);

            Map<Id, Account> accountMap = new Map<Id, Account>(accounts);
            Map<Id, Account> accountsToUpdate = new Map<Id, Account>();

            for (Opportunity opp : filteredOpps) {
                Account acct = accountMap.get(opp.AccountId);
                for (String acctField : acctFields) {
                    String oppField = CA_Constants.ACCOUNT_OPP_MAPPING.get(acctField);
                    Object acctValue = acct.get(acctField);
                    Object oppValue = opp.get(oppField);
                    if (oldMap == null) {
                        if (acctValue != null && oppValue == null) {
                            opp.put(oppField, acctValue);
                        }
                    } else {
                        if (opp.get(oppField) != null && acctValue != oppValue) {
                            acct.put(acctField, oppValue);
                            if (!accountsToUpdate.containsKey(acct.Id)) {
                                accountsToUpdate.put(acct.Id, acct);
                            }
                        }
                    }
                }
            }

            if (!accountsToUpdate.isEmpty()) {
                TriggerControl.disableTrigger(AccountTriggerHandler.SELF);
                try {
                    update accountsToUpdate.values();
                } catch (Exception ex) {
                    ErrorLogger.logError(ex);
                }
                TriggerControl.enableTrigger(AccountTriggerHandler.SELF);
            }
        }
    }

    /**
     * @description Inner class that provides a mechanism to execute SOQL queries without sharing rules.
     *  This is necessary for scenarios where a Lead is converted to an Account and Opportunity,
     *  and the user performing the conversion is not the owner of the resulting Account and Opportunity.
     */
    private without sharing class QueryWithoutSharing {
        public QueryWithoutSharing() {
        }

        public List<SObject> query(String soql, Set<Id> accountIds) {
            return Database.query(soql);
        }
    }
}