// File: crates/kgraph_utils/src/mca.rs

#[cfg(feature = "v1")]
use std::str::FromStr;

#[cfg(feature = "v1")]
use api_models::payment_methods::RequestPaymentMethodTypes;
use api_models::{admin as admin_api, enums as api_enums, refunds::MinorUnit};
use euclid::{
    dirval,
    frontend::{ast, dir},
    types::{NumValue, NumValueRefinement},
};
use hyperswitch_constraint_graph as cgraph;
use strum::IntoEnumIterator;

use crate::{error::KgraphError, transformers::IntoDirValue, types as kgraph_types};

pub const DOMAIN_IDENTIFIER: &str = "payment_methods_enabled_for_merchantconnectoraccount";

// #[cfg(feature = "v1")]
fn get_dir_value_payment_method(
    from: api_enums::PaymentMethodType,
) -> Result<dir::DirValue, KgraphError> {
    match from {
        api_enums::PaymentMethodType::AmazonPay => Ok(dirval!(WalletType = AmazonPay)),
        api_enums::PaymentMethodType::Skrill => Ok(dirval!(WalletType = Skrill)),
        api_enums::PaymentMethodType::Paysera => Ok(dirval!(WalletType = Paysera)),
        api_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)),
        api_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)),
        #[cfg(feature = "v2")]
        api_enums::PaymentMethodType::Card => Ok(dirval!(CardType = Card)),
        api_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)),
        api_enums::PaymentMethodType::Ideal => Ok(dirval!(BankRedirectType = Ideal)),
        api_enums::PaymentMethodType::Sofort => Ok(dirval!(BankRedirectType = Sofort)),
        api_enums::PaymentMethodType::Eps => Ok(dirval!(BankRedirectType = Eps)),
        api_enums::PaymentMethodType::Eft => Ok(dirval!(BankRedirectType = Eft)),
        api_enums::PaymentMethodType::Klarna => Ok(dirval!(PayLaterType = Klarna)),
        api_enums::PaymentMethodType::Flexiti => Ok(dirval!(PayLaterType = Flexiti)),
        api_enums::PaymentMethodType::Affirm => Ok(dirval!(PayLaterType = Affirm)),
        api_enums::PaymentMethodType::Payjustnow => Ok(dirval!(PayLaterType = Payjustnow)),
        api_enums::PaymentMethodType::AfterpayClearpay => {
            Ok(dirval!(PayLaterType = AfterpayClearpay))
        }
        api_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)),
        api_enums::PaymentMethodType::Bluecode => Ok(dirval!(WalletType = Bluecode)),
        api_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)),
        api_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)),
        api_enums::PaymentMethodType::CryptoCurrency => Ok(dirval!(CryptoType = CryptoCurrency)),
        api_enums::PaymentMethodType::Ach => Ok(dirval!(BankDebitType = Ach)),

        api_enums::PaymentMethodType::Bacs => Ok(dirval!(BankDebitType = Bacs)),

        api_enums::PaymentMethodType::Becs => Ok(dirval!(BankDebitType = Becs)),
        api_enums::PaymentMethodType::Sepa => Ok(dirval!(BankDebitType = Sepa)),
        api_enums::PaymentMethodType::SepaGuarenteedDebit => {
            Ok(dirval!(BankDebitType = SepaGuarenteedDebit))
        }
        api_enums::PaymentMethodType::AliPay => Ok(dirval!(WalletType = AliPay)),
        api_enums::PaymentMethodType::AliPayHk => Ok(dirval!(WalletType = AliPayHk)),
        api_enums::PaymentMethodType::BancontactCard => {
            Ok(dirval!(BankRedirectType = BancontactCard))
        }
        api_enums::PaymentMethodType::Blik => Ok(dirval!(BankRedirectType = Blik)),
        api_enums::PaymentMethodType::MbWay => Ok(dirval!(WalletType = MbWay)),
        api_enums::PaymentMethodType::MobilePay => Ok(dirval!(WalletType = MobilePay)),
        api_enums::PaymentMethodType::Cashapp => Ok(dirval!(WalletType = Cashapp)),
        api_enums::PaymentMethodType::Multibanco => Ok(dirval!(BankTransferType = Multibanco)),
        api_enums::PaymentMethodType::Pix => Ok(dirval!(BankTransferType = Pix)),
        api_enums::PaymentMethodType::Pse => Ok(dirval!(BankTransferType = Pse)),
        api_enums::PaymentMethodType::Interac => Ok(dirval!(BankRedirectType = Interac)),
        api_enums::PaymentMethodType::OnlineBankingCzechRepublic => {
            Ok(dirval!(BankRedirectType = OnlineBankingCzechRepublic))
        }
        api_enums::PaymentMethodType::OnlineBankingFinland => {
            Ok(dirval!(BankRedirectType = OnlineBankingFinland))
        }
        api_enums::PaymentMethodType::OnlineBankingPoland => {
            Ok(dirval!(BankRedirectType = OnlineBankingPoland))
        }
        api_enums::PaymentMethodType::OnlineBankingSlovakia => {
            Ok(dirval!(BankRedirectType = OnlineBankingSlovakia))
        }
        api_enums::PaymentMethodType::Swish => Ok(dirval!(WalletType = Swish)),
        api_enums::PaymentMethodType::Trustly => Ok(dirval!(BankRedirectType = Trustly)),
        api_enums::PaymentMethodType::Bizum => Ok(dirval!(BankRedirectType = Bizum)),

        api_enums::PaymentMethodType::PayBright => Ok(dirval!(PayLaterType = PayBright)),
        api_enums::PaymentMethodType::Walley => Ok(dirval!(PayLaterType = Walley)),
        api_enums::PaymentMethodType::Przelewy24 => Ok(dirval!(BankRedirectType = Przelewy24)),
        api_enums::PaymentMethodType::WeChatPay => Ok(dirval!(WalletType = WeChatPay)),

        api_enums::PaymentMethodType::ClassicReward => Ok(dirval!(RewardType = ClassicReward)),
        api_enums::PaymentMethodType::Evoucher => Ok(dirval!(RewardType = Evoucher)),
        api_enums::PaymentMethodType::SamsungPay => Ok(dirval!(WalletType = SamsungPay)),
        api_enums::PaymentMethodType::GoPay => Ok(dirval!(WalletType = GoPay)),
        api_enums::PaymentMethodType::KakaoPay => Ok(dirval!(WalletType = KakaoPay)),
        api_enums::PaymentMethodType::Twint => Ok(dirval!(WalletType = Twint)),
        api_enums::PaymentMethodType::Gcash => Ok(dirval!(WalletType = Gcash)),
        api_enums::PaymentMethodType::Vipps => Ok(dirval!(WalletType = Vipps)),
        api_enums::PaymentMethodType::Momo => Ok(dirval!(WalletType = Momo)),
        api_enums::PaymentMethodType::Alma => Ok(dirval!(PayLaterType = Alma)),
        api_enums::PaymentMethodType::Dana => Ok(dirval!(WalletType = Dana)),
        api_enums::PaymentMethodType::OnlineBankingFpx => {
            Ok(dirval!(BankRedirectType = OnlineBankingFpx))
        }
        api_enums::PaymentMethodType::OnlineBankingThailand => {
            Ok(dirval!(BankRedirectType = OnlineBankingThailand))
        }
        api_enums::PaymentMethodType::LocalBankRedirect => {
            Ok(dirval!(BankRedirectType = LocalBankRedirect))
        }
        api_enums::PaymentMethodType::TouchNGo => Ok(dirval!(WalletType = TouchNGo)),
        api_enums::PaymentMethodType::Atome => Ok(dirval!(PayLaterType = Atome)),
        api_enums::PaymentMethodType::Boleto => Ok(dirval!(VoucherType = Boleto)),
        api_enums::PaymentMethodType::Efecty => Ok(dirval!(VoucherType = Efecty)),
        api_enums::PaymentMethodType::PagoEfectivo => Ok(dirval!(VoucherType = PagoEfectivo)),
        api_enums::PaymentMethodType::RedCompra => Ok(dirval!(VoucherType = RedCompra)),
        api_enums::PaymentMethodType::RedPagos => Ok(dirval!(VoucherType = RedPagos)),
        api_enums::PaymentMethodType::Alfamart => Ok(dirval!(VoucherType = Alfamart)),
        api_enums::PaymentMethodType::BcaBankTransfer => {
            Ok(dirval!(BankTransferType = BcaBankTransfer))
        }
        api_enums::PaymentMethodType::BniVa => Ok(dirval!(BankTransferType = BniVa)),
        api_enums::PaymentMethodType::BriVa => Ok(dirval!(BankTransferType = BriVa)),
        api_enums::PaymentMethodType::Breadpay => Ok(dirval!(PayLaterType = Breadpay)),
        api_enums::PaymentMethodType::CimbVa => Ok(dirval!(BankTransferType = CimbVa)),
        api_enums::PaymentMethodType::DanamonVa => Ok(dirval!(BankTransferType = DanamonVa)),
        api_enums::PaymentMethodType::Indomaret => Ok(dirval!(VoucherType = Indomaret)),
        api_enums::PaymentMethodType::MandiriVa => Ok(dirval!(BankTransferType = MandiriVa)),
        api_enums::PaymentMethodType::LocalBankTransfer => {
            Ok(dirval!(BankTransferType = LocalBankTransfer))
        }
        api_enums::PaymentMethodType::InstantBankTransfer => {
            Ok(dirval!(BankTransferType = InstantBankTransfer))
        }
        api_enums::PaymentMethodType::InstantBankTransferFinland => {
            Ok(dirval!(BankTransferType = InstantBankTransferFinland))
        }
        api_enums::PaymentMethodType::InstantBankTransferPoland => {
            Ok(dirval!(BankTransferType = InstantBankTransferPoland))
        }
        api_enums::PaymentMethodType::SepaBankTransfer => {
            Ok(dirval!(BankTransferType = SepaBankTransfer))
        }
        api_enums::PaymentMethodType::PermataBankTransfer => {
            Ok(dirval!(BankTransferType = PermataBankTransfer))
        }
        api_enums::PaymentMethodType::IndonesianBankTransfer => {
            Ok(dirval!(BankTransferType = IndonesianBankTransfer))
        }
        api_enums::PaymentMethodType::PaySafeCard => Ok(dirval!(GiftCardType = PaySafeCard)),
        api_enums::PaymentMethodType::SevenEleven => Ok(dirval!(VoucherType = SevenEleven)),
        api_enums::PaymentMethodType::Lawson => Ok(dirval!(VoucherType = Lawson)),
        api_enums::PaymentMethodType::MiniStop => Ok(dirval!(VoucherType = MiniStop)),
        api_enums::PaymentMethodType::FamilyMart => Ok(dirval!(VoucherType = FamilyMart)),
        api_enums::PaymentMethodType::Seicomart => Ok(dirval!(VoucherType = Seicomart)),
        api_enums::PaymentMethodType::PayEasy => Ok(dirval!(VoucherType = PayEasy)),
        api_enums::PaymentMethodType::Givex => Ok(dirval!(GiftCardType = Givex)),
        api_enums::PaymentMethodType::BhnCardNetwork => Ok(dirval!(GiftCardType = BhnCardNetwork)),
        api_enums::PaymentMethodType::Benefit => Ok(dirval!(CardRedirectType = Benefit)),
        api_enums::PaymentMethodType::Knet => Ok(dirval!(CardRedirectType = Knet)),
        api_enums::PaymentMethodType::OpenBankingUk => {
            Ok(dirval!(BankRedirectType = OpenBankingUk))
        }
        api_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)),
        api_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)),
        api_enums::PaymentMethodType::CardRedirect => Ok(dirval!(CardRedirectType = CardRedirect)),
        api_enums::PaymentMethodType::Venmo => Ok(dirval!(WalletType = Venmo)),
        api_enums::PaymentMethodType::UpiIntent => Ok(dirval!(UpiType = UpiIntent)),
        api_enums::PaymentMethodType::UpiCollect => Ok(dirval!(UpiType = UpiCollect)),
        api_enums::PaymentMethodType::UpiQr => Ok(dirval!(UpiType = UpiQr)),
        api_enums::PaymentMethodType::Mifinity => Ok(dirval!(WalletType = Mifinity)),
        api_enums::PaymentMethodType::Fps => Ok(dirval!(RealTimePaymentType = Fps)),
        api_enums::PaymentMethodType::DuitNow => Ok(dirval!(RealTimePaymentType = DuitNow)),
        api_enums::PaymentMethodType::PromptPay => Ok(dirval!(RealTimePaymentType = PromptPay)),
        api_enums::PaymentMethodType::VietQr => Ok(dirval!(RealTimePaymentType = VietQr)),
        api_enums::PaymentMethodType::OpenBankingPIS => {
            Ok(dirval!(OpenBankingType = OpenBankingPIS))
        }
        api_enums::PaymentMethodType::Paze => Ok(dirval!(WalletType = Paze)),
        api_enums::PaymentMethodType::DirectCarrierBilling => {
            Ok(dirval!(MobilePaymentType = DirectCarrierBilling))
        }
        api_enums::PaymentMethodType::RevolutPay => Ok(dirval!(WalletType = RevolutPay)),
    }
}

#[cfg(feature = "v2")]
fn compile_request_pm_types(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    pm_types: common_types::payment_methods::RequestPaymentMethodTypes,
    pm: api_enums::PaymentMethod,
) -> Result<cgraph::NodeId, KgraphError> {
    let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

    let pmt_info = "PaymentMethodType";
    let pmt_id = builder.make_value_node(
        (pm_types.payment_method_subtype, pm)
            .into_dir_value()
            .map(Into::into)?,
        Some(pmt_info),
        None::<()>,
    );
    agg_nodes.push((
        pmt_id,
        cgraph::Relation::Positive,
        match pm_types.payment_method_subtype {
            api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit => {
                cgraph::Strength::Weak
            }

            _ => cgraph::Strength::Strong,
        },
    ));

    if let Some(card_networks) = pm_types.card_networks {
        if !card_networks.is_empty() {
            let dir_vals: Vec<dir::DirValue> = card_networks
                .into_iter()
                .map(IntoDirValue::into_dir_value)
                .collect::<Result<_, _>>()?;

            let card_network_info = "Card Networks";
            let card_network_id = builder
                .make_in_aggregator(dir_vals, Some(card_network_info), None::<()>)
                .map_err(KgraphError::GraphConstructionError)?;

            agg_nodes.push((
                card_network_id,
                cgraph::Relation::Positive,
                cgraph::Strength::Weak,
            ));
        }
    }

    let currencies_data = pm_types
        .accepted_currencies
        .and_then(|accepted_currencies| match accepted_currencies {
            common_types::payment_methods::AcceptedCurrencies::EnableOnly(curr)
                if !curr.is_empty() =>
            {
                Some((
                    curr.into_iter()
                        .map(IntoDirValue::into_dir_value)
                        .collect::<Result<_, _>>()
                        .ok()?,
                    cgraph::Relation::Positive,
                ))
            }

            common_types::payment_methods::AcceptedCurrencies::DisableOnly(curr)
                if !curr.is_empty() =>
            {
                Some((
                    curr.into_iter()
                        .map(IntoDirValue::into_dir_value)
                        .collect::<Result<_, _>>()
                        .ok()?,
                    cgraph::Relation::Negative,
                ))
            }

            _ => None,
        });

    if let Some((currencies, relation)) = currencies_data {
        let accepted_currencies_info = "Accepted Currencies";
        let accepted_currencies_id = builder
            .make_in_aggregator(currencies, Some(accepted_currencies_info), None::<()>)
            .map_err(KgraphError::GraphConstructionError)?;

        agg_nodes.push((accepted_currencies_id, relation, cgraph::Strength::Strong));
    }

    let mut amount_nodes = Vec::with_capacity(2);

    if let Some(min_amt) = pm_types.minimum_amount {
        let num_val = NumValue {
            number: min_amt,
            refinement: Some(NumValueRefinement::GreaterThanEqual),
        };

        let min_amt_info = "Minimum Amount";
        let min_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(num_val).into(),
            Some(min_amt_info),
            None::<()>,
        );

        amount_nodes.push(min_amt_id);
    }

    if let Some(max_amt) = pm_types.maximum_amount {
        let num_val = NumValue {
            number: max_amt,
            refinement: Some(NumValueRefinement::LessThanEqual),
        };

        let max_amt_info = "Maximum Amount";
        let max_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(num_val).into(),
            Some(max_amt_info),
            None::<()>,
        );

        amount_nodes.push(max_amt_id);
    }

    if !amount_nodes.is_empty() {
        let zero_num_val = NumValue {
            number: MinorUnit::zero(),
            refinement: None,
        };

        let zero_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(zero_num_val).into(),
            Some("zero_amount"),
            None::<()>,
        );

        let or_node_neighbor_id = if amount_nodes.len() == 1 {
            amount_nodes
                .first()
                .copied()
                .ok_or(KgraphError::IndexingError)?
        } else {
            let nodes = amount_nodes
                .iter()
                .copied()
                .map(|node_id| {
                    (
                        node_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    )
                })
                .collect::<Vec<_>>();

            builder
                .make_all_aggregator(
                    &nodes,
                    Some("amount_constraint_aggregator"),
                    None::<()>,
                    None,
                )
                .map_err(KgraphError::GraphConstructionError)?
        };

        let any_aggregator = builder
            .make_any_aggregator(
                &[
                    (
                        zero_amt_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                    (
                        or_node_neighbor_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                ],
                Some("zero_plus_limits_amount_aggregator"),
                None::<()>,
                None,
            )
            .map_err(KgraphError::GraphConstructionError)?;

        agg_nodes.push((
            any_aggregator,
            cgraph::Relation::Positive,
            cgraph::Strength::Strong,
        ));
    }

    let pmt_all_aggregator_info = "All Aggregator for PaymentMethodType";
    builder
        .make_all_aggregator(&agg_nodes, Some(pmt_all_aggregator_info), None::<()>, None)
        .map_err(KgraphError::GraphConstructionError)
}

#[cfg(feature = "v1")]
fn compile_request_pm_types(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    pm_types: RequestPaymentMethodTypes,
    pm: api_enums::PaymentMethod,
) -> Result<cgraph::NodeId, KgraphError> {
    let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

    let pmt_info = "PaymentMethodType";
    let pmt_id = builder.make_value_node(
        (pm_types.payment_method_type, pm)
            .into_dir_value()
            .map(Into::into)?,
        Some(pmt_info),
        None::<()>,
    );
    agg_nodes.push((
        pmt_id,
        cgraph::Relation::Positive,
        match pm_types.payment_method_type {
            api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit => {
                cgraph::Strength::Weak
            }

            _ => cgraph::Strength::Strong,
        },
    ));

    if let Some(card_networks) = pm_types.card_networks {
        if !card_networks.is_empty() {
            let dir_vals: Vec<dir::DirValue> = card_networks
                .into_iter()
                .map(IntoDirValue::into_dir_value)
                .collect::<Result<_, _>>()?;

            let card_network_info = "Card Networks";
            let card_network_id = builder
                .make_in_aggregator(dir_vals, Some(card_network_info), None::<()>)
                .map_err(KgraphError::GraphConstructionError)?;

            agg_nodes.push((
                card_network_id,
                cgraph::Relation::Positive,
                cgraph::Strength::Weak,
            ));
        }
    }

    let currencies_data = pm_types
        .accepted_currencies
        .and_then(|accepted_currencies| match accepted_currencies {
            admin_api::AcceptedCurrencies::EnableOnly(curr) if !curr.is_empty() => Some((
                curr.into_iter()
                    .map(IntoDirValue::into_dir_value)
                    .collect::<Result<_, _>>()
                    .ok()?,
                cgraph::Relation::Positive,
            )),

            admin_api::AcceptedCurrencies::DisableOnly(curr) if !curr.is_empty() => Some((
                curr.into_iter()
                    .map(IntoDirValue::into_dir_value)
                    .collect::<Result<_, _>>()
                    .ok()?,
                cgraph::Relation::Negative,
            )),

            _ => None,
        });

    if let Some((currencies, relation)) = currencies_data {
        let accepted_currencies_info = "Accepted Currencies";
        let accepted_currencies_id = builder
            .make_in_aggregator(currencies, Some(accepted_currencies_info), None::<()>)
            .map_err(KgraphError::GraphConstructionError)?;

        agg_nodes.push((accepted_currencies_id, relation, cgraph::Strength::Strong));
    }

    let mut amount_nodes = Vec::with_capacity(2);

    if let Some(min_amt) = pm_types.minimum_amount {
        let num_val = NumValue {
            number: min_amt,
            refinement: Some(NumValueRefinement::GreaterThanEqual),
        };

        let min_amt_info = "Minimum Amount";
        let min_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(num_val).into(),
            Some(min_amt_info),
            None::<()>,
        );

        amount_nodes.push(min_amt_id);
    }

    if let Some(max_amt) = pm_types.maximum_amount {
        let num_val = NumValue {
            number: max_amt,
            refinement: Some(NumValueRefinement::LessThanEqual),
        };

        let max_amt_info = "Maximum Amount";
        let max_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(num_val).into(),
            Some(max_amt_info),
            None::<()>,
        );

        amount_nodes.push(max_amt_id);
    }

    if !amount_nodes.is_empty() {
        let zero_num_val = NumValue {
            number: MinorUnit::zero(),
            refinement: None,
        };

        let zero_amt_id = builder.make_value_node(
            dir::DirValue::PaymentAmount(zero_num_val).into(),
            Some("zero_amount"),
            None::<()>,
        );

        let or_node_neighbor_id = if amount_nodes.len() == 1 {
            amount_nodes
                .first()
                .copied()
                .ok_or(KgraphError::IndexingError)?
        } else {
            let nodes = amount_nodes
                .iter()
                .copied()
                .map(|node_id| {
                    (
                        node_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    )
                })
                .collect::<Vec<_>>();

            builder
                .make_all_aggregator(
                    &nodes,
                    Some("amount_constraint_aggregator"),
                    None::<()>,
                    None,
                )
                .map_err(KgraphError::GraphConstructionError)?
        };

        let any_aggregator = builder
            .make_any_aggregator(
                &[
                    (
                        zero_amt_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                    (
                        or_node_neighbor_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                ],
                Some("zero_plus_limits_amount_aggregator"),
                None::<()>,
                None,
            )
            .map_err(KgraphError::GraphConstructionError)?;

        agg_nodes.push((
            any_aggregator,
            cgraph::Relation::Positive,
            cgraph::Strength::Strong,
        ));
    }

    let pmt_all_aggregator_info = "All Aggregator for PaymentMethodType";
    builder
        .make_all_aggregator(&agg_nodes, Some(pmt_all_aggregator_info), None::<()>, None)
        .map_err(KgraphError::GraphConstructionError)
}

#[cfg(feature = "v2")]
fn compile_payment_method_enabled(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    enabled: common_types::payment_methods::PaymentMethodsEnabled,
) -> Result<Option<cgraph::NodeId>, KgraphError> {
    let agg_id = if !enabled
        .payment_method_subtypes
        .as_ref()
        .map(|v| v.is_empty())
        .unwrap_or(true)
    {
        let pm_info = "PaymentMethod";
        let pm_id = builder.make_value_node(
            enabled
                .payment_method_type
                .into_dir_value()
                .map(Into::into)?,
            Some(pm_info),
            None::<()>,
        );

        let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

        if let Some(pm_types) = enabled.payment_method_subtypes {
            for pm_type in pm_types {
                let node_id =
                    compile_request_pm_types(builder, pm_type, enabled.payment_method_type)?;
                agg_nodes.push((
                    node_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Strong,
                ));
            }
        }

        let any_aggregator_info = "Any aggregation for PaymentMethodsType";
        let pm_type_agg_id = builder
            .make_any_aggregator(&agg_nodes, Some(any_aggregator_info), None::<()>, None)
            .map_err(KgraphError::GraphConstructionError)?;

        let all_aggregator_info = "All aggregation for PaymentMethod";
        let enabled_pm_agg_id = builder
            .make_all_aggregator(
                &[
                    (pm_id, cgraph::Relation::Positive, cgraph::Strength::Strong),
                    (
                        pm_type_agg_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                ],
                Some(all_aggregator_info),
                None::<()>,
                None,
            )
            .map_err(KgraphError::GraphConstructionError)?;

        Some(enabled_pm_agg_id)
    } else {
        None
    };

    Ok(agg_id)
}

#[cfg(feature = "v1")]
fn compile_payment_method_enabled(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    enabled: admin_api::PaymentMethodsEnabled,
) -> Result<Option<cgraph::NodeId>, KgraphError> {
    let agg_id = if !enabled
        .payment_method_types
        .as_ref()
        .map(|v| v.is_empty())
        .unwrap_or(true)
    {
        let pm_info = "PaymentMethod";
        let pm_id = builder.make_value_node(
            enabled.payment_method.into_dir_value().map(Into::into)?,
            Some(pm_info),
            None::<()>,
        );

        let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

        if let Some(pm_types) = enabled.payment_method_types {
            for pm_type in pm_types {
                let node_id = compile_request_pm_types(builder, pm_type, enabled.payment_method)?;
                agg_nodes.push((
                    node_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Strong,
                ));
            }
        }

        let any_aggregator_info = "Any aggregation for PaymentMethodsType";
        let pm_type_agg_id = builder
            .make_any_aggregator(&agg_nodes, Some(any_aggregator_info), None::<()>, None)
            .map_err(KgraphError::GraphConstructionError)?;

        let all_aggregator_info = "All aggregation for PaymentMethod";
        let enabled_pm_agg_id = builder
            .make_all_aggregator(
                &[
                    (pm_id, cgraph::Relation::Positive, cgraph::Strength::Strong),
                    (
                        pm_type_agg_id,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Strong,
                    ),
                ],
                Some(all_aggregator_info),
                None::<()>,
                None,
            )
            .map_err(KgraphError::GraphConstructionError)?;

        Some(enabled_pm_agg_id)
    } else {
        None
    };

    Ok(agg_id)
}

macro_rules! collect_global_variants {
    ($parent_enum:ident) => {
        &mut dir::enums::$parent_enum::iter()
            .map(dir::DirValue::$parent_enum)
            .collect::<Vec<_>>()
    };
}

// #[cfg(feature = "v1")]
fn global_vec_pmt(
    enabled_pmt: Vec<dir::DirValue>,
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
) -> Vec<cgraph::NodeId> {
    let mut global_vector: Vec<dir::DirValue> = Vec::new();

    global_vector.append(collect_global_variants!(PayLaterType));
    global_vector.append(collect_global_variants!(WalletType));
    global_vector.append(collect_global_variants!(BankRedirectType));
    global_vector.append(collect_global_variants!(BankDebitType));
    global_vector.append(collect_global_variants!(CryptoType));
    global_vector.append(collect_global_variants!(RewardType));
    global_vector.append(collect_global_variants!(RealTimePaymentType));
    global_vector.append(collect_global_variants!(UpiType));
    global_vector.append(collect_global_variants!(VoucherType));
    global_vector.append(collect_global_variants!(GiftCardType));
    global_vector.append(collect_global_variants!(BankTransferType));
    global_vector.append(collect_global_variants!(CardRedirectType));
    global_vector.append(collect_global_variants!(OpenBankingType));
    global_vector.append(collect_global_variants!(MobilePaymentType));
    global_vector.push(dir::DirValue::PaymentMethod(
        dir::enums::PaymentMethod::Card,
    ));

    let global_vector = global_vector
        .into_iter()
        .filter(|global_value| !enabled_pmt.contains(global_value))
        .collect::<Vec<_>>();

    global_vector
        .into_iter()
        .map(|dir_v| {
            builder.make_value_node(
                cgraph::NodeValue::Value(dir_v),
                Some("Payment Method Type"),
                None::<()>,
            )
        })
        .collect::<Vec<_>>()
}

fn compile_graph_for_countries_and_currencies(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    config: &kgraph_types::CurrencyCountryFlowFilter,
    payment_method_type_node: cgraph::NodeId,
) -> Result<cgraph::NodeId, KgraphError> {
    let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();
    agg_nodes.push((
        payment_method_type_node,
        cgraph::Relation::Positive,
        cgraph::Strength::Normal,
    ));
    if let Some(country) = config.country.clone() {
        let node_country = country
            .into_iter()
            .map(|country| dir::DirValue::BillingCountry(api_enums::Country::from_alpha2(country)))
            .collect();
        let country_agg = builder
            .make_in_aggregator(node_country, Some("Configs for Country"), None::<()>)
            .map_err(KgraphError::GraphConstructionError)?;
        agg_nodes.push((
            country_agg,
            cgraph::Relation::Positive,
            cgraph::Strength::Weak,
        ))
    }

    if let Some(currency) = config.currency.clone() {
        let node_currency = currency
            .into_iter()
            .map(IntoDirValue::into_dir_value)
            .collect::<Result<Vec<_>, _>>()?;
        let currency_agg = builder
            .make_in_aggregator(node_currency, Some("Configs for Currency"), None::<()>)
            .map_err(KgraphError::GraphConstructionError)?;
        agg_nodes.push((
            currency_agg,
            cgraph::Relation::Positive,
            cgraph::Strength::Normal,
        ))
    }
    if let Some(capture_method) = config
        .not_available_flows
        .and_then(|naf| naf.capture_method)
    {
        let make_capture_node = builder.make_value_node(
            cgraph::NodeValue::Value(dir::DirValue::CaptureMethod(capture_method)),
            Some("Configs for CaptureMethod"),
            None::<()>,
        );
        agg_nodes.push((
            make_capture_node,
            cgraph::Relation::Negative,
            cgraph::Strength::Normal,
        ))
    }

    builder
        .make_all_aggregator(
            &agg_nodes,
            Some("Country & Currency Configs With Payment Method Type"),
            None::<()>,
            None,
        )
        .map_err(KgraphError::GraphConstructionError)
}

// #[cfg(feature = "v1")]
fn compile_config_graph(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    config: &kgraph_types::CountryCurrencyFilter,
    connector: api_enums::RoutableConnectors,
) -> Result<cgraph::NodeId, KgraphError> {
    let mut agg_node_id: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();
    let mut pmt_enabled: Vec<dir::DirValue> = Vec::new();
    if let Some(pmt) = config
        .connector_configs
        .get(&connector)
        .or(config.default_configs.as_ref())
        .map(|inner| inner.0.clone())
    {
        for pm_filter_key in pmt {
            match pm_filter_key {
                (kgraph_types::PaymentMethodFilterKey::PaymentMethodType(pm), filter) => {
                    let dir_val_pm = get_dir_value_payment_method(pm)?;

                    let pm_node = if pm == api_enums::PaymentMethodType::Credit
                        || pm == api_enums::PaymentMethodType::Debit
                    {
                        pmt_enabled
                            .push(dir::DirValue::PaymentMethod(api_enums::PaymentMethod::Card));
                        builder.make_value_node(
                            cgraph::NodeValue::Value(dir::DirValue::PaymentMethod(
                                dir::enums::PaymentMethod::Card,
                            )),
                            Some("PaymentMethod"),
                            None::<()>,
                        )
                    } else {
                        pmt_enabled.push(dir_val_pm.clone());
                        builder.make_value_node(
                            cgraph::NodeValue::Value(dir_val_pm),
                            Some("PaymentMethodType"),
                            None::<()>,
                        )
                    };

                    let node_config =
                        compile_graph_for_countries_and_currencies(builder, &filter, pm_node)?;

                    agg_node_id.push((
                        node_config,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Normal,
                    ));
                }
                (kgraph_types::PaymentMethodFilterKey::CardNetwork(cn), filter) => {
                    let dir_val_cn = cn.clone().into_dir_value()?;
                    pmt_enabled.push(dir_val_cn);
                    let cn_node = builder.make_value_node(
                        cn.clone().into_dir_value().map(Into::into)?,
                        Some("CardNetwork"),
                        None::<()>,
                    );
                    let node_config =
                        compile_graph_for_countries_and_currencies(builder, &filter, cn_node)?;

                    agg_node_id.push((
                        node_config,
                        cgraph::Relation::Positive,
                        cgraph::Strength::Normal,
                    ));
                }
            }
        }
    }
    let global_vector_pmt: Vec<cgraph::NodeId> = global_vec_pmt(pmt_enabled, builder);
    let any_agg_pmt: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = global_vector_pmt
        .into_iter()
        .map(|node| (node, cgraph::Relation::Positive, cgraph::Strength::Normal))
        .collect::<Vec<_>>();
    let any_agg_node = builder
        .make_any_aggregator(
            &any_agg_pmt,
            Some("Any Aggregator For Payment Method Types"),
            None::<()>,
            None,
        )
        .map_err(KgraphError::GraphConstructionError)?;

    agg_node_id.push((
        any_agg_node,
        cgraph::Relation::Positive,
        cgraph::Strength::Normal,
    ));

    builder
        .make_any_aggregator(&agg_node_id, Some("Configs"), None::<()>, None)
        .map_err(KgraphError::GraphConstructionError)
}

#[cfg(feature = "v2")]
fn compile_merchant_connector_graph(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    mca: admin_api::MerchantConnectorResponse,
    config: &kgraph_types::CountryCurrencyFilter,
) -> Result<(), KgraphError> {
    let connector = common_enums::RoutableConnectors::try_from(mca.connector_name)
        .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name))?;

    let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

    if let Some(pms_enabled) = mca.payment_methods_enabled.clone() {
        for pm_enabled in pms_enabled {
            let maybe_pm_enabled_id = compile_payment_method_enabled(builder, pm_enabled)?;
            if let Some(pm_enabled_id) = maybe_pm_enabled_id {
                agg_nodes.push((
                    pm_enabled_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Strong,
                ));
            }
        }
    }

    let aggregator_info = "Available Payment methods for connector";
    let pms_enabled_agg_id = builder
        .make_any_aggregator(&agg_nodes, Some(aggregator_info), None::<()>, None)
        .map_err(KgraphError::GraphConstructionError)?;

    let config_info = "Config for respective PaymentMethodType for the connector";

    let config_enabled_agg_id = compile_config_graph(builder, config, connector)?;

    let domain_level_node_id = builder
        .make_all_aggregator(
            &[
                (
                    config_enabled_agg_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Normal,
                ),
                (
                    pms_enabled_agg_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Normal,
                ),
            ],
            Some(config_info),
            None::<()>,
            None,
        )
        .map_err(KgraphError::GraphConstructionError)?;
    let connector_dir_val = dir::DirValue::Connector(Box::new(ast::ConnectorChoice { connector }));

    let connector_info = "Connector";
    let connector_node_id =
        builder.make_value_node(connector_dir_val.into(), Some(connector_info), None::<()>);

    builder
        .make_edge(
            domain_level_node_id,
            connector_node_id,
            cgraph::Strength::Normal,
            cgraph::Relation::Positive,
            None::<cgraph::DomainId>,
        )
        .map_err(KgraphError::GraphConstructionError)?;

    Ok(())
}

#[cfg(feature = "v1")]
fn compile_merchant_connector_graph(
    builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
    mca: admin_api::MerchantConnectorResponse,
    config: &kgraph_types::CountryCurrencyFilter,
) -> Result<(), KgraphError> {
    let connector = common_enums::RoutableConnectors::from_str(&mca.connector_name)
        .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?;

    let mut agg_nodes: Vec<(cgraph::NodeId, cgraph::Relation, cgraph::Strength)> = Vec::new();

    if let Some(pms_enabled) = mca.payment_methods_enabled.clone() {
        for pm_enabled in pms_enabled {
            let maybe_pm_enabled_id = compile_payment_method_enabled(builder, pm_enabled)?;
            if let Some(pm_enabled_id) = maybe_pm_enabled_id {
                agg_nodes.push((
                    pm_enabled_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Strong,
                ));
            }
        }
    }

    let aggregator_info = "Available Payment methods for connector";
    let pms_enabled_agg_id = builder
        .make_any_aggregator(&agg_nodes, Some(aggregator_info), None::<()>, None)
        .map_err(KgraphError::GraphConstructionError)?;

    let config_info = "Config for respective PaymentMethodType for the connector";

    let config_enabled_agg_id = compile_config_graph(builder, config, connector)?;

    let domain_level_node_id = builder
        .make_all_aggregator(
            &[
                (
                    config_enabled_agg_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Normal,
                ),
                (
                    pms_enabled_agg_id,
                    cgraph::Relation::Positive,
                    cgraph::Strength::Normal,
                ),
            ],
            Some(config_info),
            None::<()>,
            None,
        )
        .map_err(KgraphError::GraphConstructionError)?;
    let connector_dir_val = dir::DirValue::Connector(Box::new(ast::ConnectorChoice { connector }));

    let connector_info = "Connector";
    let connector_node_id =
        builder.make_value_node(connector_dir_val.into(), Some(connector_info), None::<()>);

    builder
        .make_edge(
            domain_level_node_id,
            connector_node_id,
            cgraph::Strength::Normal,
            cgraph::Relation::Positive,
            None::<cgraph::DomainId>,
        )
        .map_err(KgraphError::GraphConstructionError)?;

    Ok(())
}

// #[cfg(feature = "v1")]
pub fn make_mca_graph(
    accts: Vec<admin_api::MerchantConnectorResponse>,
    config: &kgraph_types::CountryCurrencyFilter,
) -> Result<cgraph::ConstraintGraph<dir::DirValue>, KgraphError> {
    let mut builder = cgraph::ConstraintGraphBuilder::new();
    let _domain = builder.make_domain(
        DOMAIN_IDENTIFIER.to_string(),
        "Payment methods enabled for MerchantConnectorAccount",
    );
    for acct in accts {
        compile_merchant_connector_graph(&mut builder, acct, config)?;
    }

    Ok(builder.build())
}

#[cfg(feature = "v1")]
#[cfg(test)]
mod tests {
    use std::collections::{HashMap, HashSet};

    use api_models::enums as api_enums;
    use euclid::{
        dirval,
        dssa::graph::{AnalysisContext, CgraphExt},
    };
    use hyperswitch_constraint_graph::{ConstraintGraph, CycleCheck, Memoization};

    use super::*;
    use crate::types as kgraph_types;

    fn build_test_data() -> ConstraintGraph<dir::DirValue> {
        use api_models::{admin::*, payment_methods::*};
        let profile_id = common_utils::generate_profile_id_of_default_length();

        // #[cfg(feature = "v2")]
        // let stripe_account = MerchantConnectorResponse {
        //     connector_type: api_enums::ConnectorType::FizOperations,
        //     connector_name: "stripe".to_string(),
        //     id: common_utils::generate_merchant_connector_account_id_of_default_length(),
        //     connector_label: Some("something".to_string()),
        //     connector_account_details: masking::Secret::new(serde_json::json!({})),
        //     disabled: None,
        //     metadata: None,
        //     payment_methods_enabled: Some(vec![PaymentMethodsEnabled {
        //         payment_method: api_enums::PaymentMethod::Card,
        //         payment_method_types: Some(vec![
        //             RequestPaymentMethodTypes {
        //                 payment_method_type: api_enums::PaymentMethodType::Credit,
        //                 payment_experience: None,
        //                 card_networks: Some(vec![
        //                     api_enums::CardNetwork::Visa,
        //                     api_enums::CardNetwork::Mastercard,
        //                 ]),
        //                 accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![
        //                     api_enums::Currency::INR,
        //                 ])),
        //                 accepted_countries: None,
        //                 minimum_amount: Some(MinorUnit::new(10)),
        //                 maximum_amount: Some(MinorUnit::new(1000)),
        //                 recurring_enabled: true,
        //                 installment_payment_enabled: true,
        //             },
        //             RequestPaymentMethodTypes {
        //                 payment_method_type: api_enums::PaymentMethodType::Debit,
        //                 payment_experience: None,
        //                 card_networks: Some(vec![
        //                     api_enums::CardNetwork::Maestro,
        //                     api_enums::CardNetwork::JCB,
        //                 ]),
        //                 accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![
        //                     api_enums::Currency::GBP,
        //                 ])),
        //                 accepted_countries: None,
        //                 minimum_amount: Some(MinorUnit::new(10)),
        //                 maximum_amount: Some(MinorUnit::new(1000)),
        //                 recurring_enabled: true,
        //                 installment_payment_enabled: true,
        //             },
        //         ]),
        //     }]),
        //     frm_configs: None,
        //     connector_webhook_details: None,
        //     profile_id,
        //     applepay_verified_domains: None,
        //     pm_auth_config: None,
        //     status: api_enums::ConnectorStatus::Inactive,
        //     additional_merchant_data: None,
        //     connector_wallets_details: None,
        // };
        #[cfg(feature = "v1")]
        let stripe_account = MerchantConnectorResponse {
            connector_type: api_enums::ConnectorType::FizOperations,
            connector_name: "stripe".to_string(),
            merchant_connector_id:
                common_utils::generate_merchant_connector_account_id_of_default_length(),
            business_country: Some(api_enums::CountryAlpha2::US),
            connector_label: Some("something".to_string()),
            business_label: Some("food".to_string()),
            business_sub_label: None,
            connector_account_details: masking::Secret::new(serde_json::json!({})),
            test_mode: None,
            disabled: None,
            metadata: None,
            payment_methods_enabled: Some(vec![PaymentMethodsEnabled {
                payment_method: api_enums::PaymentMethod::Card,
                payment_method_types: Some(vec![
                    RequestPaymentMethodTypes {
                        payment_method_type: api_enums::PaymentMethodType::Credit,
                        payment_experience: None,
                        card_networks: Some(vec![
                            api_enums::CardNetwork::Visa,
                            api_enums::CardNetwork::Mastercard,
                        ]),
                        accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![
                            api_enums::Currency::INR,
                        ])),
                        accepted_countries: None,
                        minimum_amount: Some(MinorUnit::new(10)),
                        maximum_amount: Some(MinorUnit::new(1000)),
                        recurring_enabled: Some(true),
                        installment_payment_enabled: Some(true),
                    },
                    RequestPaymentMethodTypes {
                        payment_method_type: api_enums::PaymentMethodType::Debit,
                        payment_experience: None,
                        card_networks: Some(vec![
                            api_enums::CardNetwork::Maestro,
                            api_enums::CardNetwork::JCB,
                        ]),
                        accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![
                            api_enums::Currency::GBP,
                        ])),
                        accepted_countries: None,
                        minimum_amount: Some(MinorUnit::new(10)),
                        maximum_amount: Some(MinorUnit::new(1000)),
                        recurring_enabled: Some(true),
                        installment_payment_enabled: Some(true),
                    },
                ]),
            }]),
            frm_configs: None,
            connector_webhook_details: None,
            profile_id,
            applepay_verified_domains: None,
            pm_auth_config: None,
            status: api_enums::ConnectorStatus::Inactive,
            additional_merchant_data: None,
            connector_wallets_details: None,
        };

        let config_map = kgraph_types::CountryCurrencyFilter {
            connector_configs: HashMap::from([(
                api_enums::RoutableConnectors::Stripe,
                kgraph_types::PaymentMethodFilters(HashMap::from([
                    (
                        kgraph_types::PaymentMethodFilterKey::PaymentMethodType(
                            api_enums::PaymentMethodType::Credit,
                        ),
                        kgraph_types::CurrencyCountryFlowFilter {
                            currency: Some(HashSet::from([
                                api_enums::Currency::INR,
                                api_enums::Currency::USD,
                            ])),
                            country: Some(HashSet::from([api_enums::CountryAlpha2::IN])),
                            not_available_flows: Some(kgraph_types::NotAvailableFlows {
                                capture_method: Some(api_enums::CaptureMethod::Manual),
                            }),
                        },
                    ),
                    (
                        kgraph_types::PaymentMethodFilterKey::PaymentMethodType(
                            api_enums::PaymentMethodType::Debit,
                        ),
                        kgraph_types::CurrencyCountryFlowFilter {
                            currency: Some(HashSet::from([
                                api_enums::Currency::GBP,
                                api_enums::Currency::PHP,
                            ])),
                            country: Some(HashSet::from([api_enums::CountryAlpha2::IN])),
                            not_available_flows: Some(kgraph_types::NotAvailableFlows {
                                capture_method: Some(api_enums::CaptureMethod::Manual),
                            }),
                        },
                    ),
                ])),
            )]),
            default_configs: None,
        };

        make_mca_graph(vec![stripe_account], &config_map).expect("Failed graph construction")
    }

    #[test]
    fn test_credit_card_success_case() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(PaymentMethod = Card),
                dirval!(CardType = Credit),
                dirval!(CardNetwork = Visa),
                dirval!(PaymentCurrency = INR),
                dirval!(PaymentAmount = 101),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        assert!(result.is_ok());
    }

    #[test]
    fn test_debit_card_success_case() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(PaymentMethod = Card),
                dirval!(CardType = Debit),
                dirval!(CardNetwork = Maestro),
                dirval!(PaymentCurrency = GBP),
                dirval!(PaymentAmount = 100),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        assert!(result.is_ok());
    }

    #[test]
    fn test_single_mismatch_failure_case() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(PaymentMethod = Card),
                dirval!(CardType = Debit),
                dirval!(CardNetwork = Maestro),
                dirval!(PaymentCurrency = PHP),
                dirval!(PaymentAmount = 100),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_amount_mismatch_failure_case() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(PaymentMethod = Card),
                dirval!(CardType = Debit),
                dirval!(CardNetwork = Visa),
                dirval!(PaymentCurrency = GBP),
                dirval!(PaymentAmount = 7),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_incomplete_data_failure_case() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(PaymentMethod = Card),
                dirval!(CardType = Debit),
                dirval!(PaymentCurrency = GBP),
                dirval!(PaymentAmount = 7),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        //println!("{:#?}", result);
        //println!("{}", serde_json::to_string_pretty(&result).expect("Hello"));

        assert!(result.is_err());
    }

    #[test]
    fn test_incomplete_data_failure_case2() {
        let graph = build_test_data();

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &AnalysisContext::from_dir_values([
                dirval!(Connector = Stripe),
                dirval!(CardType = Debit),
                dirval!(CardNetwork = Visa),
                dirval!(PaymentCurrency = GBP),
                dirval!(PaymentAmount = 100),
            ]),
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        //println!("{:#?}", result);
        //println!("{}", serde_json::to_string_pretty(&result).expect("Hello"));

        assert!(result.is_err());
    }

    #[test]
    fn test_sandbox_applepay_bug_usecase() {
        let value = serde_json::json!([
            {
                "connector_type": "payment_processor",
                "connector_name": "bluesnap",
                "merchant_connector_id": "REDACTED",
                "status": "inactive",
                "connector_account_details": {
                    "auth_type": "BodyKey",
                    "api_key": "REDACTED",
                    "key1": "REDACTED"
                },
                "test_mode": true,
                "disabled": false,
                "payment_methods_enabled": [
                    {
                        "payment_method": "card",
                        "payment_method_types": [
                            {
                                "payment_method_type": "credit",
                                "payment_experience": null,
                                "card_networks": [
                                    "Mastercard",
                                    "Visa",
                                    "AmericanExpress",
                                    "JCB",
                                    "DinersClub",
                                    "Discover",
                                    "CartesBancaires",
                                    "UnionPay"
                                ],
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            },
                            {
                                "payment_method_type": "debit",
                                "payment_experience": null,
                                "card_networks": [
                                    "Mastercard",
                                    "Visa",
                                    "Interac",
                                    "AmericanExpress",
                                    "JCB",
                                    "DinersClub",
                                    "Discover",
                                    "CartesBancaires",
                                    "UnionPay"
                                ],
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            }
                        ]
                    },
                    {
                        "payment_method": "wallet",
                        "payment_method_types": [
                            {
                                "payment_method_type": "google_pay",
                                "payment_experience": "invoke_sdk_client",
                                "card_networks": null,
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            }
                        ]
                    }
                ],
                "metadata": {},
                "business_country": "US",
                "business_label": "default",
                "business_sub_label": null,
                "frm_configs": null
            },
            {
                "connector_type": "payment_processor",
                "connector_name": "stripe",
                "merchant_connector_id": "REDACTED",
                "status": "inactive",
                "connector_account_details": {
                    "auth_type": "HeaderKey",
                    "api_key": "REDACTED"
                },
                "test_mode": true,
                "disabled": false,
                "payment_methods_enabled": [
                    {
                        "payment_method": "card",
                        "payment_method_types": [
                            {
                                "payment_method_type": "credit",
                                "payment_experience": null,
                                "card_networks": [
                                    "Mastercard",
                                    "Visa",
                                    "AmericanExpress",
                                    "JCB",
                                    "DinersClub",
                                    "Discover",
                                    "CartesBancaires",
                                    "UnionPay"
                                ],
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            },
                            {
                                "payment_method_type": "debit",
                                "payment_experience": null,
                                "card_networks": [
                                    "Mastercard",
                                    "Visa",
                                    "Interac",
                                    "AmericanExpress",
                                    "JCB",
                                    "DinersClub",
                                    "Discover",
                                    "CartesBancaires",
                                    "UnionPay"
                                ],
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            }
                        ]
                    },
                    {
                        "payment_method": "wallet",
                        "payment_method_types": [
                            {
                                "payment_method_type": "apple_pay",
                                "payment_experience": "invoke_sdk_client",
                                "card_networks": null,
                                "accepted_currencies": null,
                                "accepted_countries": null,
                                "minimum_amount": 1,
                                "maximum_amount": 68607706,
                                "recurring_enabled": true,
                                "installment_payment_enabled": true
                            }
                        ]
                    },
                    {
                        "payment_method": "pay_later",
                        "payment_method_types": []
                    }
                ],
                "metadata": {},
                "business_country": "US",
                "business_label": "default",
                "business_sub_label": null,
                "frm_configs": null
            }
        ]);

        let data: Vec<admin_api::MerchantConnectorResponse> =
            serde_json::from_value(value).expect("data");
        let config = kgraph_types::CountryCurrencyFilter {
            connector_configs: HashMap::new(),
            default_configs: None,
        };
        let graph = make_mca_graph(data, &config).expect("graph");
        let context = AnalysisContext::from_dir_values([
            dirval!(Connector = Stripe),
            dirval!(PaymentAmount = 212),
            dirval!(PaymentCurrency = ILS),
            dirval!(PaymentMethod = Wallet),
            dirval!(WalletType = ApplePay),
        ]);

        let result = graph.key_value_analysis(
            dirval!(Connector = Stripe),
            &context,
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );

        assert!(result.is_ok(), "stripe validation failed");

        let result = graph.key_value_analysis(
            dirval!(Connector = Bluesnap),
            &context,
            &mut Memoization::new(),
            &mut CycleCheck::new(),
            None,
        );
        assert!(result.is_err(), "bluesnap validation failed");
    }
}


// File: crates/external_services/Cargo.toml

[package]
name = "external_services"
description = "Interactions of the router with external systems"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
license.workspace = true

[features]
aws_kms = ["dep:aws-config", "dep:aws-sdk-kms"]
email = ["dep:aws-config"]
aws_s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
hashicorp-vault = ["dep:vaultrs"]
superposition = ["dep:open-feature", "dep:superposition_provider"]
v1 = ["hyperswitch_interfaces/v1", "common_utils/v1"]
v2 = ["hyperswitch_interfaces/v2", "common_utils/v2"]
revenue_recovery = [
    "dep:prost",
    "dep:router_env",
    "tokio/macros",
    "tokio/rt-multi-thread",
    "dep:hyper-util",
    "dep:http-body-util",
    "dep:prost-types",
]
dynamic_routing = [
    "dep:prost",
    "dep:api_models",
    "tokio/macros",
    "tokio/rt-multi-thread",
    "dep:router_env",
    "dep:hyper-util",
    "dep:http-body-util",
]

[dependencies]
async-trait = "0.1.88"
aws-config = { version = "1.5.10", optional = true, features = ["behavior-version-latest"] }
aws-sdk-kms = { version = "1.51.0", optional = true }
aws-sdk-sesv2 = "1.57.0"
aws-sdk-sts = "1.51.0"
aws-sdk-s3 = { version = "1.65.0", optional = true }
aws-smithy-runtime = "1.8.3"
base64 = "0.22.1"
dyn-clone = "1.0.19"
error-stack = "0.4.1"
hex = "0.4.3"
hyper = "0.14.32"
hyper-proxy = "0.9.1"
lettre = "0.11.16"
once_cell = "1.21.3"
serde = { version = "1.0.219", features = ["derive"] }
thiserror = "1.0.69"
serde_json = "1.0.140"
serde_urlencoded = "0.7.1"
vaultrs = { version = "0.7.4", optional = true }
prost = { version = "0.13", optional = true }
prost-types = { version = "0.13", optional = true }
time = { version = "0.3.41", features = ["serde", "serde-well-known", "std"] }
tokio = "1.48.0"
tonic = "0.13.1"
tonic-reflection = "0.13.1"
tonic-types = "0.13.1"
typed-builder = "0.21.2"
hyper-util = { version = "0.1.12", optional = true }
http-body-util = { version = "0.1.3", optional = true }
reqwest = { version = "0.11.27", features = ["rustls-tls"] }
http = "0.2.12"
url = { version = "2.5.4", features = ["serde"] }
quick-xml = { version = "0.31.0", features = ["serialize"] }
unified-connector-service-client = { git = "https://github.com/juspay/connector-service", rev = "b5a367c1c775c32503feb00cdc6cd063cc97ea3b", package = "rust-grpc-client" }
open-feature = { version = "0.2.5", optional = true }
superposition_provider = { version = "0.88.1", optional = true }

# First party crates
common_utils = { version = "0.1.0", path = "../common_utils" }
common_enums = { version = "0.1.0", path = "../common_enums" }
hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces", default-features = false }
masking = { version = "0.1.0", path = "../masking" }
router_env = { version = "0.1.0", path = "../router_env", features = [
    "log_extra_implicit_fields",
    "log_custom_entries_to_extra",
] }
api_models = { version = "0.1.0", path = "../api_models", optional = true }

[build-dependencies]
tonic-build = "0.13.1"
router_env = { version = "0.1.0", path = "../router_env", default-features = false, optional = true }

[lints]
workspace = true


// File: crates/external_services/build.rs

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Compilation for revenue recovery protos
    #[cfg(feature = "revenue_recovery")]
    {
        let proto_base_path = router_env::workspace_path().join("proto");
        let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
        let recovery_proto_files = [proto_base_path.join("recovery_decider.proto")];

        #[allow(clippy::expect_used, clippy::unwrap_in_result)]
        tonic_build::configure()
            .out_dir(&out_dir)
            .compile_well_known_types(true)
            .extern_path(".google.protobuf.Timestamp", "::prost_types::Timestamp")
            .compile_protos(&recovery_proto_files, &[&proto_base_path])
            .expect("Failed to compile revenue-recovery proto files");
    }

    // Compilation for dynamic_routing protos
    #[cfg(feature = "dynamic_routing")]
    {
        // Get the directory of the current crate
        let proto_path = router_env::workspace_path().join("proto");
        let success_rate_proto_file = proto_path.join("success_rate.proto");
        let contract_routing_proto_file = proto_path.join("contract_routing.proto");
        let elimination_proto_file = proto_path.join("elimination_rate.proto");
        let health_check_proto_file = proto_path.join("health_check.proto");
        let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);

        // Compile the .proto file
        #[allow(clippy::expect_used, clippy::unwrap_in_result)]
        tonic_build::configure()
            .out_dir(out_dir)
            .compile_protos(
                &[
                    success_rate_proto_file,
                    health_check_proto_file,
                    elimination_proto_file,
                    contract_routing_proto_file,
                ],
                &[proto_path],
            )
            .expect("Failed to compile proto files");
    }
    Ok(())
}


// File: crates/external_services/README.md

# External Services

This crate includes interactions with external systems, which may be shared
across crates within this workspace.


// File: crates/external_services/src/superposition.rs

//! Superposition client for dynamic configuration management

/// Type definitions for Superposition integration
pub mod types;

use std::collections::HashMap;

use common_utils::errors::CustomResult;
use error_stack::report;
use masking::ExposeInterface;

pub use self::types::{ConfigContext, SuperpositionClientConfig, SuperpositionError};

fn convert_open_feature_value(value: open_feature::Value) -> Result<serde_json::Value, String> {
    match value {
        open_feature::Value::String(s) => Ok(serde_json::Value::String(s)),
        open_feature::Value::Bool(b) => Ok(serde_json::Value::Bool(b)),
        open_feature::Value::Int(n) => Ok(serde_json::Value::Number(serde_json::Number::from(n))),
        open_feature::Value::Float(f) => serde_json::Number::from_f64(f)
            .map(serde_json::Value::Number)
            .ok_or_else(|| format!("Invalid number: {f}")),
        open_feature::Value::Struct(sv) => Ok(types::JsonValue::try_from(sv)?.into_inner()),
        open_feature::Value::Array(values) => Ok(serde_json::Value::Array(
            values
                .into_iter()
                .map(convert_open_feature_value)
                .collect::<Result<Vec<_>, _>>()?,
        )),
    }
}

/// Superposition client wrapper
// Debug trait cannot be derived because open_feature::Client doesn't implement Debug
#[allow(missing_debug_implementations)]
pub struct SuperpositionClient {
    client: open_feature::Client,
}

impl SuperpositionClient {
    /// Create a new Superposition client
    pub async fn new(config: SuperpositionClientConfig) -> CustomResult<Self, SuperpositionError> {
        let provider_options = superposition_provider::SuperpositionProviderOptions {
            endpoint: config.endpoint.clone(),
            token: config.token.expose(),
            org_id: config.org_id.clone(),
            workspace_id: config.workspace_id.clone(),
            fallback_config: None,
            evaluation_cache: None,
            refresh_strategy: superposition_provider::RefreshStrategy::Polling(
                superposition_provider::PollingStrategy {
                    interval: config.polling_interval,
                    timeout: config.request_timeout,
                },
            ),
            experimentation_options: None,
        };

        // Create provider and set up OpenFeature
        let provider = superposition_provider::SuperpositionProvider::new(provider_options);

        // Initialize OpenFeature API and set provider
        let mut api = open_feature::OpenFeature::singleton_mut().await;
        api.set_provider(provider).await;

        // Create client
        let client = api.create_client();

        router_env::logger::info!("Superposition client initialized successfully");

        Ok(Self { client })
    }

    /// Build evaluation context for Superposition requests
    fn build_evaluation_context(
        &self,
        context: Option<&ConfigContext>,
    ) -> open_feature::EvaluationContext {
        open_feature::EvaluationContext {
            custom_fields: context.map_or(HashMap::new(), |ctx| {
                ctx.values
                    .iter()
                    .map(|(k, v)| {
                        (
                            k.clone(),
                            open_feature::EvaluationContextFieldValue::String(v.clone()),
                        )
                    })
                    .collect()
            }),
            targeting_key: None,
        }
    }

    /// Get a boolean configuration value from Superposition
    pub async fn get_bool_value(
        &self,
        key: &str,
        context: Option<&ConfigContext>,
    ) -> CustomResult<bool, SuperpositionError> {
        let evaluation_context = self.build_evaluation_context(context);

        self.client
            .get_bool_value(key, Some(&evaluation_context), None)
            .await
            .map_err(|e| {
                report!(SuperpositionError::ClientError(format!(
                    "Failed to get bool value for key '{key}': {e:?}"
                )))
            })
    }

    /// Get a string configuration value from Superposition
    pub async fn get_string_value(
        &self,
        key: &str,
        context: Option<&ConfigContext>,
    ) -> CustomResult<String, SuperpositionError> {
        let evaluation_context = self.build_evaluation_context(context);

        self.client
            .get_string_value(key, Some(&evaluation_context), None)
            .await
            .map_err(|e| {
                report!(SuperpositionError::ClientError(format!(
                    "Failed to get string value for key '{key}': {e:?}"
                )))
            })
    }

    /// Get an integer configuration value from Superposition
    pub async fn get_int_value(
        &self,
        key: &str,
        context: Option<&ConfigContext>,
    ) -> CustomResult<i64, SuperpositionError> {
        let evaluation_context = self.build_evaluation_context(context);

        self.client
            .get_int_value(key, Some(&evaluation_context), None)
            .await
            .map_err(|e| {
                report!(SuperpositionError::ClientError(format!(
                    "Failed to get int value for key '{key}': {e:?}"
                )))
            })
    }

    /// Get a float configuration value from Superposition
    pub async fn get_float_value(
        &self,
        key: &str,
        context: Option<&ConfigContext>,
    ) -> CustomResult<f64, SuperpositionError> {
        let evaluation_context = self.build_evaluation_context(context);

        self.client
            .get_float_value(key, Some(&evaluation_context), None)
            .await
            .map_err(|e| {
                report!(SuperpositionError::ClientError(format!(
                    "Failed to get float value for key '{key}': {e:?}"
                )))
            })
    }

    /// Get an object configuration value from Superposition
    pub async fn get_object_value(
        &self,
        key: &str,
        context: Option<&ConfigContext>,
    ) -> CustomResult<serde_json::Value, SuperpositionError> {
        let evaluation_context = self.build_evaluation_context(context);

        let json_result = self
            .client
            .get_struct_value::<types::JsonValue>(key, Some(&evaluation_context), None)
            .await
            .map_err(|e| {
                report!(SuperpositionError::ClientError(format!(
                    "Failed to get object value for key '{key}': {e:?}"
                )))
            })?;

        Ok(json_result.into_inner())
    }
}


// File: crates/external_services/src/crm.rs

use std::sync::Arc;

use common_utils::{
    errors::CustomResult,
    ext_traits::ConfigExt,
    request::{Method, Request, RequestBuilder, RequestContent},
};
use error_stack::ResultExt;
use http::header;
use hyperswitch_interfaces::{
    crm::{CrmInterface, CrmPayload},
    errors::HttpClientError,
    types::Proxy,
};
use router_env::logger;

use crate::{http_client, hubspot_proxy::HubspotRequest};

/// Hubspot Crm configuration
#[derive(Debug, Clone, serde::Deserialize)]
pub struct HubspotProxyConfig {
    /// The ID of the Hubspot form to be submitted.
    pub form_id: String,

    /// The URL to which the Hubspot form data will be sent.
    pub request_url: String,
}

impl HubspotProxyConfig {
    /// Validates Hubspot configuration
    pub(super) fn validate(&self) -> Result<(), InvalidCrmConfig> {
        use common_utils::fp_utils::when;

        when(self.request_url.is_default_or_empty(), || {
            Err(InvalidCrmConfig("request url must not be empty"))
        })?;

        when(self.form_id.is_default_or_empty(), || {
            Err(InvalidCrmConfig("form_id must not be empty"))
        })
    }
}

/// Error thrown when the crm config is invalid
#[derive(Debug, Clone)]
pub struct InvalidCrmConfig(pub &'static str);

impl std::error::Error for InvalidCrmConfig {}

impl std::fmt::Display for InvalidCrmConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "crm: {}", self.0)
    }
}

#[derive(Debug, Clone, Copy)]
/// NoCrm struct
pub struct NoCrm;

/// Enum representing different Crm configurations
#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(tag = "crm_manager")]
#[serde(rename_all = "snake_case")]
pub enum CrmManagerConfig {
    /// Hubspot Crm configuration
    HubspotProxy {
        /// Hubspot Crm configuration
        hubspot_proxy: HubspotProxyConfig,
    },

    /// No Crm configuration
    #[default]
    NoCrm,
}

impl CrmManagerConfig {
    /// Verifies that the client configuration is usable
    pub fn validate(&self) -> Result<(), InvalidCrmConfig> {
        match self {
            Self::HubspotProxy { hubspot_proxy } => hubspot_proxy.validate(),
            Self::NoCrm => Ok(()),
        }
    }

    /// Retrieves the appropriate Crm client based on the configuration.
    pub async fn get_crm_client(&self) -> Arc<dyn CrmInterface> {
        match self {
            Self::HubspotProxy { hubspot_proxy } => Arc::new(hubspot_proxy.clone()),
            Self::NoCrm => Arc::new(NoCrm),
        }
    }
}

#[async_trait::async_trait]
impl CrmInterface for NoCrm {
    async fn make_body(&self, _details: CrmPayload) -> RequestContent {
        RequestContent::Json(Box::new(()))
    }

    async fn make_request(&self, _body: RequestContent, _origin_base_url: String) -> Request {
        RequestBuilder::default().build()
    }

    async fn send_request(
        &self,
        _proxy: &Proxy,
        _request: Request,
    ) -> CustomResult<reqwest::Response, HttpClientError> {
        logger::info!("No CRM configured!");
        Err(HttpClientError::UnexpectedState).attach_printable("No CRM configured!")
    }
}

#[async_trait::async_trait]
impl CrmInterface for HubspotProxyConfig {
    async fn make_body(&self, details: CrmPayload) -> RequestContent {
        RequestContent::FormUrlEncoded(Box::new(HubspotRequest::new(
            details.business_country_name.unwrap_or_default(),
            self.form_id.clone(),
            details.poc_name.unwrap_or_default(),
            details.poc_email.clone().unwrap_or_default(),
            details.legal_business_name.unwrap_or_default(),
            details.business_website.unwrap_or_default(),
        )))
    }

    async fn make_request(&self, body: RequestContent, origin_base_url: String) -> Request {
        RequestBuilder::new()
            .method(Method::Post)
            .url(self.request_url.as_str())
            .set_body(body)
            .attach_default_headers()
            .headers(vec![(
                header::ORIGIN.to_string(),
                format!("{origin_base_url}/dashboard").into(),
            )])
            .build()
    }

    async fn send_request(
        &self,
        proxy: &Proxy,
        request: Request,
    ) -> CustomResult<reqwest::Response, HttpClientError> {
        http_client::send_request(proxy, request, None).await
    }
}


// File: crates/external_services/src/grpc_client.rs

/// Dyanimc Routing Client interface implementation
#[cfg(feature = "dynamic_routing")]
pub mod dynamic_routing;
/// gRPC based Heath Check Client interface implementation
#[cfg(feature = "dynamic_routing")]
pub mod health_check_client;
/// gRPC based Recovery Trainer Client interface implementation
#[cfg(feature = "revenue_recovery")]
pub mod revenue_recovery;

/// gRPC based Unified Connector Service Client interface implementation
pub mod unified_connector_service;
use std::{fmt::Debug, sync::Arc};

#[cfg(feature = "dynamic_routing")]
use common_utils::consts;
use common_utils::{id_type, ucs_types};
#[cfg(feature = "dynamic_routing")]
use dynamic_routing::{DynamicRoutingClientConfig, RoutingStrategy};
#[cfg(feature = "dynamic_routing")]
use health_check_client::HealthCheckClient;
#[cfg(any(feature = "dynamic_routing", feature = "revenue_recovery"))]
use hyper_util::client::legacy::connect::HttpConnector;
#[cfg(any(feature = "dynamic_routing", feature = "revenue_recovery"))]
use router_env::logger;
use router_env::RequestId;
#[cfg(any(feature = "dynamic_routing", feature = "revenue_recovery"))]
use tonic::body::Body;
use typed_builder::TypedBuilder;

#[cfg(feature = "revenue_recovery")]
pub use self::revenue_recovery::{
    recovery_decider_client::{
        DeciderRequest, DeciderResponse, RecoveryDeciderClientConfig,
        RecoveryDeciderClientInterface, RecoveryDeciderError, RecoveryDeciderResult,
    },
    GrpcRecoveryHeaders,
};
use crate::grpc_client::unified_connector_service::{
    UnifiedConnectorServiceClient, UnifiedConnectorServiceClientConfig,
};

#[cfg(any(feature = "dynamic_routing", feature = "revenue_recovery"))]
/// Hyper based Client type for maintaining connection pool for all gRPC services
pub type Client = hyper_util::client::legacy::Client<HttpConnector, Body>;

/// Struct contains all the gRPC Clients
#[derive(Debug, Clone)]
pub struct GrpcClients {
    /// The routing client
    #[cfg(feature = "dynamic_routing")]
    pub dynamic_routing: Option<RoutingStrategy>,
    /// Health Check client for all gRPC services
    #[cfg(feature = "dynamic_routing")]
    pub health_client: HealthCheckClient,
    /// Recovery Decider Client
    #[cfg(feature = "revenue_recovery")]
    pub recovery_decider_client: Option<Box<dyn RecoveryDeciderClientInterface>>,
    /// Unified Connector Service client
    pub unified_connector_service_client: Option<UnifiedConnectorServiceClient>,
}

/// Type that contains the configs required to construct a  gRPC client with its respective services.
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
pub struct GrpcClientSettings {
    #[cfg(feature = "dynamic_routing")]
    /// Configs for Dynamic Routing Client
    pub dynamic_routing_client: Option<DynamicRoutingClientConfig>,
    #[cfg(feature = "revenue_recovery")]
    /// Configs for Recovery Decider Client
    pub recovery_decider_client: Option<RecoveryDeciderClientConfig>,
    /// Configs for Unified Connector Service client
    pub unified_connector_service: Option<UnifiedConnectorServiceClientConfig>,
}

impl GrpcClientSettings {
    /// # Panics
    ///
    /// This function will panic if it fails to establish a connection with the gRPC server.
    /// This function will be called at service startup.
    #[allow(clippy::expect_used)]
    pub async fn get_grpc_client_interface(&self) -> Arc<GrpcClients> {
        #[cfg(any(feature = "dynamic_routing", feature = "revenue_recovery"))]
        let client =
            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
                .http2_only(true)
                .build_http();

        #[cfg(feature = "dynamic_routing")]
        let dynamic_routing_connection = self
            .dynamic_routing_client
            .clone()
            .map(|config| config.get_dynamic_routing_connection(client.clone()))
            .transpose()
            .expect("Failed to establish a connection with the Dynamic Routing Server")
            .flatten();

        #[cfg(feature = "dynamic_routing")]
        let health_client = HealthCheckClient::build_connections(self, client.clone())
            .await
            .expect("Failed to build gRPC connections");

        let unified_connector_service_client =
            UnifiedConnectorServiceClient::build_connections(self).await;

        #[cfg(feature = "revenue_recovery")]
        let recovery_decider_client = {
            match &self.recovery_decider_client {
                Some(config) => {
                    // Validate the config first
                    config
                        .validate()
                        .expect("Recovery Decider configuration validation failed");

                    // Create the client
                    let client = config
                        .get_recovery_decider_connection(client.clone())
                        .expect(
                            "Failed to establish a connection with the Recovery Decider Server",
                        );

                    logger::info!("Recovery Decider gRPC client successfully initialized");
                    let boxed_client: Box<dyn RecoveryDeciderClientInterface> = Box::new(client);
                    Some(boxed_client)
                }
                None => {
                    logger::debug!("Recovery Decider client configuration not provided, client will be disabled");
                    None
                }
            }
        };

        Arc::new(GrpcClients {
            #[cfg(feature = "dynamic_routing")]
            dynamic_routing: dynamic_routing_connection,
            #[cfg(feature = "dynamic_routing")]
            health_client,
            #[cfg(feature = "revenue_recovery")]
            recovery_decider_client,
            unified_connector_service_client,
        })
    }
}

/// Contains grpc headers
#[derive(Debug)]
pub struct GrpcHeaders {
    /// Tenant id
    pub tenant_id: String,
    /// Request id
    pub request_id: Option<String>,
}

/// Contains grpc headers for Ucs
#[derive(Debug, TypedBuilder)]
pub struct GrpcHeadersUcs {
    /// Tenant id
    tenant_id: String,
    /// Request id
    request_id: Option<RequestId>,
    /// Lineage ids
    lineage_ids: LineageIds,
    /// External vault proxy metadata
    external_vault_proxy_metadata: Option<String>,
    /// Merchant Reference Id
    merchant_reference_id: Option<ucs_types::UcsReferenceId>,

    shadow_mode: Option<bool>,
}

/// Type aliase for GrpcHeaders builder in initial stage
pub type GrpcHeadersUcsBuilderInitial =
    GrpcHeadersUcsBuilder<((String,), (Option<RequestId>,), (), (), (), (Option<bool>,))>;
/// Type aliase for GrpcHeaders builder in intermediate stage
pub type GrpcHeadersUcsBuilderFinal = GrpcHeadersUcsBuilder<(
    (String,),
    (Option<RequestId>,),
    (LineageIds,),
    (Option<String>,),
    (Option<ucs_types::UcsReferenceId>,),
    (Option<bool>,),
)>;

/// struct to represent set of Lineage ids
#[derive(Debug, Clone, serde::Serialize)]
pub struct LineageIds {
    merchant_id: id_type::MerchantId,
    profile_id: id_type::ProfileId,
}
impl LineageIds {
    /// constructor for LineageIds
    pub fn new(merchant_id: id_type::MerchantId, profile_id: id_type::ProfileId) -> Self {
        Self {
            merchant_id,
            profile_id,
        }
    }
    /// get url encoded string representation of LineageIds
    pub fn get_url_encoded_string(self) -> Result<String, serde_urlencoded::ser::Error> {
        serde_urlencoded::to_string(&self)
    }
}

#[cfg(feature = "dynamic_routing")]
/// Trait to add necessary headers to the tonic Request
pub(crate) trait AddHeaders {
    /// Add necessary header fields to the tonic Request
    fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders);
}

#[cfg(feature = "dynamic_routing")]
impl<T> AddHeaders for tonic::Request<T> {
    #[track_caller]
    fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders) {
        headers.tenant_id
            .parse()
            .map(|tenant_id| {
                self
                    .metadata_mut()
                    .append(consts::TENANT_HEADER, tenant_id)
            })
            .inspect_err(
                |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::TENANT_HEADER),
            )
            .ok();

        headers.request_id.map(|request_id| {
            request_id
                .parse()
                .map(|request_id| {
                    self
                        .metadata_mut()
                        .append(consts::X_REQUEST_ID, request_id)
                })
                .inspect_err(
                    |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::X_REQUEST_ID),
                )
                .ok();
        });
    }
}

#[cfg(feature = "dynamic_routing")]
pub(crate) fn create_grpc_request<T: Debug>(message: T, headers: GrpcHeaders) -> tonic::Request<T> {
    let mut request = tonic::Request::new(message);
    request.add_headers_to_grpc_request(headers);

    logger::info!(?request);

    request
}


// File: crates/external_services/src/no_encryption.rs

//! No encryption functionalities

pub mod core;

pub mod implementers;


// File: crates/external_services/src/lib.rs

//! Interactions with external systems.

#![warn(missing_docs, missing_debug_implementations)]

#[cfg(feature = "aws_kms")]
pub mod aws_kms;
/// crm module
pub mod crm;
#[cfg(feature = "email")]
pub mod email;
pub mod file_storage;
/// Building grpc clients to communicate with the server
pub mod grpc_client;
#[cfg(feature = "hashicorp-vault")]
pub mod hashicorp_vault;
/// http_client module
pub mod http_client;
/// hubspot_proxy module
pub mod hubspot_proxy;
pub mod managers;
pub mod no_encryption;
#[cfg(feature = "superposition")]
pub mod superposition;
/// deserializers module_path
pub mod utils;

#[cfg(feature = "revenue_recovery")]
/// date_time module
pub mod date_time {
    use error_stack::ResultExt;

    /// Errors in time conversion
    #[derive(Debug, thiserror::Error)]
    pub enum DateTimeConversionError {
        #[error("Invalid timestamp value from prost Timestamp: out of representable range")]
        /// Error for out of range
        TimestampOutOfRange,
    }

    /// Converts a `time::PrimitiveDateTime` to a `prost_types::Timestamp`.
    pub fn convert_to_prost_timestamp(dt: time::PrimitiveDateTime) -> prost_types::Timestamp {
        let odt = dt.assume_utc();
        prost_types::Timestamp {
            seconds: odt.unix_timestamp(),
            // This conversion is safe as nanoseconds (0..999_999_999) always fit within an i32.
            #[allow(clippy::as_conversions)]
            nanos: odt.nanosecond() as i32,
        }
    }

    /// Converts a `prost_types::Timestamp` to an `time::PrimitiveDateTime`.
    pub fn convert_from_prost_timestamp(
        ts: &prost_types::Timestamp,
    ) -> error_stack::Result<time::PrimitiveDateTime, DateTimeConversionError> {
        let timestamp_nanos = i128::from(ts.seconds) * 1_000_000_000 + i128::from(ts.nanos);

        time::OffsetDateTime::from_unix_timestamp_nanos(timestamp_nanos)
            .map(|offset_dt| time::PrimitiveDateTime::new(offset_dt.date(), offset_dt.time()))
            .change_context(DateTimeConversionError::TimestampOutOfRange)
    }
}

/// Crate specific constants
pub mod consts {
    /// General purpose base64 engine
    #[cfg(feature = "aws_kms")]
    pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
        base64::engine::general_purpose::STANDARD;

    /// Header key used to specify the connector name in UCS requests.
    pub(crate) const UCS_HEADER_CONNECTOR: &str = "x-connector";

    /// Header key used to indicate the authentication type being used.
    pub(crate) const UCS_HEADER_AUTH_TYPE: &str = "x-auth";

    /// Header key for sending the API key used for authentication.
    pub(crate) const UCS_HEADER_API_KEY: &str = "x-api-key";

    /// Header key for sending an additional secret key used in some auth types.
    pub(crate) const UCS_HEADER_KEY1: &str = "x-key1";

    /// Header key for sending the API secret in signature-based authentication.
    pub(crate) const UCS_HEADER_API_SECRET: &str = "x-api-secret";

    /// Header key for sending a second additional key used in multi-auth authentication.
    pub(crate) const UCS_HEADER_KEY2: &str = "x-key2";

    /// Header key for sending the AUTH KEY MAP in currency-based authentication.
    pub(crate) const UCS_HEADER_AUTH_KEY_MAP: &str = "x-auth-key-map";

    /// Header key for sending the EXTERNAL VAULT METADATA in proxy payments
    pub(crate) const UCS_HEADER_EXTERNAL_VAULT_METADATA: &str = "x-external-vault-metadata";

    /// Header key for sending the list of lineage ids
    pub(crate) const UCS_LINEAGE_IDS: &str = "x-lineage-ids";

    /// Header key for sending the merchant reference id to UCS
    pub(crate) const UCS_HEADER_REFERENCE_ID: &str = "x-reference-id";
}

/// Metrics for interactions with external systems.
#[cfg(feature = "aws_kms")]
pub mod metrics {
    use router_env::{counter_metric, global_meter, histogram_metric_f64};

    global_meter!(GLOBAL_METER, "EXTERNAL_SERVICES");

    #[cfg(feature = "aws_kms")]
    counter_metric!(AWS_KMS_DECRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Decryption failures
    #[cfg(feature = "aws_kms")]
    counter_metric!(AWS_KMS_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Encryption failures

    #[cfg(feature = "aws_kms")]
    histogram_metric_f64!(AWS_KMS_DECRYPT_TIME, GLOBAL_METER); // Histogram for AWS KMS decryption time (in sec)
    #[cfg(feature = "aws_kms")]
    histogram_metric_f64!(AWS_KMS_ENCRYPT_TIME, GLOBAL_METER); // Histogram for AWS KMS encryption time (in sec)
}