// File: crates/router/src/core/payments/payment_methods.rs

//! Contains functions of payment methods that are used in payments
//! one of such functions is `list_payment_methods`

use std::{
    collections::{BTreeMap, HashSet},
    str::FromStr,
};

use common_utils::{
    ext_traits::{OptionExt, ValueExt},
    id_type,
};
use error_stack::ResultExt;
use hyperswitch_interfaces::secrets_interface::secret_state::RawSecret;

use super::errors;
use crate::{
    configs::settings,
    core::{payment_methods, payments::helpers},
    db::errors::StorageErrorExt,
    logger, routes,
    types::{self, api, domain, storage},
};

#[cfg(feature = "v2")]
pub async fn list_payment_methods(
    state: routes::SessionState,
    platform: domain::Platform,
    profile: domain::Profile,
    payment_id: id_type::GlobalPaymentId,
    req: api_models::payments::ListMethodsForPaymentsRequest,
    header_payload: &hyperswitch_domain_models::payments::HeaderPayload,
) -> errors::RouterResponse<api_models::payments::PaymentMethodListResponseForPayments> {
    let db = &*state.store;

    let payment_intent = db
        .find_payment_intent_by_id(
            &payment_id,
            platform.get_processor().get_key_store(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    validate_payment_status_for_payment_method_list(payment_intent.status)?;

    let payment_connector_accounts = db
        .list_enabled_connector_accounts_by_profile_id(
            profile.get_id(),
            platform.get_processor().get_key_store(),
            common_enums::ConnectorType::PaymentProcessor,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("error when fetching merchant connector accounts")?;

    let customer_payment_methods = match &payment_intent.customer_id {
        Some(customer_id) => Some(
            payment_methods::list_customer_payment_methods_core(&state, &platform, customer_id)
                .await?,
        ),
        None => None,
    };

    let response =
        FlattenedPaymentMethodsEnabled(hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts))
            .perform_filtering(
                &state,
                &platform,
                profile.get_id(),
                &req,
                &payment_intent,
            ).await?
            .store_gift_card_mca_in_redis(&payment_id, db, &profile).await
            .merge_and_transform()
            .get_required_fields(RequiredFieldsInput::new(state.conf.required_fields.clone(), payment_intent.setup_future_usage))
            .perform_surcharge_calculation()
            .populate_pm_subtype_specific_data(&state.conf.bank_config)
            .generate_response(customer_payment_methods);

    Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
        response,
    ))
}

/// Container for the inputs required for the required fields
struct RequiredFieldsInput {
    required_fields_config: settings::RequiredFields,
    setup_future_usage: common_enums::FutureUsage,
}

impl RequiredFieldsInput {
    fn new(
        required_fields_config: settings::RequiredFields,
        setup_future_usage: common_enums::FutureUsage,
    ) -> Self {
        Self {
            required_fields_config,
            setup_future_usage,
        }
    }
}

trait GetRequiredFields {
    fn get_required_fields(
        &self,
        payment_method_enabled: &MergedEnabledPaymentMethod,
    ) -> Option<&settings::RequiredFieldFinal>;
}

impl GetRequiredFields for settings::RequiredFields {
    fn get_required_fields(
        &self,
        payment_method_enabled: &MergedEnabledPaymentMethod,
    ) -> Option<&settings::RequiredFieldFinal> {
        self.0
            .get(&payment_method_enabled.payment_method_type)
            .and_then(|required_fields_for_payment_method| {
                required_fields_for_payment_method
                    .0
                    .get(&payment_method_enabled.payment_method_subtype)
            })
            .map(|connector_fields| &connector_fields.fields)
            .and_then(|connector_hashmap| {
                payment_method_enabled
                    .connectors
                    .first()
                    .and_then(|connector| connector_hashmap.get(connector))
            })
    }
}

struct FlattenedPaymentMethodsEnabled(
    hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled,
);

/// Container for the filtered payment methods
struct FilteredPaymentMethodsEnabled(
    Vec<hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector>,
);

impl FilteredPaymentMethodsEnabled {
    fn merge_and_transform(self) -> MergedEnabledPaymentMethodTypes {
        let values = self
            .0
            .into_iter()
            // BTreeMap used to ensure soretd response, otherwise the response is arbitrarily ordered
            .fold(BTreeMap::new(), |mut acc, item| {
                let key = (
                    item.payment_method,
                    item.payment_methods_enabled.payment_method_subtype,
                );
                let (experiences, connectors) = acc
                    .entry(key)
                    // HashSet used to ensure payment_experience does not have duplicates, due to multiple connectors for a pm_subtype
                    .or_insert_with(|| (HashSet::new(), Vec::new()));

                if let Some(experience) = item.payment_methods_enabled.payment_experience {
                    experiences.insert(experience);
                }
                connectors.push(item.connector);

                acc
            })
            .into_iter()
            .map(
                |(
                    (payment_method_type, payment_method_subtype),
                    (payment_experience, connectors),
                )| {
                    MergedEnabledPaymentMethod {
                        payment_method_type,
                        payment_method_subtype,
                        payment_experience: if payment_experience.is_empty() {
                            None
                        } else {
                            Some(payment_experience.into_iter().collect())
                        },
                        connectors,
                    }
                },
            )
            .collect();
        MergedEnabledPaymentMethodTypes(values)
    }
    async fn store_gift_card_mca_in_redis(
        self,
        payment_id: &id_type::GlobalPaymentId,
        db: &dyn crate::db::StorageInterface,
        profile: &domain::Profile,
    ) -> Self {
        let gift_card_connector_id = self
            .0
            .iter()
            .find(|item| item.payment_method == common_enums::PaymentMethod::GiftCard)
            .map(|item| &item.merchant_connector_id);

        if let Some(gift_card_mca) = gift_card_connector_id {
            let gc_key = payment_id.get_gift_card_connector_key();
            let redis_expiry = profile
                .get_order_fulfillment_time()
                .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME);

            let redis_conn = db
                .get_redis_conn()
                .map_err(|redis_error| logger::error!(?redis_error))
                .ok();

            if let Some(rc) = redis_conn {
                rc.set_key_with_expiry(
                    &gc_key.as_str().into(),
                    gift_card_mca.get_string_repr().to_string(),
                    redis_expiry,
                )
                .await
                .attach_printable("Failed to store gift card mca_id in redis")
                .unwrap_or_else(|error| {
                    logger::error!(?error);
                })
            };
        } else {
            logger::error!(
                "Could not find any configured MCA supporting gift card for payment_id -> {}",
                payment_id.get_string_repr()
            );
        }

        self
    }
}

/// Element container to hold the filtered payment methods with payment_experience and connectors merged for a pm_subtype
struct MergedEnabledPaymentMethod {
    payment_method_subtype: common_enums::PaymentMethodType,
    payment_method_type: common_enums::PaymentMethod,
    payment_experience: Option<Vec<common_enums::PaymentExperience>>,
    connectors: Vec<api_models::enums::Connector>,
}

/// Container to hold the filtered payment methods with payment_experience and connectors merged for a pm_subtype
struct MergedEnabledPaymentMethodTypes(Vec<MergedEnabledPaymentMethod>);

impl MergedEnabledPaymentMethodTypes {
    fn get_required_fields(
        self,
        input: RequiredFieldsInput,
    ) -> RequiredFieldsForEnabledPaymentMethodTypes {
        let required_fields_config = input.required_fields_config;
        let is_cit_transaction = input.setup_future_usage == common_enums::FutureUsage::OffSession;

        let required_fields_info = self
            .0
            .into_iter()
            .map(|payment_methods_enabled| {
                let required_fields =
                    required_fields_config.get_required_fields(&payment_methods_enabled);

                let required_fields = required_fields
                    .map(|required_fields| {
                        let common_required_fields = required_fields
                            .common
                            .iter()
                            .flatten()
                            .map(ToOwned::to_owned);

                        // Collect mandate required fields because this is for zero auth mandates only
                        let mandate_required_fields = required_fields
                            .mandate
                            .iter()
                            .flatten()
                            .map(ToOwned::to_owned);

                        // Collect non-mandate required fields because this is for zero auth mandates only
                        let non_mandate_required_fields = required_fields
                            .non_mandate
                            .iter()
                            .flatten()
                            .map(ToOwned::to_owned);

                        // Combine mandate and non-mandate required fields based on setup_future_usage
                        if is_cit_transaction {
                            common_required_fields
                                .chain(non_mandate_required_fields)
                                .collect::<Vec<_>>()
                        } else {
                            common_required_fields
                                .chain(mandate_required_fields)
                                .collect::<Vec<_>>()
                        }
                    })
                    .unwrap_or_default();

                RequiredFieldsForEnabledPaymentMethod {
                    required_fields,
                    payment_method_type: payment_methods_enabled.payment_method_type,
                    payment_method_subtype: payment_methods_enabled.payment_method_subtype,
                    payment_experience: payment_methods_enabled.payment_experience,
                    connectors: payment_methods_enabled.connectors,
                }
            })
            .collect();

        RequiredFieldsForEnabledPaymentMethodTypes(required_fields_info)
    }
}

/// Element container to hold the filtered payment methods with required fields
struct RequiredFieldsForEnabledPaymentMethod {
    required_fields: Vec<api_models::payment_methods::RequiredFieldInfo>,
    payment_method_subtype: common_enums::PaymentMethodType,
    payment_method_type: common_enums::PaymentMethod,
    payment_experience: Option<Vec<common_enums::PaymentExperience>>,
    connectors: Vec<api_models::enums::Connector>,
}

/// Container to hold the filtered payment methods enabled with required fields
struct RequiredFieldsForEnabledPaymentMethodTypes(Vec<RequiredFieldsForEnabledPaymentMethod>);

impl RequiredFieldsForEnabledPaymentMethodTypes {
    fn perform_surcharge_calculation(
        self,
    ) -> RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes {
        // TODO: Perform surcharge calculation
        let details_with_surcharge = self
            .0
            .into_iter()
            .map(
                |payment_methods_enabled| RequiredFieldsAndSurchargeForEnabledPaymentMethodType {
                    payment_method_type: payment_methods_enabled.payment_method_type,
                    required_fields: payment_methods_enabled.required_fields,
                    payment_method_subtype: payment_methods_enabled.payment_method_subtype,
                    payment_experience: payment_methods_enabled.payment_experience,
                    surcharge: None,
                    connectors: payment_methods_enabled.connectors,
                },
            )
            .collect();

        RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes(details_with_surcharge)
    }
}

/// Element Container to hold the filtered payment methods enabled with required fields and surcharge
struct RequiredFieldsAndSurchargeForEnabledPaymentMethodType {
    required_fields: Vec<api_models::payment_methods::RequiredFieldInfo>,
    payment_method_subtype: common_enums::PaymentMethodType,
    payment_method_type: common_enums::PaymentMethod,
    payment_experience: Option<Vec<common_enums::PaymentExperience>>,
    connectors: Vec<api_models::enums::Connector>,
    surcharge: Option<api_models::payment_methods::SurchargeDetailsResponse>,
}

/// Container to hold the filtered payment methods enabled with required fields and surcharge
struct RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes(
    Vec<RequiredFieldsAndSurchargeForEnabledPaymentMethodType>,
);

fn get_pm_subtype_specific_data(
    bank_config: &settings::BankRedirectConfig,
    payment_method_type: common_enums::enums::PaymentMethod,
    payment_method_subtype: common_enums::enums::PaymentMethodType,
    connectors: &[api_models::enums::Connector],
) -> Option<api_models::payment_methods::PaymentMethodSubtypeSpecificData> {
    match payment_method_type {
        // TODO: Return card_networks
        common_enums::PaymentMethod::Card | common_enums::PaymentMethod::CardRedirect => None,

        common_enums::PaymentMethod::BankRedirect
        | common_enums::PaymentMethod::BankTransfer
        | common_enums::PaymentMethod::BankDebit
        | common_enums::PaymentMethod::OpenBanking => {
            if let Some(connector_bank_names) = bank_config.0.get(&payment_method_subtype) {
                let bank_names = connectors
                    .iter()
                    .filter_map(|connector| {
                        connector_bank_names.0.get(&connector.to_string())
                            .map(|connector_hash_set| {
                                connector_hash_set.banks.clone()
                            })
                            .or_else(|| {
                                logger::debug!("Could not find any configured connectors for payment_method -> {payment_method_subtype} for connector -> {connector}");
                                None
                            })
                    })
                    .flatten()
                    .collect();
                Some(
                    api_models::payment_methods::PaymentMethodSubtypeSpecificData::Bank {
                        bank_names,
                    },
                )
            } else {
                logger::debug!("Could not find any configured banks for payment_method -> {payment_method_subtype}");
                None
            }
        }

        common_enums::PaymentMethod::PayLater
        | common_enums::PaymentMethod::Wallet
        | common_enums::PaymentMethod::Crypto
        | common_enums::PaymentMethod::Reward
        | common_enums::PaymentMethod::RealTimePayment
        | common_enums::PaymentMethod::Upi
        | common_enums::PaymentMethod::Voucher
        | common_enums::PaymentMethod::GiftCard
        | common_enums::PaymentMethod::MobilePayment => None,
    }
}

impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes {
    fn populate_pm_subtype_specific_data(
        self,
        bank_config: &settings::BankRedirectConfig,
    ) -> RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes {
        let response_payment_methods = self
            .0
            .into_iter()
            .map(|payment_methods_enabled| {
                RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodType {
                    payment_method_type: payment_methods_enabled.payment_method_type,
                    payment_method_subtype: payment_methods_enabled.payment_method_subtype,
                    payment_experience: payment_methods_enabled.payment_experience,
                    required_fields: payment_methods_enabled.required_fields,
                    surcharge: payment_methods_enabled.surcharge,
                    pm_subtype_specific_data: get_pm_subtype_specific_data(
                        bank_config,
                        payment_methods_enabled.payment_method_type,
                        payment_methods_enabled.payment_method_subtype,
                        &payment_methods_enabled.connectors,
                    ),
                }
            })
            .collect();

        RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes(
            response_payment_methods,
        )
    }
}

/// Element Container to hold the filtered payment methods enabled with required fields, surcharge and subtype specific data
struct RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodType {
    required_fields: Vec<api_models::payment_methods::RequiredFieldInfo>,
    payment_method_subtype: common_enums::PaymentMethodType,
    payment_method_type: common_enums::PaymentMethod,
    payment_experience: Option<Vec<common_enums::PaymentExperience>>,
    surcharge: Option<api_models::payment_methods::SurchargeDetailsResponse>,
    pm_subtype_specific_data: Option<api_models::payment_methods::PaymentMethodSubtypeSpecificData>,
}

/// Container to hold the filtered payment methods enabled with required fields, surcharge and subtype specific data
struct RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes(
    Vec<RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodType>,
);

impl RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes {
    fn generate_response(
        self,
        customer_payment_methods: Option<
            Vec<api_models::payment_methods::CustomerPaymentMethodResponseItem>,
        >,
    ) -> api_models::payments::PaymentMethodListResponseForPayments {
        let response_payment_methods = self
            .0
            .into_iter()
            .map(|payment_methods_enabled| {
                api_models::payments::ResponsePaymentMethodTypesForPayments {
                    payment_method_type: payment_methods_enabled.payment_method_type,
                    payment_method_subtype: payment_methods_enabled.payment_method_subtype,
                    payment_experience: payment_methods_enabled.payment_experience,
                    required_fields: payment_methods_enabled.required_fields,
                    surcharge_details: payment_methods_enabled.surcharge,
                    extra_information: payment_methods_enabled.pm_subtype_specific_data,
                }
            })
            .collect();

        api_models::payments::PaymentMethodListResponseForPayments {
            payment_methods_enabled: response_payment_methods,
            customer_payment_methods,
        }
    }
}

impl FlattenedPaymentMethodsEnabled {
    async fn perform_filtering(
        self,
        state: &routes::SessionState,
        platform: &domain::Platform,
        profile_id: &id_type::ProfileId,
        req: &api_models::payments::ListMethodsForPaymentsRequest,
        payment_intent: &hyperswitch_domain_models::payments::PaymentIntent,
    ) -> errors::RouterResult<FilteredPaymentMethodsEnabled> {
        let billing_address = payment_intent
            .billing_address
            .clone()
            .and_then(|address| address.into_inner().address);

        let mut response: Vec<hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector> = vec![];

        for payment_method_enabled_details in self.0.payment_methods_enabled {
            filter_payment_methods(
                payment_method_enabled_details,
                req,
                &mut response,
                Some(payment_intent),
                billing_address.as_ref(),
                &state.conf,
            )
            .await?;
        }

        Ok(FilteredPaymentMethodsEnabled(response))
    }
}

// note: v2 type for ListMethodsForPaymentMethodsRequest will not have the installment_payment_enabled field,
#[cfg(feature = "v2")]
#[allow(clippy::too_many_arguments)]
pub async fn filter_payment_methods(
    payment_method_type_details: hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector,
    req: &api_models::payments::ListMethodsForPaymentsRequest,
    resp: &mut Vec<
        hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector,
    >,
    payment_intent: Option<&storage::PaymentIntent>,
    address: Option<&hyperswitch_domain_models::address::AddressDetails>,
    configs: &settings::Settings<RawSecret>,
) -> errors::CustomResult<(), errors::ApiErrorResponse> {
    let payment_method = payment_method_type_details.payment_method;
    let mut payment_method_object = payment_method_type_details.payment_methods_enabled.clone();

    // filter based on request parameters
    let request_based_filter =
        filter_recurring_based(&payment_method_object, req.recurring_enabled)
            && filter_amount_based(&payment_method_object, req.amount)
            && filter_card_network_based(
                payment_method_object.card_networks.as_ref(),
                req.card_networks.as_ref(),
                payment_method_object.payment_method_subtype,
            );

    // filter based on payment intent
    let intent_based_filter = if let Some(payment_intent) = payment_intent {
        filter_country_based(address, &payment_method_object)
            && filter_currency_based(
                payment_intent.amount_details.currency,
                &payment_method_object,
            )
            && filter_amount_based(
                &payment_method_object,
                Some(payment_intent.amount_details.calculate_net_amount()),
            )
            && filter_zero_mandate_based(configs, payment_intent, &payment_method_type_details)
            && filter_allowed_payment_method_types_based(
                payment_intent.allowed_payment_method_types.as_ref(),
                payment_method_object.payment_method_subtype,
            )
    } else {
        true
    };

    // filter based on payment method type configuration
    let config_based_filter = filter_config_based(
        configs,
        &payment_method_type_details.connector.to_string(),
        payment_method_object.payment_method_subtype,
        payment_intent,
        &mut payment_method_object.card_networks,
        address.and_then(|inner| inner.country),
        payment_intent.map(|value| value.amount_details.currency),
    );

    // if all filters pass, add the payment method type details to the response
    if request_based_filter && intent_based_filter && config_based_filter {
        resp.push(payment_method_type_details);
    }

    Ok(())
}

// filter based on country supported by payment method type
// return true if the intent's country is null or if the country is in the accepted countries list
fn filter_country_based(
    address: Option<&hyperswitch_domain_models::address::AddressDetails>,
    pm: &common_types::payment_methods::RequestPaymentMethodTypes,
) -> bool {
    address.is_none_or(|address| {
        address.country.as_ref().is_none_or(|country| {
            pm.accepted_countries.as_ref().is_none_or(|ac| match ac {
                common_types::payment_methods::AcceptedCountries::EnableOnly(acc) => {
                    acc.contains(country)
                }
                common_types::payment_methods::AcceptedCountries::DisableOnly(den) => {
                    !den.contains(country)
                }
                common_types::payment_methods::AcceptedCountries::AllAccepted => true,
            })
        })
    })
}

// filter based on currency supported by payment method type
// return true if the intent's currency is null or if the currency is in the accepted currencies list
fn filter_currency_based(
    currency: common_enums::Currency,
    pm: &common_types::payment_methods::RequestPaymentMethodTypes,
) -> bool {
    pm.accepted_currencies.as_ref().is_none_or(|ac| match ac {
        common_types::payment_methods::AcceptedCurrencies::EnableOnly(acc) => {
            acc.contains(&currency)
        }
        common_types::payment_methods::AcceptedCurrencies::DisableOnly(den) => {
            !den.contains(&currency)
        }
        common_types::payment_methods::AcceptedCurrencies::AllAccepted => true,
    })
}

// filter based on payment method type configuration
// return true if the payment method type is in the configuration for the connector
// return true if the configuration is not available for the connector
fn filter_config_based<'a>(
    config: &'a settings::Settings<RawSecret>,
    connector: &'a str,
    payment_method_type: common_enums::PaymentMethodType,
    payment_intent: Option<&storage::PaymentIntent>,
    card_network: &mut Option<Vec<common_enums::CardNetwork>>,
    country: Option<common_enums::CountryAlpha2>,
    currency: Option<common_enums::Currency>,
) -> bool {
    config
        .pm_filters
        .0
        .get(connector)
        .or_else(|| config.pm_filters.0.get("default"))
        .and_then(|inner| match payment_method_type {
            common_enums::PaymentMethodType::Credit | common_enums::PaymentMethodType::Debit => {
                inner
                    .0
                    .get(&settings::PaymentMethodFilterKey::PaymentMethodType(
                        payment_method_type,
                    ))
                    .map(|value| filter_config_country_currency_based(value, country, currency))
            }
            payment_method_type => inner
                .0
                .get(&settings::PaymentMethodFilterKey::PaymentMethodType(
                    payment_method_type,
                ))
                .map(|value| filter_config_country_currency_based(value, country, currency)),
        })
        .unwrap_or(true)
}

// filter country and currency based on config for payment method type
// return true if the country and currency are in the accepted countries and currencies list
fn filter_config_country_currency_based(
    item: &settings::CurrencyCountryFlowFilter,
    country: Option<common_enums::CountryAlpha2>,
    currency: Option<common_enums::Currency>,
) -> bool {
    let country_condition = item
        .country
        .as_ref()
        .zip(country.as_ref())
        .map(|(lhs, rhs)| lhs.contains(rhs));
    let currency_condition = item
        .currency
        .as_ref()
        .zip(currency)
        .map(|(lhs, rhs)| lhs.contains(&rhs));
    country_condition.unwrap_or(true) && currency_condition.unwrap_or(true)
}

// filter based on recurring enabled parameter of request
// return true if recurring_enabled is null or if it matches the payment method's recurring_enabled
fn filter_recurring_based(
    payment_method: &common_types::payment_methods::RequestPaymentMethodTypes,
    recurring_enabled: Option<bool>,
) -> bool {
    recurring_enabled.is_none_or(|enabled| payment_method.recurring_enabled == Some(enabled))
}

// filter based on valid amount range of payment method type
// return true if the amount is within the payment method's minimum and maximum amount range
// return true if the amount is null or zero
fn filter_amount_based(
    payment_method: &common_types::payment_methods::RequestPaymentMethodTypes,
    amount: Option<types::MinorUnit>,
) -> bool {
    let min_check = amount
        .and_then(|amt| payment_method.minimum_amount.map(|min_amt| amt >= min_amt))
        .unwrap_or(true);
    let max_check = amount
        .and_then(|amt| payment_method.maximum_amount.map(|max_amt| amt <= max_amt))
        .unwrap_or(true);
    (min_check && max_check) || amount == Some(types::MinorUnit::zero())
}

// return true if the intent is a zero mandate intent and the payment method is supported for zero mandates
// return false if the intent is a zero mandate intent and the payment method is not supported for zero mandates
// return true if the intent is not a zero mandate intent
fn filter_zero_mandate_based(
    configs: &settings::Settings<RawSecret>,
    payment_intent: &storage::PaymentIntent,
    payment_method_type_details: &hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector,
) -> bool {
    if payment_intent.setup_future_usage == common_enums::FutureUsage::OffSession
        && payment_intent.amount_details.calculate_net_amount() == types::MinorUnit::zero()
    {
        configs
            .zero_mandates
            .supported_payment_methods
            .0
            .get(&payment_method_type_details.payment_method)
            .and_then(|supported_pm_for_mandates| {
                supported_pm_for_mandates
                    .0
                    .get(
                        &payment_method_type_details
                            .payment_methods_enabled
                            .payment_method_subtype,
                    )
                    .map(|supported_connector_for_mandates| {
                        supported_connector_for_mandates
                            .connector_list
                            .contains(&payment_method_type_details.connector)
                    })
            })
            .unwrap_or(false)
    } else {
        true
    }
}

// filter based on allowed payment method types
// return true if the allowed types are null or if the payment method type is in the allowed types list
fn filter_allowed_payment_method_types_based(
    allowed_types: Option<&Vec<api_models::enums::PaymentMethodType>>,
    payment_method_type: api_models::enums::PaymentMethodType,
) -> bool {
    allowed_types.is_none_or(|pm| pm.contains(&payment_method_type))
}

// filter based on card networks
// return true if the payment method type's card networks are a subset of the request's card networks
// return true if the card networks are not specified in the request
fn filter_card_network_based(
    pm_card_networks: Option<&Vec<api_models::enums::CardNetwork>>,
    request_card_networks: Option<&Vec<api_models::enums::CardNetwork>>,
    pm_type: api_models::enums::PaymentMethodType,
) -> bool {
    match pm_type {
        api_models::enums::PaymentMethodType::Credit
        | api_models::enums::PaymentMethodType::Debit => {
            match (pm_card_networks, request_card_networks) {
                (Some(pm_card_networks), Some(request_card_networks)) => request_card_networks
                    .iter()
                    .all(|card_network| pm_card_networks.contains(card_network)),
                (None, Some(_)) => false,
                _ => true,
            }
        }
        _ => true,
    }
}

/// Validate if payment methods list can be performed on the current status of payment intent
fn validate_payment_status_for_payment_method_list(
    intent_status: common_enums::IntentStatus,
) -> Result<(), errors::ApiErrorResponse> {
    match intent_status {
        common_enums::IntentStatus::RequiresPaymentMethod => Ok(()),
        common_enums::IntentStatus::Succeeded
        | common_enums::IntentStatus::Conflicted
        | common_enums::IntentStatus::Failed
        | common_enums::IntentStatus::Cancelled
        | common_enums::IntentStatus::CancelledPostCapture
        | common_enums::IntentStatus::Processing
        | common_enums::IntentStatus::RequiresCustomerAction
        | common_enums::IntentStatus::RequiresMerchantAction
        | common_enums::IntentStatus::RequiresCapture
        | common_enums::IntentStatus::PartiallyAuthorizedAndRequiresCapture
        | common_enums::IntentStatus::PartiallyCaptured
        | common_enums::IntentStatus::RequiresConfirmation
        | common_enums::IntentStatus::PartiallyCapturedAndCapturable
        | common_enums::IntentStatus::Expired => {
            Err(errors::ApiErrorResponse::PaymentUnexpectedState {
                current_flow: "list_payment_methods".to_string(),
                field_name: "status".to_string(),
                current_value: intent_status.to_string(),
                states: ["requires_payment_method".to_string()].join(", "),
            })
        }
    }
}


// File: crates/router/src/core/payments/types.rs

use std::{collections::HashMap, num::TryFromIntError};

use api_models::payment_methods::SurchargeDetailsResponse;
use common_utils::{
    errors::CustomResult,
    ext_traits::{Encode, OptionExt},
    types::{self as common_types, ConnectorTransactionIdTrait},
};
use error_stack::ResultExt;
use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt;
pub use hyperswitch_domain_models::router_request_types::{
    self, AuthenticationData, SplitRefundsRequest, StripeSplitRefund, SurchargeDetails,
};
use redis_interface::errors::RedisError;
use router_env::{instrument, logger, tracing};

use crate::{
    consts as router_consts,
    core::errors::{self, RouterResult},
    routes::SessionState,
    types::{
        domain::Profile,
        storage::{self, enums as storage_enums},
        transformers::ForeignTryFrom,
    },
};

#[derive(Clone, Debug)]
pub struct MultipleCaptureData {
    // key -> capture_id, value -> Capture
    all_captures: HashMap<String, storage::Capture>,
    latest_capture: storage::Capture,
    pub expand_captures: Option<bool>,
    _private: Private, // to restrict direct construction of MultipleCaptureData
}
#[derive(Clone, Debug)]
struct Private {}

impl MultipleCaptureData {
    pub fn new_for_sync(
        captures: Vec<storage::Capture>,
        expand_captures: Option<bool>,
    ) -> RouterResult<Self> {
        let latest_capture = captures
            .last()
            .ok_or(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Cannot create MultipleCaptureData with empty captures list")?
            .clone();
        let multiple_capture_data = Self {
            all_captures: captures
                .into_iter()
                .map(|capture| (capture.capture_id.clone(), capture))
                .collect(),
            latest_capture,
            _private: Private {},
            expand_captures,
        };
        Ok(multiple_capture_data)
    }

    pub fn new_for_create(
        mut previous_captures: Vec<storage::Capture>,
        new_capture: storage::Capture,
    ) -> Self {
        previous_captures.push(new_capture.clone());
        Self {
            all_captures: previous_captures
                .into_iter()
                .map(|capture| (capture.capture_id.clone(), capture))
                .collect(),
            latest_capture: new_capture,
            _private: Private {},
            expand_captures: None,
        }
    }

    pub fn update_capture(&mut self, updated_capture: storage::Capture) {
        let capture_id = &updated_capture.capture_id;
        if self.all_captures.contains_key(capture_id) {
            self.all_captures
                .entry(capture_id.into())
                .and_modify(|capture| *capture = updated_capture.clone());
        }
    }
    pub fn get_total_blocked_amount(&self) -> common_types::MinorUnit {
        self.all_captures
            .iter()
            .fold(common_types::MinorUnit::new(0), |accumulator, capture| {
                accumulator
                    + match capture.1.status {
                        storage_enums::CaptureStatus::Charged
                        | storage_enums::CaptureStatus::Pending => capture.1.amount,
                        storage_enums::CaptureStatus::Started
                        | storage_enums::CaptureStatus::Failed => common_types::MinorUnit::new(0),
                    }
            })
    }
    pub fn get_total_charged_amount(&self) -> common_types::MinorUnit {
        self.all_captures
            .iter()
            .fold(common_types::MinorUnit::new(0), |accumulator, capture| {
                accumulator
                    + match capture.1.status {
                        storage_enums::CaptureStatus::Charged => capture.1.amount,
                        storage_enums::CaptureStatus::Pending
                        | storage_enums::CaptureStatus::Started
                        | storage_enums::CaptureStatus::Failed => common_types::MinorUnit::new(0),
                    }
            })
    }
    pub fn get_captures_count(&self) -> RouterResult<i16> {
        i16::try_from(self.all_captures.len())
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error while converting from usize to i16")
    }
    pub fn get_status_count(&self) -> HashMap<storage_enums::CaptureStatus, i16> {
        let mut hash_map: HashMap<storage_enums::CaptureStatus, i16> = HashMap::new();
        hash_map.insert(storage_enums::CaptureStatus::Charged, 0);
        hash_map.insert(storage_enums::CaptureStatus::Pending, 0);
        hash_map.insert(storage_enums::CaptureStatus::Started, 0);
        hash_map.insert(storage_enums::CaptureStatus::Failed, 0);
        self.all_captures
            .iter()
            .fold(hash_map, |mut accumulator, capture| {
                let current_capture_status = capture.1.status;
                accumulator
                    .entry(current_capture_status)
                    .and_modify(|count| *count += 1);
                accumulator
            })
    }
    pub fn get_attempt_status(
        &self,
        authorized_amount: common_types::MinorUnit,
    ) -> storage_enums::AttemptStatus {
        let total_captured_amount = self.get_total_charged_amount();
        if authorized_amount == total_captured_amount {
            return storage_enums::AttemptStatus::Charged;
        }
        let status_count_map = self.get_status_count();
        if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) {
            storage_enums::AttemptStatus::PartialChargedAndChargeable
        } else {
            storage_enums::AttemptStatus::CaptureInitiated
        }
    }
    pub fn get_pending_captures(&self) -> Vec<&storage::Capture> {
        self.all_captures
            .iter()
            .filter(|capture| capture.1.status == storage_enums::CaptureStatus::Pending)
            .map(|key_value| key_value.1)
            .collect()
    }
    pub fn get_all_captures(&self) -> Vec<&storage::Capture> {
        self.all_captures
            .iter()
            .map(|key_value| key_value.1)
            .collect()
    }
    pub fn get_capture_by_capture_id(&self, capture_id: String) -> Option<&storage::Capture> {
        self.all_captures.get(&capture_id)
    }
    pub fn get_capture_by_connector_capture_id(
        &self,
        connector_capture_id: &String,
    ) -> Option<&storage::Capture> {
        self.all_captures
            .iter()
            .find(|(_, capture)| {
                capture.get_optional_connector_transaction_id() == Some(connector_capture_id)
            })
            .map(|(_, capture)| capture)
    }
    pub fn get_latest_capture(&self) -> &storage::Capture {
        &self.latest_capture
    }
    pub fn get_pending_connector_capture_ids(&self) -> Vec<String> {
        let pending_connector_capture_ids = self
            .get_pending_captures()
            .into_iter()
            .filter_map(|capture| capture.get_optional_connector_transaction_id().cloned())
            .collect();
        pending_connector_capture_ids
    }
    pub fn get_pending_captures_without_connector_capture_id(&self) -> Vec<&storage::Capture> {
        self.get_pending_captures()
            .into_iter()
            .filter(|capture| capture.get_optional_connector_transaction_id().is_none())
            .collect()
    }
}

#[cfg(feature = "v2")]
impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse {
    type Error = TryFromIntError;
    fn foreign_try_from(
        (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt),
    ) -> Result<Self, Self::Error> {
        todo!()
    }
}

#[cfg(feature = "v1")]
impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse {
    type Error = TryFromIntError;
    fn foreign_try_from(
        (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt),
    ) -> Result<Self, Self::Error> {
        let currency = payment_attempt.currency.unwrap_or_default();
        let display_surcharge_amount = currency
            .to_currency_base_unit_asf64(surcharge_details.surcharge_amount.get_amount_as_i64())?;
        let display_tax_on_surcharge_amount = currency.to_currency_base_unit_asf64(
            surcharge_details
                .tax_on_surcharge_amount
                .get_amount_as_i64(),
        )?;
        let display_total_surcharge_amount = currency.to_currency_base_unit_asf64(
            (surcharge_details.surcharge_amount + surcharge_details.tax_on_surcharge_amount)
                .get_amount_as_i64(),
        )?;
        Ok(Self {
            surcharge: surcharge_details.surcharge.clone().into(),
            tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into),
            display_surcharge_amount,
            display_tax_on_surcharge_amount,
            display_total_surcharge_amount,
        })
    }
}

#[derive(Eq, Hash, PartialEq, Clone, Debug, strum::Display)]
pub enum SurchargeKey {
    Token(String),
    PaymentMethodData(
        common_enums::PaymentMethod,
        common_enums::PaymentMethodType,
        Option<common_enums::CardNetwork>,
    ),
}

#[derive(Clone, Debug)]
pub struct SurchargeMetadata {
    surcharge_results: HashMap<SurchargeKey, SurchargeDetails>,
    pub payment_attempt_id: String,
}

impl SurchargeMetadata {
    pub fn new(payment_attempt_id: String) -> Self {
        Self {
            surcharge_results: HashMap::new(),
            payment_attempt_id,
        }
    }
    pub fn is_empty_result(&self) -> bool {
        self.surcharge_results.is_empty()
    }
    pub fn get_surcharge_results_size(&self) -> usize {
        self.surcharge_results.len()
    }
    pub fn insert_surcharge_details(
        &mut self,
        surcharge_key: SurchargeKey,
        surcharge_details: SurchargeDetails,
    ) {
        self.surcharge_results
            .insert(surcharge_key, surcharge_details);
    }
    pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> {
        self.surcharge_results.get(&surcharge_key)
    }
    pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String {
        format!("surcharge_metadata_{payment_attempt_id}")
    }
    pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> {
        self.surcharge_results
            .iter()
            .map(|(surcharge_key, surcharge_details)| {
                let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key);
                (key, surcharge_details.to_owned())
            })
            .collect()
    }
    pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String {
        match surcharge_key {
            SurchargeKey::Token(token) => {
                format!("token_{token}")
            }
            SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => {
                if let Some(card_network) = card_network {
                    format!("{payment_method}_{payment_method_type}_{card_network}")
                } else {
                    format!("{payment_method}_{payment_method_type}")
                }
            }
        }
    }
    #[instrument(skip_all)]
    pub async fn persist_individual_surcharge_details_in_redis(
        &self,
        state: &SessionState,
        business_profile: &Profile,
    ) -> RouterResult<()> {
        if !self.is_empty_result() {
            let redis_conn = state
                .store
                .get_redis_conn()
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to get redis connection")?;
            let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id);

            let mut value_list = Vec::with_capacity(self.get_surcharge_results_size());
            for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() {
                value_list.push((
                    key,
                    value
                        .encode_to_string_of_json()
                        .change_context(errors::ApiErrorResponse::InternalServerError)
                        .attach_printable("Failed to encode to string of json")?,
                ));
            }
            let intent_fulfillment_time = business_profile
                .get_order_fulfillment_time()
                .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME);
            redis_conn
                .set_hash_fields(
                    &redis_key.as_str().into(),
                    value_list,
                    Some(intent_fulfillment_time),
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to write to redis")?;
            logger::debug!("Surcharge results stored in redis with key = {}", redis_key);
        }
        Ok(())
    }

    #[instrument(skip_all)]
    pub async fn get_individual_surcharge_detail_from_redis(
        state: &SessionState,
        surcharge_key: SurchargeKey,
        payment_attempt_id: &str,
    ) -> CustomResult<SurchargeDetails, RedisError> {
        let redis_conn = state
            .store
            .get_redis_conn()
            .attach_printable("Failed to get redis connection")?;
        let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id);
        let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key);
        let result = redis_conn
            .get_hash_field_and_deserialize(
                &redis_key.as_str().into(),
                &value_key,
                "SurchargeDetails",
            )
            .await;
        logger::debug!(
            "Surcharge result fetched from redis with key = {} and {}",
            redis_key,
            value_key
        );
        result
    }
}

impl ForeignTryFrom<&router_request_types::authentication::AuthenticationStore>
    for router_request_types::UcsAuthenticationData
{
    type Error = error_stack::Report<errors::ApiErrorResponse>;
    fn foreign_try_from(
        authentication_store: &router_request_types::authentication::AuthenticationStore,
    ) -> Result<Self, Self::Error> {
        let authentication = &authentication_store.authentication;
        if authentication.authentication_status == common_enums::AuthenticationStatus::Success {
            let threeds_server_transaction_id =
                authentication.threeds_server_transaction_id.clone();
            let message_version = authentication.message_version.clone();
            let cavv = authentication_store
                .cavv
                .clone()
                .get_required_value("cavv")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("cavv must not be null when authentication_status is success")?;
            Ok(Self {
                trans_status: authentication.trans_status.clone(),
                eci: authentication.eci.clone(),
                cavv: Some(cavv),
                threeds_server_transaction_id,
                message_version,
                ds_trans_id: authentication.ds_trans_id.clone(),
                acs_trans_id: authentication.acs_trans_id.clone(),
                transaction_id: authentication.connector_authentication_id.clone(),
                ucaf_collection_indicator: None,
            })
        } else {
            Err(errors::ApiErrorResponse::PaymentAuthenticationFailed { data: None }.into())
        }
    }
}

impl ForeignTryFrom<&router_request_types::authentication::AuthenticationStore>
    for AuthenticationData
{
    type Error = error_stack::Report<errors::ApiErrorResponse>;
    fn foreign_try_from(
        authentication_store: &router_request_types::authentication::AuthenticationStore,
    ) -> Result<Self, Self::Error> {
        let authentication = &authentication_store.authentication;
        if authentication.authentication_status == common_enums::AuthenticationStatus::Success {
            let threeds_server_transaction_id =
                authentication.threeds_server_transaction_id.clone();
            let message_version = authentication.message_version.clone();
            let cavv = authentication_store
                .cavv
                .clone()
                .get_required_value("cavv")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("cavv must not be null when authentication_status is success")?;
            Ok(Self {
                eci: authentication.eci.clone(),
                created_at: authentication.created_at,
                cavv,
                threeds_server_transaction_id,
                message_version,
                ds_trans_id: authentication.ds_trans_id.clone(),
                authentication_type: authentication.authentication_type,
                challenge_code: authentication.challenge_code.clone(),
                challenge_cancel: authentication.challenge_cancel.clone(),
                challenge_code_reason: authentication.challenge_code_reason.clone(),
                message_extension: authentication.message_extension.clone(),
                acs_trans_id: authentication.acs_trans_id.clone(),
                transaction_status: authentication.trans_status.clone(),
                exemption_indicator: None,
                cb_network_params: None,
            })
        } else {
            Err(errors::ApiErrorResponse::PaymentAuthenticationFailed { data: None }.into())
        }
    }
}

impl ForeignTryFrom<&api_models::payments::ExternalThreeDsData> for AuthenticationData {
    type Error = error_stack::Report<errors::ApiErrorResponse>;

    fn foreign_try_from(
        external_auth_data: &api_models::payments::ExternalThreeDsData,
    ) -> Result<Self, Self::Error> {
        let cavv = match &external_auth_data.authentication_cryptogram {
            api_models::payments::Cryptogram::Cavv {
                authentication_cryptogram,
            } => authentication_cryptogram.clone(),
        };

        Ok(Self {
            eci: Some(external_auth_data.eci.clone()),
            cavv,
            threeds_server_transaction_id: Some(external_auth_data.ds_trans_id.clone()),
            message_version: Some(external_auth_data.version.clone()),
            ds_trans_id: Some(external_auth_data.ds_trans_id.clone()),
            created_at: time::PrimitiveDateTime::new(
                time::OffsetDateTime::now_utc().date(),
                time::OffsetDateTime::now_utc().time(),
            ),
            challenge_code: None,
            challenge_cancel: None,
            challenge_code_reason: None,
            message_extension: None,
            acs_trans_id: None,
            authentication_type: None,
            transaction_status: Some(external_auth_data.transaction_status.clone()),
            exemption_indicator: external_auth_data.exemption_indicator.clone(),
            cb_network_params: external_auth_data.network_params.clone(),
        })
    }
}