/***********************************************************************************************************
 * Name         : AdvancePaymentTriggerHelper
 * Purpose      : Implementation for methods called by AdvancePaymentTriggerHandler
 **********************************************************************************************************/
public without sharing class AdvancePaymentTriggerHelper {
    /**
     * @description Updates record Name with the 18 digit Id.
     * @param List<AdvancePayment__c> List of records triggered in trigger new after update
     */
    public static void updateNameWithRecordId(List<AdvancePayment__c> newList) {
        List<AdvancePayment__c> paymentsToUpdate = new List<AdvancePayment__c>();

        for (AdvancePayment__c ap : newList) {
            paymentsToUpdate.add(new AdvancePayment__c(Id = ap.Id, Name = ap.Id));
        }

        TriggerControl.disableTrigger(AdvancePaymentTriggerHandler.SELF);
        update paymentsToUpdate;
        TriggerControl.enableTrigger(AdvancePaymentTriggerHandler.SELF);
    }

    /**
     * @description Prepares IDs for async approver update in AdvancePayment__c.
     * @param List<AdvancePayment__c> List of records triggered in trigger new after update
     */
    public static void updateCurrentApproverNameAndId(List<AdvancePayment__c> newList) {
        Set<Id> advPaymentIds = new Set<Id>();

        for (AdvancePayment__c advPayment : newList) {
            if (
                advPayment.Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_STATUS ||
                advPayment.Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_MILESTONE_APPROVAL_STATUS
            ) {
                advPaymentIds.add(advPayment.Id);
            }
        }

        if (!advPaymentIds.isEmpty()) {
            updateCurrentApproverNameAndIdAsync(advPaymentIds);
        }
    }

    /**
     * @description future method that updates Approver__c in AdvancePayment__c after unlocking and relocking records.
     * @param Set<Id> List of the IDs related to the trigger.new
     */
    @Future
    public static void updateCurrentApproverNameAndIdAsync(Set<Id> advPaymentIds) {
        List<ProcessInstanceWorkitem> processInstanceWorkitems = [
            SELECT Id, Actor.Name, ActorId, ProcessInstance.Status, ProcessInstance.TargetObjectId
            FROM ProcessInstanceWorkitem
            WHERE
                ProcessInstance.TargetObjectId IN :advPaymentIds
                AND ProcessInstance.Status = :AP_Constants.PROCESS_INSTANCE_PENDING_STATUS
        ];

        Map<Id, String> recentApprovers = new Map<Id, String>();
        Map<Id, String> recentApproversIds = new Map<Id, String>();

        for (ProcessInstanceWorkitem pi : processInstanceWorkitems) {
            recentApprovers.put(pi.ProcessInstance.TargetObjectId, pi.Actor.Name);
            recentApproversIds.put(pi.ProcessInstance.TargetObjectId, pi.ActorId);
        }

        List<AdvancePayment__c> paymentsToUpdate = new List<AdvancePayment__c>();

        for (Id advPaymentId : recentApprovers.keySet()) {
            AdvancePayment__c advPayment = new AdvancePayment__c(Id = advPaymentId);
            if (advPayment != null) {
                String newApproverName = recentApprovers.get(advPaymentId);
                String newApproverId = recentApproversIds.get(advPaymentId);
                if (advPayment.ApproverId__c != newApproverId) {
                    advPayment.Approver__c = newApproverName;
                    advPayment.ApproverId__c = newApproverId;
                    paymentsToUpdate.add(advPayment);
                }
            }
        }

        if (!paymentsToUpdate.isEmpty()) {
            Approval.UnlockResult[] unlockResults = Approval.unlock(paymentsToUpdate, false);
            TriggerControl.disableTrigger(AdvancePaymentTriggerHandler.SELF);
            Database.update(paymentsToUpdate, false);
            TriggerControl.enableTrigger(AdvancePaymentTriggerHandler.SELF);
            Approval.LockResult[] lockResults = Approval.lock(paymentsToUpdate, false);
        }
    }

    /**
     * @description updates the rejection comment field when a request is rejected
     * @param List<AdvancePayment__c> List of records triggered in trigger new before update
     */
    public static void updateRejectionComment(List<AdvancePayment__c> newList) {
        Set<Id> paymentIds = new Set<Id>();
        for (AdvancePayment__c advPayment : newList) {
            if (advPayment.Status__c == AP_Constants.PROCESS_INSTANCE_REJECTED_STATUS) {
                paymentIds.add(advPayment.Id);
            }
        }
        if (!paymentIds.isEmpty()) {
            Map<Id, Id> mapPaymentIdMostRecentProcessInstanceId = new Map<Id, Id>();
            Map<Id, String> mapProcessInstanceComment = new Map<Id, String>();
            List<ProcessInstance> processIntancesList = [
                SELECT Id, TargetObjectId
                FROM ProcessInstance
                WHERE TargetObjectId IN :paymentIds AND Status = :AP_Constants.PROCESS_INSTANCE_REJECTED_STATUS
                ORDER BY CompletedDate ASC
            ];
            for (ProcessInstance process : processIntancesList) {
                mapPaymentIdMostRecentProcessInstanceId.put(process.TargetObjectId, process.Id);
            }
            List<ProcessInstanceStep> processIntancesStepList = [
                SELECT ProcessInstanceId, Comments
                FROM ProcessInstanceStep
                WHERE ProcessInstanceId IN :mapPaymentIdMostRecentProcessInstanceId.values()
                ORDER BY CreatedDate ASC
            ];
            for (ProcessInstanceStep step : processIntancesStepList) {
                mapProcessInstanceComment.put(step.ProcessInstanceId, step.Comments);
            }

            List<AdvancePayment__c> paymentsToUpdate = new List<AdvancePayment__c>();

            for (AdvancePayment__c advPayment : newList) {
                advPayment.RefusalReason__c = mapProcessInstanceComment.get(
                    mapPaymentIdMostRecentProcessInstanceId.get(advPayment.Id)
                );
            }
        }
    }

    /**
     * @description Calculates and updates withholding tax for AdvancePayment__c based on tax data object
     * @param List<AdvancePayment__c> List of records triggered in trigger new before update/insert
     * @param Map<Id, AdvancePayment__c> Map of updated records from Trigger.oldMap
     */
    public static void updateWithholdingRate(List<AdvancePayment__c> newList, Map<Id, AdvancePayment__c> oldMap) {
        List<String> relevantRecordTypes = ConfigUtils.getConfigMultipleStrings(
            ConfigUtils.WITHHOLDING_RATE_RECORD_TYPES
        );
        Map<Id, Schema.RecordTypeInfo> recordTypeInfoById = Schema.SObjectType.AdvancePayment__c.getRecordTypeInfosById();
        List<AdvancePayment__c> paymentsOptOut = new List<AdvancePayment__c>();
        List<AdvancePayment__c> paymentsRequiringTaxTreaty = new List<AdvancePayment__c>();
        Set<String> countriesRequiringTaxTreaty = new Set<String>();

        for (AdvancePayment__c payment : newList) {
            if (
                oldMap == null ||
                shouldRecalculateTax(payment, oldMap.get(payment.Id), relevantRecordTypes, recordTypeInfoById)
            ) {
                if (
                    AP_Constants.US_WHT_TREATY_COUNTRIES.contains(payment.CountryName__c) &&
                    payment.UsWhtTreatyStatus__c == AP_Constants.WHT_OPT_OUT
                ) {
                    paymentsOptOut.add(payment);
                } else {
                    paymentsRequiringTaxTreaty.add(payment);
                    countriesRequiringTaxTreaty.add(payment.CountryName__c);
                }
            }
        }

        for (AdvancePayment__c payment : paymentsOptOut) {
            Decimal usSourceRate = payment.USSourceSales__c == null ? 0 : payment.USSourceSales__c / 100;
            payment.WithholdingTax__c = payment.GrossAmount__c * usSourceRate * AP_Constants.WHT_OPT_OUT_FIX_RATE;
        }
        if (!paymentsRequiringTaxTreaty.isEmpty()) {
            Map<String, List<WithholdingTaxCriteria__c>> taxCriteriaByCountry = getTaxCriteriaByCountry(
                countriesRequiringTaxTreaty
            );
            if (!taxCriteriaByCountry.isEmpty()) {
                applyTaxTreatyForPayments(paymentsRequiringTaxTreaty, taxCriteriaByCountry);
            }
        }
    }

    /**
     * Determines whether the WithholdingTax__c field should be recalculated for a given payment.
     * @param payment The current AdvancePayment__c record.
     * @param oldMap of AdvancePayment__c records, keyed by Id.
     * @param relevantRecordTypes A list of developer names for RecordTypes that are part of this calculation.
     * @param recordTypeInfoById A map of Record Type Ids to RecordTypeInfo for AdvancePayment__c.
     * @return true if a recalculation is required, false otherwise.
     */
    private static Boolean shouldRecalculateTax(
        AdvancePayment__c payment,
        AdvancePayment__c oldPayment,
        List<String> relevantRecordTypes,
        Map<Id, Schema.RecordTypeInfo> recordTypeInfoById
    ) {
        if (
            !relevantRecordTypes.contains(recordTypeInfoById.get(payment.RecordTypeId).DeveloperName) ||
            String.isBlank(payment.CountryName__c)
        ) {
            return false;
        }

        return (oldPayment.USSourceSales__c != payment.USSourceSales__c ||
        oldPayment.GrossAmount__c != payment.GrossAmount__c ||
        oldPayment.CountryName__c != payment.CountryName__c ||
        oldPayment.UsWhtTreatyStatus__c != payment.UsWhtTreatyStatus__c);
    }

    /**
     * Fetches the WithholdingTaxCriteria__c records for each specified country and
     * returns a map of CountryName__c -> List of WithholdingTaxCriteria__c.     *
     * @param countriesRequiringTaxTreaty The set of country names for which tax criteria are needed.
     * @return A map where the key is the country's name (CountryName__c) and the value is a list
     *         of WithholdingTaxCriteria__c.
     */
    private static Map<String, List<WithholdingTaxCriteria__c>> getTaxCriteriaByCountry(
        Set<String> countriesRequiringTaxTreaty
    ) {
        Map<String, List<WithholdingTaxCriteria__c>> taxCriteriaByCountry = new Map<String, List<WithholdingTaxCriteria__c>>();

        List<WithholdingTaxCriteria__c> whtCriteriaList = [
            SELECT CountryName__c, TaxTreatyWithholdingRate__c, TaxManagedDate__c
            FROM WithholdingTaxCriteria__c
            WHERE CountryName__c IN :countriesRequiringTaxTreaty
            ORDER BY CountryName__c, TaxManagedDate__c DESC
        ];

        for (WithholdingTaxCriteria__c tax : whtCriteriaList) {
            if (!taxCriteriaByCountry.containsKey(tax.CountryName__c)) {
                taxCriteriaByCountry.put(tax.CountryName__c, new List<WithholdingTaxCriteria__c>());
            }
            taxCriteriaByCountry.get(tax.CountryName__c).add(tax);
        }

        return taxCriteriaByCountry;
    }

    /**
     * Applies the appropriate tax treaty rates to the given payments.
     * @param paymentsRequiringTaxTreaty A list of AdvancePayment__c records that require treaty calculations.
     * @param taxCriteriaByCountry A map of country name -> list of WithholdingTaxCriteria__c records,
     *                             sorted by TaxManagedDate__c descending.
     */
    private static void applyTaxTreatyForPayments(
        List<AdvancePayment__c> paymentsRequiringTaxTreaty,
        Map<String, List<WithholdingTaxCriteria__c>> taxCriteriaByCountry
    ) {
        for (AdvancePayment__c payment : paymentsRequiringTaxTreaty) {
            List<WithholdingTaxCriteria__c> taxList = taxCriteriaByCountry.get(payment.CountryName__c);

            for (WithholdingTaxCriteria__c tax : taxList) {
                DateTime comparisonDate = payment.CreatedDate != null ? payment.CreatedDate : System.now();

                if (comparisonDate >= tax.TaxManagedDate__c) {
                    Decimal taxTreatyRatePercent = tax.TaxTreatyWithholdingRate__c / 100;

                    payment.TaxTreatyWithholdingRate__c = tax.TaxTreatyWithholdingRate__c;
                    if (
                        payment.UsWhtTreatyStatus__c == AP_Constants.WHT_OPT_IN &&
                        AP_Constants.US_WHT_TREATY_COUNTRIES.contains(payment.CountryName__c)
                    ) {
                        payment.WithholdingTax__c = payment.GrossAmount__c * taxTreatyRatePercent;
                    } else {
                        Decimal usSourceRate = (payment.USSourceSales__c == null) ? 0 : payment.USSourceSales__c / 100;
                        payment.WithholdingTax__c = payment.GrossAmount__c * usSourceRate * taxTreatyRatePercent;
                    }

                    break;
                }
            }
        }
    }

    /**
     * @description Prevents a user from approving, rejecting (or making any kind of change)
     *  during the approval process
     * @param List<AdvancePayment__c> List of records triggered in trigger new before update/insert
     * @param Map<Id, AdvancePayment__c> Map of updated records from Trigger.oldMap
     * @return List<AdvancePayment__c> payments eligible to undergo the approval status
     *  validation after being checked for self-approval or self-rejection
     */
    public static List<AdvancePayment__c> preventUpdatingOwnRecords(
        List<AdvancePayment__c> newList,
        Map<Id, AdvancePayment__c> oldMap
    ) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AP)) {
            return new List<AdvancePayment__c>();
        }

        List<AdvancePayment__c> eligiblePaymentsForApproval = new List<AdvancePayment__c>();
        for (AdvancePayment__c payment : newList) {
            if (
                (oldMap.get(payment.Id).Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_MILESTONE_APPROVAL_STATUS ||
                oldMap.get(payment.Id).Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_STATUS) &&
                payment.Status__c != AP_Constants.ADVANCE_PAYMENT_RECALLED_STATUS &&
                payment.Status__c != AP_Constants.ADVANCE_PAYMENT_CANCELED_STATUS &&
                payment.Submitter__c != null &&
                UserInfo.getUserId() == payment.Submitter__c
            ) {
                payment.addError(AP_Constants.CANNOT_APPROVE_REJECT_OWN_PAYMENT_REQUEST);
            } else {
                eligiblePaymentsForApproval.add(payment);
            }
        }
        return eligiblePaymentsForApproval;
    }

    /**
     * @description Validates that MilestoneReached__c must be true
     *  when Status__c is being changed from "Pending Milestone Approval" to "Pending Approval"
     * @param List<AdvancePayment__c> newList - List of payments
     * @param Map<Id, AdvancePayment__c> oldMap - Map of old records from trigger.oldMap
     */
    public static void validatePendingApprovalStatus(
        List<AdvancePayment__c> eligiblePaymentsForApproval,
        Map<Id, AdvancePayment__c> oldMap
    ) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AP)) {
            return;
        }
        for (AdvancePayment__c payment : eligiblePaymentsForApproval) {
            if (
                payment.Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_STATUS &&
                oldMap.get(payment.Id).Status__c == AP_Constants.ADVANCE_PAYMENT_PENDING_MILESTONE_APPROVAL_STATUS
            ) {
                if (!payment.MilestoneReached__c) {
                    payment.addError(AP_Constants.MILESTONE_APPROVAL_REQUIREMENT);
                }
            }
        }
    }

    /**
     * @description Validates that fields are only updated according to the status of the record.
     *  When the status is "Approved", only fields in the Field Sets "PaymentConfirmationDetailsFields"
     *    and "EditableFieldsAfterApproval" can be updated.
     *  When the status is "Canceled", only fields in the Field Set "EditableFieldsAfterCancel" can be updated.
     *  An error is thrown if non-editable fields are modified for either status.
     * @param List<AdvancePayment__c> payments - List of payments being updated
     * @param Map<Id, AdvancePayment__c> oldMap - Map of old records from trigger.oldMap
     */
    public static void preventUnallowedFieldUpdates(
        List<AdvancePayment__c> payments,
        Map<Id, AdvancePayment__c> oldMap
    ) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AP)) {
            return;
        }
        List<AdvancePayment__c> restrictedPayments = new List<AdvancePayment__c>();

        for (AdvancePayment__c payment : payments) {
            String oldStatus = oldMap.get(payment.Id).Status__c;

            if (
                oldStatus == AP_Constants.ADVANCE_PAYMENT_APPROVED_STATUS ||
                oldStatus == AP_Constants.ADVANCE_PAYMENT_CANCELED_STATUS
            ) {
                restrictedPayments.add(payment);
            }
        }

        if (!restrictedPayments.isEmpty()) {
            Set<String> allEditableFieldsAfterApproval = new Set<String>();
            allEditableFieldsAfterApproval.addAll(
                SchemaUtils.getFieldSetAPINames(
                        SObjectType.AdvancePayment__c.FieldSets.PaymentConfirmationDetailsFields.getFields()
                    )
                    .keySet()
            );
            allEditableFieldsAfterApproval.addAll(
                SchemaUtils.getFieldSetAPINames(
                        SObjectType.AdvancePayment__c.FieldSets.EditableFieldsAfterApproval.getFields()
                    )
                    .keySet()
            );

            Set<String> allEditableFieldsAfterCancel = new Set<String>();
            allEditableFieldsAfterCancel.addAll(
                SchemaUtils.getFieldSetAPINames(
                        SObjectType.AdvancePayment__c.FieldSets.EditableFieldsAfterCancel.getFields()
                    )
                    .keySet()
            );

            List<Schema.SObjectField> allPaymentFields = AdvancePayment__c.SObjectType.getDescribe()
                .fields.getMap()
                .values();

            for (AdvancePayment__c payment : restrictedPayments) {
                AdvancePayment__c oldPayment = oldMap.get(payment.Id);
                String oldStatus = oldPayment.Status__c;

                List<Schema.SObjectField> fieldsWithError = new List<Schema.SObjectField>();

                if (oldStatus == AP_Constants.ADVANCE_PAYMENT_CANCELED_STATUS) {
                    SchemaUtils.checkEditableFields(
                        allPaymentFields,
                        allEditableFieldsAfterCancel,
                        fieldsWithError,
                        payment,
                        oldPayment
                    );
                } else {
                    if (
                        payment.get(AdvancePayment__c.LabelClientName__c) !=
                        oldPayment.get(AdvancePayment__c.LabelClientName__c) &&
                        payment.Status__c != AP_Constants.ADVANCE_PAYMENT_CANCELED_STATUS
                    ) {
                        fieldsWithError.add(AdvancePayment__c.LabelClientName__c);
                    }

                    SchemaUtils.checkEditableFields(
                        allPaymentFields,
                        allEditableFieldsAfterApproval,
                        fieldsWithError,
                        payment,
                        oldPayment
                    );
                }

                if (!fieldsWithError.isEmpty()) {
                    String errorMsg = oldStatus == AP_Constants.ADVANCE_PAYMENT_APPROVED_STATUS
                        ? AP_Constants.ERROR_MESSAGE_EDIT_APPROVED_PAYMENT
                        : AP_Constants.ERROR_MESSAGE_EDIT_CANCELED_PAYMENT;
                    for (Schema.SObjectField field : fieldsWithError) {
                        payment.addError(field, errorMsg);
                    }
                }
            }
        }
    }

    /**
     * @description Validates if fields from the PaymentConfirmationDetailsFields field set
     *   are being modified, and checks if the current user has the necessary custom permission
     *   to perform those updates based on the record type of each Advance Payment.
     * @param List<AdvancePayment__c> payments - List of Advance Payment records being updated.
     * @param Map<Id, AdvancePayment__c> oldMap - Map of old Advance Payment records from trigger.oldMap.
     */
    public static void validatePaymentConfirmationFields(
        List<AdvancePayment__c> payments,
        Map<Id, AdvancePayment__c> oldMap
    ) {
        if (BypassCodeValidationUtils.shouldBypass(BypassCodeValidationUtils.AP)) {
            return;
        }

        if (!FeatureManagement.checkPermission(AP_Constants.EDIT_PAYMENT_DETAILS_BYPASS)) {
            List<AdvancePayment__c> updatedPaymentsToCheck = new List<AdvancePayment__c>();

            Set<String> paymentConfirmationFields = SchemaUtils.getFieldSetAPINames(
                    SObjectType.AdvancePayment__c.FieldSets.PaymentConfirmationDetailsFields.getFields()
                )
                .keySet();

            for (AdvancePayment__c newPayment : payments) {
                AdvancePayment__c oldPayment = oldMap.get(newPayment.Id);

                for (String fieldName : paymentConfirmationFields) {
                    if (newPayment.get(fieldName) != oldPayment.get(fieldName)) {
                        updatedPaymentsToCheck.add(newPayment);
                        break;
                    }
                }
            }

            if (!updatedPaymentsToCheck.isEmpty()) {
                Map<Id, RecordType> recordTypes = new Map<Id, RecordType>(
                    [SELECT Id, DeveloperName FROM RecordType WHERE SObjectType = :AdvancePaymentTriggerHandler.SELF]
                );

                for (AdvancePayment__c advPayment : updatedPaymentsToCheck) {
                    if (
                        !FeatureManagement.checkPermission(
                            AP_Constants.CUSTOM_PERMISSION_ADV_PAYMENT_EDIT_PAYMENT_DETAILS_PREFIX +
                            recordTypes.get(advPayment.RecordTypeId).DeveloperName
                        )
                    ) {
                        advPayment.addError(AP_Constants.ERROR_MESSAGE_CANNOT_EDIT_PAYMENT_DETAILS);
                    }
                }
            }
        }
    }
}