/***********************************************************************************************************
 * Name         : CurrencyUtils
 * Purpose      : Utils class to handle currency conversions methods
 **********************************************************************************************************/
public with sharing class CurrencyUtils {
    public static final String ISO_CODE = 'IsoCode';
    public static final String CURRENCY_ISO_CODE = 'CurrencyIsoCode';
    public static final String EXCHANGE_RATE_FIELD = 'ExchangeRate__c';
    public static final String CLOSE_DATE_FIELD = 'CloseDate';

    /**
     * @description converts specified currency fields in a list of SObjects to USD
     *   based on historical exchange rates, updating the corresponding fields with the converted values.
     * @param sObjectList A list of SObject records to be processed for currency conversion.
     * @param conversionDateField The API name of the field that contains the date used to determine the applicable conversion rate.
     * @param amountToConvertAndConvertedFieldsMap A map where the key is the API name
     *   of the field that contains the amount to be converted, and the value is the API name of the field where the converted USD amount will be stored.
     * @param currenciesList  A list of currency ISO codes that should be considered when retrieving conversion rates.
     */
    public static void calculateValueInUSD(
        List<SObject> sObjectList,
        String conversionDateField,
        Map<String, String> amountToConvertAndConvertedFieldsMap,
        List<String> currenciesList
    ) {
        Map<String, List<DatedConversionRate>> mapCurrencyDatedConversionRate = new Map<String, List<DatedConversionRate>>();

        List<DatedConversionRate> conversionRateList = [
            SELECT Id, IsoCode, StartDate, NextStartDate, ConversionRate
            FROM DatedConversionRate
            WHERE IsoCode IN :currenciesList
            ORDER BY IsoCode, StartDate DESC
        ];

        for (DatedConversionRate convRate : conversionRateList) {
            SchemaUtils.addToFieldNameSObjectListMap(mapCurrencyDatedConversionRate, convRate, ISO_CODE);
        }

        for (SObject sObj : sObjectList) {
            Date conversionDate = (Date) sObj.get(conversionDateField);
            String currencyIsoCode = (String) sObj.get(CURRENCY_ISO_CODE);
            if (conversionDate != null && mapCurrencyDatedConversionRate.containsKey(currencyIsoCode)) {
                for (DatedConversionRate convRate : mapCurrencyDatedConversionRate.get(currencyIsoCode)) {
                    if (conversionDate >= convRate.StartDate && conversionDate <= convRate.NextStartDate.addDays(-1)) {
                        for (String amountToConvertField : amountToConvertAndConvertedFieldsMap.keySet()) {
                            Decimal sourceAmount = (Decimal) sObj.get(amountToConvertField);
                            Decimal usdValue = (1 / convRate.ConversionRate) * sourceAmount;
                            String convertedAmountField = amountToConvertAndConvertedFieldsMap.get(
                                amountToConvertField
                            );
                            sObj.put(convertedAmountField, usdValue);
                        }

                        break;
                    }
                }
            }
        }
    }

    /**
     * @description Updates the exchange rate field (ExchangeRate__c) for a list of Opportunity records
     * @param newList Trigger.new
     * @param oldMap Trigger.oldMap
     */
    public static void refreshExchangeRate(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> filteredOpps = new List<Opportunity>();
        List<String> currenciesList = new List<String>();

        for (Opportunity opp : newList) {
            Opportunity oldOpp = (oldMap != null) ? oldMap.get(opp.Id) : null;

            Boolean shouldRefresh =
                oldOpp == null ||
                oldOpp.CloseDate != opp.CloseDate ||
                oldOpp.CurrencyIsoCode != opp.CurrencyIsoCode ||
                opp.ExchangeRateRefresh__c == true;

            if (shouldRefresh) {
                opp.ExchangeRateRefresh__c = false;
                filteredOpps.add(opp);
                currenciesList.add(opp.CurrencyIsoCode);
            }
        }

        if (!filteredOpps.isEmpty()) {
            updateExchangeRateField(
                filteredOpps,
                CurrencyUtils.CLOSE_DATE_FIELD,
                CurrencyUtils.EXCHANGE_RATE_FIELD,
                currenciesList
            );
            FormulaUtils.recalcFormulasPreservingAddressCodes(filteredOpps);
        }
    }

    /**
     * @description Updates the specified exchange rate field on a list of SObject records
     *              based on the provided conversion date field, exchange rate field name,
     *              and a list of currencies.
     * @param sObjectList List of SObject records for which the exchange rate field needs to be updated.
     * @param conversionDateField API name of the date field used to determine the applicable conversion rate.
     * @param exchangeRateFieldName API name of the field where the conversion rate will be stored.
     * @param currenciesList List of currency ISO codes to filter the DatedConversionRate records.
     */
    public static void updateExchangeRateField(
        List<SObject> sObjectList,
        String conversionDateField,
        String exchangeRateFieldName,
        List<String> currenciesList
    ) {
        Map<String, List<DatedConversionRate>> mapCurrencyDatedConversionRate = new Map<String, List<DatedConversionRate>>();

        List<DatedConversionRate> conversionRateList = [
            SELECT Id, IsoCode, StartDate, NextStartDate, ConversionRate
            FROM DatedConversionRate
            WHERE IsoCode IN :currenciesList
            ORDER BY IsoCode, StartDate DESC
        ];

        for (DatedConversionRate convRate : conversionRateList) {
            SchemaUtils.addToFieldNameSObjectListMap(mapCurrencyDatedConversionRate, convRate, ISO_CODE);
        }

        for (SObject sObj : sObjectList) {
            Date conversionDate = (Date) sObj.get(conversionDateField);
            String currencyIsoCode = (String) sObj.get(CURRENCY_ISO_CODE);
            if (conversionDate != null && mapCurrencyDatedConversionRate.containsKey(currencyIsoCode)) {
                for (DatedConversionRate convRate : mapCurrencyDatedConversionRate.get(currencyIsoCode)) {
                    if (conversionDate >= convRate.StartDate && conversionDate <= convRate.NextStartDate.addDays(-1)) {
                        sObj.put(exchangeRateFieldName, convRate.ConversionRate);
                        break;
                    }
                }
            }
        }
    }
}