/***********************************************************************************************************
 * Name         : AWAL_OpportunityTriggerHelper
 * Purpose      : Helper methods for AWAL_OpportunityTriggerHandler
 **********************************************************************************************************/
public with sharing class AWAL_OpportunityTriggerHelper {
    public static Map<Id, Opportunity> mastersMigratedOppsToBypass;

    /**
     * @description clear specific fields values when a user is cloning an Opp
     * @param newList Trigger.new
     */
    public static void clearCloneFields(List<Opportunity> opportunities) {
        List<Schema.FieldSetMember> fieldSetMembers = SObjectType.Opportunity.FieldSets.AWAL_FieldsToClearOnClone.getFields();

        for (Opportunity opp : opportunities) {
            if (opp.isClone()) {
                for (Schema.FieldSetMember fieldSetMember : fieldSetMembers) {
                    opp.put(
                        fieldSetMember.getFieldPath(),
                        fieldSetMember.getType() == Schema.DisplayType.Boolean ? false : null
                    );
                }
                opp.put('StageName', AWAL_Constants.OPPORTUNITY_STAGE_DEAL_TRACKING);
                opp.put('AWALStatusPath__c', AWAL_Constants.OPPORTUNITY_STAGE_DEAL_TRACKING);
                opp.put(CurrencyUtils.CLOSE_DATE_FIELD, Date.today().addDays(90));
            }
        }
    }

    /**
     * @description Removes the value for dependent fields when they are no longer valid (hidden)
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void handleFieldVisibilityDependencies(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        FieldVisibilityDependency.handleFieldVisibilityDependencies(
            newList,
            oldMap,
            AWAL_Constants.FIELD_VISIBILITY_DEPENDENCIES
        );

        for (Opportunity opp : newList) {
            String oldScopeValue = String.valueOf(oldMap.get(opp.Id).get('Scope_of_Deal__c') ?? '');
            String newScopeValue = String.valueOf(opp.get('Scope_of_Deal__c') ?? '');

            if (oldScopeValue != newScopeValue) {
                if (oldScopeValue.contains('Catalogue') && !newScopeValue.contains('Catalogue')) {
                    for (String catalogueField : AWAL_Constants.CATALOGUE_REQUIRED_FIELDS) {
                        opp.put(catalogueField, null);
                    }
                } else if (oldScopeValue.contains('Frontline') && !newScopeValue.contains('Frontline')) {
                    opp.put('NumberFrontlineRecordings__c', null);
                }
            }

            String oldCbValue = String.valueOf(oldMap.get(opp.Id).get('ContractBasis__c'));
            String newCbValue = String.valueOf(opp.get('ContractBasis__c'));
            if (
                AWAL_Constants.CONTRACT_BASIS_HOLDBACK_VALUES.contains(oldCbValue) &&
                !AWAL_Constants.CONTRACT_BASIS_HOLDBACK_VALUES.contains(newCbValue)
            ) {
                opp.put('HoldbackPeriodInMonths__c', null);
            }
            if (
                AWAL_Constants.CONTRACT_BASIS_EXCLUSIVE_VALUES.contains(oldCbValue) &&
                !AWAL_Constants.CONTRACT_BASIS_EXCLUSIVE_VALUES.contains(newCbValue)
            ) {
                opp.put('ExclusivePeriodInYears__c', null);
            }
        }
    }

    /**
     * @description Ensures the proper number of decimals are stored in the DB based on the Field definition
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void formatNumericFields(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<String> NUMERIC_TYPES = new List<String>{
            Schema.DisplayType.DOUBLE.toString(),
            Schema.DisplayType.PERCENT.toString(),
            Schema.DisplayType.CURRENCY.toString(),
            Schema.DisplayType.INTEGER.toString()
        };
        Set<String> allAwalFields = SchemaUtils.getFieldSetAPINames(
                SObjectType.Opportunity.FieldSets.AWAL_AllFields.getFields()
            )
            .keySet();

        Map<String, sObjectField> fields = Schema.sObjectType.Opportunity.fields.getMap();
        List<Schema.DescribeFieldResult> describeFieldResults = new List<Schema.DescribeFieldResult>();
        for (String key : fields.keySet()) {
            Schema.DescribeFieldResult result = fields.get(key).getDescribe();
            if (
                allAwalFields.contains(result.getName()) &&
                NUMERIC_TYPES.contains(result.getType().toString()) &&
                !result.isCalculated() &&
                result.isUpdateable()
            ) {
                describeFieldResults.add(result);
            }
        }

        for (Opportunity opp : newList) {
            for (Schema.DescribeFieldResult describeFieldResult : describeFieldResults) {
                String fieldName = describeFieldResult.getName();
                if (TriggerUtils.fieldHasChanged(oldMap?.get(opp.Id), opp, fieldName)) {
                    String stringValue = String.valueOf(opp.get(fieldName));
                    if (String.isNotBlank(stringValue)) {
                        Decimal decimalValue = Decimal.valueOf(stringValue);
                        Integer decimalsProvided = stringValue.substringAfter('.').length();
                        Integer maxScale = describeFieldResult.getScale();
                        if (decimalsProvided > maxScale) {
                            opp.put(fieldName, decimalValue.setScale(maxScale, RoundingMode.HALF_UP));
                        }
                    }
                }
            }
        }
    }

    /**
     * @description Populate the MasterApproverId__c field by matching TotalCommittedFunding__c
     *  and LeadDealMaker__c fields
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void setApprover(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<MasterApproverUtils.MasterApproverParameter> masterApproverParameters = new List<MasterApproverUtils.MasterApproverParameter>();
        for (Opportunity opp : newList) {
            if (
                oldMap == null ||
                opp.LeadDealMaker__c != oldMap.get(opp.Id).LeadDealMaker__c ||
                opp.TotalCommittedFunding__c != oldMap.get(opp.Id).TotalCommittedFunding__c
            ) {
                if (opp.Is_Amendment__c) {
                    masterApproverParameters.add(
                        new MasterApproverUtils.MasterApproverParameter(
                            opp,
                            AWAL_Constants.MASTER_APPROVER_AMENDMENT_AMOUNT_FIELD,
                            AWAL_Constants.MASTER_APPROVER_ADD_CRITERIA_FIELD,
                            AWAL_Constants.AWAL,
                            AWAL_Constants.MASTER_APPROVERS_APPROVERS_TEAM
                        )
                    );
                } else {
                    masterApproverParameters.add(
                        new MasterApproverUtils.MasterApproverParameter(
                            opp,
                            AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD,
                            AWAL_Constants.MASTER_APPROVER_ADD_CRITERIA_FIELD,
                            AWAL_Constants.AWAL,
                            AWAL_Constants.MASTER_APPROVERS_APPROVERS_TEAM
                        )
                    );
                }
            }
            if (!masterApproverParameters.isEmpty()) {
                MasterApproverUtils.setMasterApprover(masterApproverParameters);
            }
        }
    }

    /**
     * @description Upsert MaximumApprovedTerms__c mirroring all fields form Opportunity when it's new
     *  or only the fields that changed when it's pre-existing
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void upsertMaxApprovedTerms(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();

        for (Opportunity opp : newList) {
            if (
                oldMap == null ||
                AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(opp.StageName) ||
                AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(oldMap.get(opp.Id).StageName) ||
                oldMap.get(opp.Id).ExchangeRate__c != opp.ExchangeRate__c
            ) {
                filteredOpps.add(opp);
            }
        }

        if (!filteredOpps.isEmpty()) {
            Set<String> allMaxApprovedTermsFields = SchemaUtils.getFieldSetAPINames(
                    SObjectType.MaximumApprovedTerms__c.FieldSets.FieldsToMirror.getFields()
                )
                .keySet();

            Map<Integer, MaximumApprovedTerms__c> oppToMaxApprovedTerms = new Map<Integer, MaximumApprovedTerms__c>();

            for (Integer i = 0; i < filteredOpps.size(); i++) {
                Opportunity opp = filteredOpps[i];
                Set<String> fieldsToMirror = new Set<String>();

                if (oldMap != null && oldMap.containsKey(opp.Id)) {
                    if (
                        AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(opp.StageName) ||
                        AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(oldMap.get(opp.Id).StageName)
                    ) {
                        for (String field : allMaxApprovedTermsFields) {
                            if (opp.get(field) != oldMap.get(opp.Id).get(field)) {
                                fieldsToMirror.add(field);
                            }
                        }
                    } else {
                        fieldsToMirror.add(CurrencyUtils.EXCHANGE_RATE_FIELD);
                    }
                } else {
                    fieldsToMirror = allMaxApprovedTermsFields;
                }

                MaximumApprovedTerms__c maxApprovedTerms = buildMaxApprovedTermsRecord(opp, fieldsToMirror);
                maxApprovedTerms.Id = opp.MaximumApprovedTerms__c;
                oppToMaxApprovedTerms.put(i, maxApprovedTerms);
            }

            if (!oppToMaxApprovedTerms.isEmpty()) {
                upsert oppToMaxApprovedTerms.values();

                if (oldMap == null) {
                    for (Integer i = 0; i < filteredOpps.size(); i++) {
                        filteredOpps[i].MaximumApprovedTerms__c = oppToMaxApprovedTerms.get(i).Id;
                    }
                }
            }
        }
    }

    /**
     * @description Build the MaximumApprovedTerms__c based on the related Opportunity
     * @param opportunity Related Opportunity
     * @param oldMap List of fields to mirror
     */
    private static MaximumApprovedTerms__c buildMaxApprovedTermsRecord(Opportunity opportunity, Set<String> fields) {
        MaximumApprovedTerms__c maxApprovedTerms = new MaximumApprovedTerms__c();

        for (String field : fields) {
            maxApprovedTerms.put(field, opportunity.get(field));
        }

        return maxApprovedTerms;
    }

    /**
     * @description applies the validation rules using field sets for Passed to Finance Approver,
     *  and Closed stages.
     * @param newList Trigger.new
     */
    public static void validateRequiredFieldsByStage(List<Opportunity> newList) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AWAL)) {
            return;
        }

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

        for (Opportunity opp : filterBypassOpportunities(newList)) {
            if (
                opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE_APPROVER ||
                opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_WON
            ) {
                filteredOpps.add(opp);
            }
        }

        if (!filteredOpps.isEmpty()) {
            Map<String, Schema.FieldSet> fieldSetsByName = Schema.SObjectType.Opportunity.fieldSets.getMap();

            Set<String> analysisCompleteSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_EMPTY_BEFORE_ANALYSIS_COMPLETE).getFields()
                )
                .keySet();
            Set<String> closedSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_EMPTY_BEFORE_CLOSED).getFields()
                )
                .keySet();

            Set<String> analysisCompleteAmendSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_EMPTY_BEFORE_ANALYSIS_COMPLETE_AMEND).getFields()
                )
                .keySet();
            Set<String> closedAmendSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_EMPTY_BEFORE_CLOSED_AMEND).getFields()
                )
                .keySet();
            for (Opportunity opp : filteredOpps) {
                Set<String> fieldsToCheck;
                if (opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE_APPROVER) {
                    fieldsToCheck = opp.Is_Amendment__c ? analysisCompleteAmendSet : analysisCompleteSet;
                } else {
                    fieldsToCheck = opp.Is_Amendment__c ? closedAmendSet : closedSet;
                }

                String errorMsg = opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE_APPROVER
                    ? AWAL_Constants.VR_ERROR_MSG_MISSING_FIELDS_AT_FINANCE_APPROVER
                    : AWAL_Constants.VR_ERROR_MSG_MISSING_FIELDS_AT_CLOSE;
                for (String fieldName : fieldsToCheck) {
                    if (opp.get(fieldName) == null) {
                        opp.addError(fieldName, errorMsg);
                    }
                }
            }
        }
    }

    /**
     * @description Implement custom validation rules that are applied to multiple fields
     *  present in the MaximumApprovedTerms__c related object
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void validateMaxApprovedTerms(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AWAL)) {
            return;
        }

        List<Opportunity> filteredOpps = new List<Opportunity>();
        Set<Id> maxApprovedTermsIds = new Set<Id>();
        Map<Id, Opportunity> approvedTermsIdToOpp = new Map<Id, Opportunity>();

        for (Opportunity opp : filterBypassOpportunities(newList)) {
            if (
                !AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(opp.StageName) &&
                !AWAL_Constants.STAGES_TO_MIRROR_MAT.contains(oldMap.get(opp.Id).StageName)
            ) {
                filteredOpps.add(opp);
                maxApprovedTermsIds.add(opp.MaximumApprovedTerms__c);
                approvedTermsIdToOpp.put(opp.MaximumApprovedTerms__c, opp);
            }
        }

        if (!filteredOpps.isEmpty()) {
            Map<String, Schema.FieldSet> fieldSetsByName = Schema.SObjectType.Opportunity.fieldSets.getMap();
            Set<String> cannotBeLowerThanSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_BE_LOWER_THAN).getFields()
                )
                .keySet();
            Set<String> cannotExceedSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_EXCEED).getFields()
                )
                .keySet();
            Set<String> cannotBeCheckedSet = SchemaUtils.getFieldSetAPINames(
                    fieldSetsByName.get(AWAL_Constants.FS_CANNOT_BE_CHECKED).getFields()
                )
                .keySet();

            List<String> fullListOfFields = new List<String>();
            fullListOfFields.addAll(cannotBeLowerThanSet);
            fullListOfFields.addAll(cannotExceedSet);
            fullListOfFields.addAll(cannotBeCheckedSet);

            String query =
                'SELECT Id, ' +
                String.join(fullListOfFields, ',') +
                ' FROM MaximumApprovedTerms__c WHERE Id IN :maxApprovedTermsIds';

            Map<Id, MaximumApprovedTerms__c> maxApprovedTermsMap = new Map<Id, MaximumApprovedTerms__c>();

            for (MaximumApprovedTerms__c mat : Database.query(query)) {
                maxApprovedTermsMap.put(mat.Id, mat);
            }

            for (Opportunity opp : filteredOpps) {
                MaximumApprovedTerms__c maxApprovedTerms = maxApprovedTermsMap.get(opp.MaximumApprovedTerms__c);
                Opportunity oldOpp = oldMap.get(opp.Id);

                validateActualVsApprovedTerms(
                    opp,
                    oldOpp,
                    maxApprovedTerms,
                    cannotBeLowerThanSet,
                    AWAL_Constants.VR_ERROR_MSG_CANNOT_BE_LOWER,
                    '<'
                );
                validateActualVsApprovedTerms(
                    opp,
                    oldOpp,
                    maxApprovedTerms,
                    cannotExceedSet,
                    AWAL_Constants.VR_ERROR_MSG_CANNOT_EXCEED,
                    '>'
                );

                for (String fieldName : cannotBeCheckedSet) {
                    String oldPicklistValue = String.valueOf(oldOpp.get(fieldName));
                    String newPicklistValue = String.valueOf(opp.get(fieldName));

                    if ((oldPicklistValue != newPicklistValue)) {
                        String maxValue = String.valueOf(maxApprovedTerms.get(fieldName));

                        if (('No'.equals(maxValue) || maxValue == null) && 'Yes'.equals(newPicklistValue)) {
                            opp.addError(fieldName, AWAL_Constants.VR_ERROR_MSG_CANNOT_BE_CHECKED);
                        }
                    }
                }
            }
        }
    }

    /**
     * @description Compare actual terms vs Max. Approved terms and throws an error when the conditions are met
     * @param opp Opportunity record that holds the actual terms
     * @param oldOpp Same as opp but from Trigger.oldMap
     * @param mat Maximum Approved Terms record
     * @param fields Set of fields to include in the comparison
     * @param errorMsg Error message to display if the conditions are met
     * @param operator Expression operator that specifies what comparison to make
     */
    private static void validateActualVsApprovedTerms(
        Opportunity opp,
        Opportunity oldOpp,
        MaximumApprovedTerms__c mat,
        Set<String> fields,
        String errorMsg,
        String operator
    ) {
        for (String field : fields) {
            if (
                opp.get(field) != oldOpp.get(field) ||
                (oldOpp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_APPROVED &&
                opp.StageName != oldOpp.StageName)
            ) {
                if (
                    field == 'NumberFrontlineRecordings__c' &&
                    opp.Scope_of_Deal__c != null &&
                    !opp.Scope_of_Deal__c.contains('Frontline')
                ) {
                    continue;
                }

                Decimal oppValue = (Decimal) opp.get(field) ?? 0;
                Decimal matValue = (Decimal) mat.get(field) ?? 0;

                if ((operator == '<' && oppValue < matValue) || (operator == '>' && oppValue > matValue)) {
                    opp.addError(field, errorMsg);
                }
            }
        }
    }

    /**
     * @description Throws an error whenever a non-approver user tries to update a record
     *  to Approved stage
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void validateApprover(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AWAL)) {
            return;
        }

        List<Opportunity> filteredOpps = new List<Opportunity>();
        Set<Id> masterApproverIds = new Set<Id>();

        for (Opportunity opp : newList) {
            if (
                opp.MasterApproverId__c != null &&
                opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_APPROVED &&
                oldMap.get(opp.Id).StageName != AWAL_Constants.OPPORTUNITY_STAGE_APPROVED
            ) {
                filteredOpps.add(opp);
                masterApproverIds.add(opp.MasterApproverId__c);
            }
        }

        if (!filteredOpps.isEmpty()) {
            List<MasterApprovers__c> masterApproversAll = MasterApproverUtils.getRecordsByCustomerAndTeam(
                new Set<String>{ AWAL_Constants.AWAL },
                null
            );

            Map<Id, Set<Id>> secondaryApproversByOpp = new Map<Id, Set<Id>>();

            List<MasterApprovers__c> secondaryApproversAll = new List<MasterApprovers__c>();
            Map<Id, MasterApprovers__c> masterApproversMap = new Map<Id, MasterApprovers__c>();
            for (MasterApprovers__c masterApprover : masterApproversAll) {
                if (masterApprover.Team__c == AWAL_Constants.MASTER_APPROVERS_SECONDARY_APPROVERS_TEAM) {
                    secondaryApproversAll.add(masterApprover);
                } else if (masterApprover.Team__c == AWAL_Constants.MASTER_APPROVERS_APPROVERS_TEAM) {
                    masterApproversMap.put(masterApprover.Id, masterApprover);
                }
            }

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

            for (Opportunity opp : filteredOpps) {
                List<MasterApprovers__c> filteredSecondaryApprovers = MasterApproverUtils.filterMultipleApprovers(
                    opp,
                    secondaryApproversAll,
                    AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD,
                    AWAL_Constants.MASTER_APPROVER_ADD_CRITERIA_FIELD,
                    AWAL_Constants.AWAL
                );

                Set<Id> secondaryApproverUserIds = new Set<Id>();
                for (MasterApprovers__c masterApprover : filteredSecondaryApprovers) {
                    secondaryApproverUserIds.add(masterApprover.User__c);
                }

                Boolean isApprover =
                    UserInfo.getUserId() == masterApproversMap.get(opp.MasterApproverId__c).User__c ||
                    FeatureManagement.checkPermission(AWAL_Constants.CP_ALLOW_TO_APPROVE_ALL_DEALS);

                Boolean isSecondaryApprover = secondaryApproverUserIds.contains(UserInfo.getUserId());

                if (!isApprover && (!isSecondaryApprover || !opp.HasAttachments__c)) {
                    opp.addError('StageName', AWAL_Constants.VR_ERROR_MSG_CANNOT_APPROVE);
                } else if (!isApprover && isSecondaryApprover && opp.HasAttachments__c) {
                    oppsWithAttachedFiles.add(opp);
                    secondaryApproversByOpp.put(opp.Id, secondaryApproverUserIds);
                }
            }

            if (!oppsWithAttachedFiles.isEmpty()) {
                Set<Id> secondaryApproverUserIds = new Set<Id>();
                for (MasterApprovers__c secondaryApprover : secondaryApproversAll) {
                    secondaryApproverUserIds.add(secondaryApprover.User__c);
                }

                Map<Id, Opportunity> oppsWithFilesMap = new Map<Id, Opportunity>(
                    [
                        SELECT
                            Id,
                            (
                                SELECT Id, CreatedById
                                FROM AttachedContentDocuments
                                WHERE CreatedById IN :secondaryApproverUserIds
                            )
                        FROM Opportunity
                        WHERE Id IN :oppsWithAttachedFiles
                    ]
                );

                for (Opportunity opp : oppsWithAttachedFiles) {
                    Boolean secondaryHasAttached = false;
                    for (AttachedContentDocument attachedDoc : oppsWithFilesMap.get(opp.Id).AttachedContentDocuments) {
                        if (attachedDoc.CreatedById == UserInfo.getUserId()) {
                            secondaryHasAttached = true;
                            break;
                        }
                    }

                    if (!secondaryHasAttached) {
                        opp.addError('StageName', AWAL_Constants.VR_ERROR_MSG_SECONDARY_SHOULD_ATTACH);
                    }
                }
            }
        }
    }

    /**
     * @description Prevents skipping stages while moving the Opportunity forward,
     *              with conditional permission for skipping specific stages.
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void validateOpportunityStages(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AWAL)) {
            return;
        }

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

        for (Opportunity opp : filterBypassOpportunities(newList)) {
            if (oldMap == null) {
                if (!AWAL_Constants.VALID_INITIAL_STAGES.contains(opp.StageName)) {
                    opp.StageName.addError(AWAL_Constants.VR_ERROR_MSG_INVALID_INITIAL_STAGE);
                } else if (
                    opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_APPROVER &&
                    ((Decimal) opp.get(AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD)) >=
                    AWAL_Constants.TOTAL_COMMITTED_FUNDING_THRESHOLD
                ) {
                    opp.StageName.addError(AWAL_Constants.VR_ERROR_MSG_CANNOT_SKIP_FINANCE);
                } else if (
                    (opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE ||
                    opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_APPROVER) &&
                    ((Decimal) opp.get(AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD)) <
                    AWAL_Constants.TOTAL_COMMITTED_FUNDING_THRESHOLD &&
                    ((Decimal) opp.get(AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD)) > 0
                ) {
                    opp.StageName.addError(AWAL_Constants.VR_ERROR_MSG_CANNOT_CREATE_DEAL_WITHOUT_PL_TOOL);
                }
            } else if (opp.StageName != oldMap.get(opp.Id).StageName) {
                filteredOpps.add(opp);
            }
        }

        if (!filteredOpps.isEmpty()) {
            for (Opportunity opp : filteredOpps) {
                Opportunity oldOpp = oldMap.get(opp.Id);

                List<String> orderedStages = AWAL_Constants.ORDERED_STAGE_LIST;
                Integer currentIndex = orderedStages.indexOf(oldOpp.StageName);
                Integer targetIndex = orderedStages.indexOf(opp.StageName);

                if (currentIndex < targetIndex) {
                    List<String> allowedStages = AWAL_Constants.VALID_STAGES_TRANSITIONS_MAP.get(oldOpp.StageName);

                    if (allowedStages == null || !allowedStages.contains(opp.StageName)) {
                        opp.StageName.addError(AWAL_Constants.VR_ERROR_MSG_CANNOT_SKIP_STAGES);
                    } else {
                        String amountField = opp.Is_Amendment__c
                            ? AWAL_Constants.MASTER_APPROVER_AMENDMENT_AMOUNT_FIELD
                            : AWAL_Constants.MASTER_APPROVER_AMOUNT_FIELD;
                        Decimal value = (Decimal) opp.get(amountField);
                        Decimal maxValue = AWAL_Constants.TOTAL_COMMITTED_FUNDING_THRESHOLD;

                        if (
                            value >= maxValue &&
                            currentIndex <= orderedStages.indexOf(AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE) &&
                            targetIndex > orderedStages.indexOf(AWAL_Constants.OPPORTUNITY_STAGE_ANALYSIS_COMPLETE)
                        ) {
                            opp.StageName.addError(
                                opp.Is_Amendment__c
                                    ? AWAL_Constants.VR_AMEND_ERROR_MSG_CANNOT_SKIP_FINANCE
                                    : AWAL_Constants.VR_ERROR_MSG_CANNOT_SKIP_FINANCE
                            );
                        }
                    }
                }
            }
        }
    }

    /**
     * @description Checks if tracked fields have been updated and insert a history
     *  record for each one that did
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void logFieldHistory(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        Map<String, String> fieldsToTrackMap = SchemaUtils.getFieldSetAPINames(
            SObjectType.Opportunity.FieldSets.AWAL_FieldsToTrack.getFields()
        );

        List<SObject> historyRecordsToInsert = new List<SObject>();

        for (Opportunity opp : newList) {
            historyRecordsToInsert.addAll(
                HistoryTracker.createHistoryRecords(oldMap.get(opp.Id), opp, fieldsToTrackMap)
            );
        }

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

    /**
     * @description Creates batches of 10 records and pass them over to the Queueable class
     *  in order to send an email for Closed Won Opportunity records
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void sendClosedWonEmail(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();

        for (Opportunity opp : newList) {
            if (
                oldMap.get(opp.Id).StageName != opp.StageName &&
                opp.StageName == AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_WON
            ) {
                filteredOpps.add(opp);
            }
        }

        if (!filteredOpps.isEmpty()) {
            System.enqueueJob(new AwalClosedWonEmailQueueable(filteredOpps));
        }
    }

    /**
     * @description Filters out opps that should bypass validations
     * @param newList   Trigger.new
     * @param oldMap    Trigger.oldMap
     * @return List<Opportunity> opps that should be validated
     */
    public static List<Opportunity> filterBypassOpportunities(List<Opportunity> newList) {
        if (mastersMigratedOppsToBypass == null) {
            Set<Id> mastersOppIds = new Set<Id>();
            for (Opportunity opp : newList) {
                if (opp.Is_Migrated__c) {
                    mastersOppIds.add(opp.Migrated_From_Opportunity__c);
                }
            }
            if (!mastersOppIds.isEmpty()) {
                List<String> stagesList = new List<String>{
                    'Pass to Approver (KLS only)',
                    AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE,
                    AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_BA
                };
                mastersMigratedOppsToBypass = new Map<Id, Opportunity>(
                    [SELECT Id FROM Opportunity WHERE Id IN :mastersOppIds AND StageName IN :stagesList]
                );
            } else {
                mastersMigratedOppsToBypass = new Map<Id, Opportunity>();
            }
        }

        List<Opportunity> oppsToValidate = new List<Opportunity>();
        for (Opportunity opp : newList) {
            if (!mastersMigratedOppsToBypass.containsKey(opp.Migrated_From_Opportunity__c)) {
                oppsToValidate.add(opp);
            }
        }
        return oppsToValidate;
    }

    /**
     * @description Syncs the Opportunity AccountId with the ArtistOrLabelName__c field.
     * @param newList   Trigger.new
     * @param oldMap    Trigger.oldMap
     */
    public static void setAccountFromArtistOrLabel(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        for (Opportunity opp : newList) {
            if (oldMap == null || TriggerUtils.fieldHasChanged(oldMap.get(opp.Id), opp, 'ArtistOrLabelName__c')) {
                opp.AccountId = opp.ArtistOrLabelName__c;
            }
        }
    }

    /**
     * @description Validations for Amendments
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void validateAmendments(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AWAL)) {
            return;
        }
        List<Opportunity> amendments = new List<Opportunity>();
        Set<Id> parentOppIds = new Set<Id>();
        for (Opportunity opp : newList) {
            if (opp.Is_Amendment__c && TriggerUtils.fieldHasChanged(opp, oldMap.get(opp.Id), 'StageName')) {
                amendments.add(opp);
                parentOppIds.add(opp.Parent_Opportunity__c);
            }
        }

        if (!amendments.isEmpty()) {
            List<String> allFields = new List<String>{
                'IsPAndLToolAttached__c',
                'ContractingPartyCountry__c',
                'ManagerCountry__c',
                'LegalEntityNameId__c',
                'Estimated_Revenue_Base_Case__c'
            };

            List<String> requiredAtPassedToFinanceApprover = new List<String>{
                'CrossedWithPriorAgreements__c',
                'License_Period_In_Years__c'
            };

            List<String> requiredAtClose = new List<String>{
                'ClientLawyer__c',
                'LawyerFirm__c',
                'LawyerEmailAddress__c'
            };

            List<String> requiredBeyondDealDiscussions = new List<String>{
                'ArtistOrLabelName__c',
                'Deal_Type_Masters__c',
                'LegalEntityType__c',
                'ExistingAgreements__c',
                'Client_Deal_Introduction__c',
                'ClientServiceTier__c',
                'Primary_Genre__c',
                'Artist_Gender__c',
                'Artist_Ethnicity_Race__c',
                'Manager_Name__c',
                'Manager_Email__c',
                'ContractBasis__c',
                'EstimatedDelivery__c',
                'EstReleaseTimingDateEstimate__c',
                'BasisForLicensePeriod__c',
                'Scope_of_Deal__c',
                'MinimumNumberOfVideos__c',
                'CommittedRights__c',
                'VideoRights__c',
                'Territory__c',
                'AWALRevenueShare__c',
                'RevenueShareEscalation__c',
                'PreRecoupmentAccountingToThirdParty__c'
            };

            allFields.addAll(requiredAtClose);
            allFields.addAll(requiredAtPassedToFinanceApprover);
            allFields.addAll(requiredBeyondDealDiscussions);

            Map<Id, List<Opportunity>> parentOppsAmendMap = new Map<Id, List<Opportunity>>();
            String CLOSED_WON = AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_WON;
            String query =
                'SELECT Id, Parent_Opportunity__c, Is_Migrated__c, Is_Amendment__c, ' +
                String.join(allFields, ',') +
                ' FROM Opportunity' +
                ' WHERE (Parent_Opportunity__c = :parentOppIds OR Id = :parentOppIds)' +
                ' AND StageName = :CLOSED_WON' +
                ' AND ( ' +
                '       (Is_Amendment__c = FALSE AND Is_Migrated__c = FALSE) ' +
                '    OR (Is_Amendment__c = TRUE  AND Parent_Opportunity__r.Is_Migrated__c = FALSE) ' +
                '     )' +
                ' ORDER BY CreatedDate ASC';

            List<Opportunity> nonMigratedOpps = Database.query(query);
            Map<Id, Opportunity> nonMigratedOppsMap = new Map<Id, Opportunity>(nonMigratedOpps);

            for (Id parentOppId : parentOppIds) {
                parentOppsAmendMap.put(parentOppId, new List<Opportunity>());
            }
            for (Opportunity opp : nonMigratedOppsMap.values()) {
                Id key = opp.Parent_Opportunity__c ?? opp.Id;
                if (parentOppsAmendMap.containsKey(key)) {
                    parentOppsAmendMap.get(key).add(opp);
                }
            }
            List<Opportunity> filteredAmendments = new List<Opportunity>();
            for (Opportunity amend : amendments) {
                if (nonMigratedOppsMap.containsKey(amend.Id)) {
                    filteredAmendments.add(amend);
                    parentOppsAmendMap.get(amend.Parent_Opportunity__c).add(amend);
                }
            }
            amendments = filteredAmendments;

            Map<Id, Opportunity> amendmentSnapshotMap = new Map<Id, Opportunity>();
            for (Opportunity amendment : amendments) {
                Opportunity snapshot = new Opportunity();
                for (Opportunity opp : parentOppsAmendMap.get(amendment.Parent_Opportunity__c)) {
                    for (String field : allFields) {
                        if (opp.get(field) != null) {
                            snapshot.put(field, opp.get(field));
                        }
                    }
                }
                amendmentSnapshotMap.put(amendment.Id, snapshot);
            }

            for (Opportunity amendment : amendments) {
                Opportunity snapshot = amendmentSnapshotMap.get(amendment.Id);

                if (
                    amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_DEAL_DISCUSSIONS &&
                    amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_DEAL_TRACKING
                ) {
                    //AWAL_Is_PL_Tool_Attached_Validation
                    if (
                        amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_LOST &&
                        amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_NO_INTEREST &&
                        amendment.TCF_NewCumulative_USD__c > 0 &&
                        amendment.TCF_NewCumulative_USD__c < 250001 &&
                        snapshot.IsPAndLToolAttached__c != 'Yes'
                    ) {
                        amendment.addError('IsPAndLToolAttached__c', AWAL_Constants.VR_AMEND_ERROR_MSG_PL_TOOL);
                    }

                    //AWAL_Require_Contracting_Party_Country
                    if (
                        amendment.TCF_NewCumulative_USD__c > 100000 &&
                        String.isBlank(snapshot.ContractingPartyCountry__c) &&
                        snapshot.ClientServiceTier__c == 'Premium'
                    ) {
                        amendment.addError('ContractingPartyCountry__c', AWAL_Constants.VR_AMEND_ERROR_MSG_CP_COUNTRY);
                    }

                    //AWAL_Require_Manager_Country
                    if (
                        amendment.TCF_NewCumulative_USD__c > 100000 &&
                        String.isBlank(snapshot.ManagerCountry__c) &&
                        snapshot.ClientServiceTier__c == 'Premium'
                    ) {
                        amendment.addError('ManagerCountry__c', AWAL_Constants.VR_AMEND_ERROR_MSG_MANAGER_COUNTRY);
                    }

                    //AWAL_Require_License_Period_In_Years
                    if (
                        snapshot.License_Period_In_Years__c == null &&
                        snapshot.BasisForLicensePeriod__c != 'Set End Date'
                    ) {
                        amendment.addError(
                            'License_Period_In_Years__c',
                            AWAL_Constants.VR_AMEND_ERROR_MSG_LICENSE_PERIOD
                        );
                    }

                    //AWAL_Require_Legal_Entity_Name
                    if (
                        snapshot.LegalEntityNameId__c == null &&
                        (snapshot.Deal_Type_Masters__c == 'Existing Client' ||
                        snapshot.ExistingAgreements__c == 'Yes' ||
                        amendment.StageName == AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_WON)
                    ) {
                        amendment.addError('LegalEntityNameId__c', AWAL_Constants.VR_AMEND_ERROR_MSG_LEGAL_ENTITY_NAME);
                    }

                    //AWAL_EstimatedRevenuePassedToApprover
                    if (
                        snapshot.Estimated_Revenue_Base_Case__c == null &&
                        AWAL_Constants.ORDERED_STAGE_LIST.indexOf(amendment.StageName) >=
                        AWAL_Constants.ORDERED_STAGE_LIST.indexOf(AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_APPROVER)
                    ) {
                    }

                    //Required fields at Stages
                    if (
                        amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_LOST &&
                        amendment.StageName != AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_NO_INTEREST
                    ) {
                        for (String field : requiredBeyondDealDiscussions) {
                            if (snapshot.get(field) == null) {
                                amendment.addError(field, AWAL_Constants.VR_AMEND_ERROR_MSG_MISSING_REQUIRED_FIELDS);
                            }
                        }
                    }

                    if (amendment.StageName == AWAL_Constants.OPPORTUNITY_STAGE_PASSED_TO_FINANCE_APPROVER) {
                        for (String field : requiredAtPassedToFinanceApprover) {
                            if (snapshot.get(field) == null) {
                                amendment.addError(
                                    field,
                                    AWAL_Constants.VR_ERROR_MSG_MISSING_FIELDS_AT_FINANCE_APPROVER
                                );
                            }
                        }
                    }

                    if (amendment.StageName == AWAL_Constants.OPPORTUNITY_STAGE_CLOSED_WON) {
                        for (String field : requiredAtClose) {
                            if (snapshot.get(field) == null) {
                                amendment.addError(field, AWAL_Constants.VR_ERROR_MSG_MISSING_FIELDS_AT_CLOSE);
                            }
                        }
                    }
                }
            }
        }
    }
}