// File: crates/router/src/core/authentication/utils.rs

use common_utils::ext_traits::AsyncExt;
use error_stack::ResultExt;
use hyperswitch_domain_models::router_data_v2::ExternalAuthenticationFlowData;
use masking::ExposeInterface;

use crate::{
    consts,
    core::{
        errors::{self, ConnectorErrorExt, StorageErrorExt},
        payments,
    },
    errors::RouterResult,
    routes::SessionState,
    services::{self, execute_connector_processing_step},
    types::{
        api, authentication::AuthenticationResponseData, domain, storage,
        transformers::ForeignFrom, RouterData,
    },
    utils::OptionExt,
};

#[cfg(feature = "v1")]
pub fn get_connector_data_if_separate_authn_supported(
    connector_call_type: &api::ConnectorCallType,
) -> Option<api::ConnectorData> {
    match connector_call_type {
        api::ConnectorCallType::PreDetermined(connector_routing_data) => {
            if connector_routing_data
                .connector_data
                .connector_name
                .is_separate_authentication_supported()
            {
                Some(connector_routing_data.connector_data.clone())
            } else {
                None
            }
        }
        api::ConnectorCallType::Retryable(connector_routing_data) => connector_routing_data
            .first()
            .and_then(|connector_routing_data| {
                if connector_routing_data
                    .connector_data
                    .connector_name
                    .is_separate_authentication_supported()
                {
                    Some(connector_routing_data.connector_data.clone())
                } else {
                    None
                }
            }),
        api::ConnectorCallType::SessionMultiple(_) => None,
    }
}

pub async fn update_trackers<F: Clone, Req>(
    state: &SessionState,
    router_data: RouterData<F, Req, AuthenticationResponseData>,
    authentication: storage::Authentication,
    acquirer_details: Option<super::types::AcquirerDetails>,
    merchant_key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore,
) -> RouterResult<storage::Authentication> {
    let authentication_update = match router_data.response {
        Ok(response) => match response {
            AuthenticationResponseData::PreAuthNResponse {
                threeds_server_transaction_id,
                maximum_supported_3ds_version,
                connector_authentication_id,
                three_ds_method_data,
                three_ds_method_url,
                message_version,
                connector_metadata,
                directory_server_id,
            } => storage::AuthenticationUpdate::PreAuthenticationUpdate {
                threeds_server_transaction_id,
                maximum_supported_3ds_version,
                connector_authentication_id,
                three_ds_method_data,
                three_ds_method_url,
                message_version,
                connector_metadata,
                authentication_status: common_enums::AuthenticationStatus::Pending,
                acquirer_bin: acquirer_details
                    .as_ref()
                    .map(|acquirer_details| acquirer_details.acquirer_bin.clone()),
                acquirer_merchant_id: acquirer_details
                    .as_ref()
                    .map(|acquirer_details| acquirer_details.acquirer_merchant_id.clone()),
                acquirer_country_code: acquirer_details
                    .and_then(|acquirer_details| acquirer_details.acquirer_country_code),
                directory_server_id,
                billing_address: None,
                shipping_address: None,
                browser_info: Box::new(None),
                email: None,
            },
            AuthenticationResponseData::AuthNResponse {
                authn_flow_type,
                authentication_value,
                trans_status,
                connector_metadata,
                ds_trans_id,
                eci,
                challenge_code,
                challenge_cancel,
                challenge_code_reason,
                message_extension,
            } => {
                authentication_value
                    .async_map(|auth_val| {
                        crate::core::payment_methods::vault::create_tokenize(
                            state,
                            auth_val.expose(),
                            None,
                            authentication
                                .authentication_id
                                .get_string_repr()
                                .to_string(),
                            merchant_key_store.key.get_inner(),
                        )
                    })
                    .await
                    .transpose()?;

                let authentication_status =
                    common_enums::AuthenticationStatus::foreign_from(trans_status.clone());

                storage::AuthenticationUpdate::AuthenticationUpdate {
                    trans_status,
                    acs_url: authn_flow_type.get_acs_url(),
                    challenge_request: authn_flow_type.get_challenge_request(),
                    challenge_request_key: authn_flow_type.get_challenge_request_key(),
                    acs_reference_number: authn_flow_type.get_acs_reference_number(),
                    acs_trans_id: authn_flow_type.get_acs_trans_id(),
                    acs_signed_content: authn_flow_type.get_acs_signed_content(),
                    authentication_type: authn_flow_type.get_decoupled_authentication_type(),
                    authentication_status,
                    connector_metadata,
                    ds_trans_id,
                    eci,
                    challenge_code,
                    challenge_cancel,
                    challenge_code_reason,
                    message_extension,
                }
            }
            AuthenticationResponseData::PostAuthNResponse {
                trans_status,
                authentication_value,
                eci,
                challenge_cancel,
                challenge_code_reason,
            } => {
                authentication_value
                    .async_map(|auth_val| {
                        crate::core::payment_methods::vault::create_tokenize(
                            state,
                            auth_val.expose(),
                            None,
                            authentication
                                .authentication_id
                                .get_string_repr()
                                .to_string(),
                            merchant_key_store.key.get_inner(),
                        )
                    })
                    .await
                    .transpose()?;
                storage::AuthenticationUpdate::PostAuthenticationUpdate {
                    authentication_status: common_enums::AuthenticationStatus::foreign_from(
                        trans_status.clone(),
                    ),
                    trans_status,
                    eci,
                    challenge_cancel,
                    challenge_code_reason,
                }
            }
            AuthenticationResponseData::PreAuthVersionCallResponse {
                maximum_supported_3ds_version,
            } => storage::AuthenticationUpdate::PreAuthenticationVersionCallUpdate {
                message_version: maximum_supported_3ds_version.clone(),
                maximum_supported_3ds_version,
            },
            AuthenticationResponseData::PreAuthThreeDsMethodCallResponse {
                threeds_server_transaction_id,
                three_ds_method_data,
                three_ds_method_url,
                connector_metadata,
            } => storage::AuthenticationUpdate::PreAuthenticationThreeDsMethodCall {
                threeds_server_transaction_id,
                three_ds_method_data,
                three_ds_method_url,
                connector_metadata,
                acquirer_bin: acquirer_details
                    .as_ref()
                    .map(|acquirer_details| acquirer_details.acquirer_bin.clone()),
                acquirer_merchant_id: acquirer_details
                    .map(|acquirer_details| acquirer_details.acquirer_merchant_id),
            },
        },
        Err(error) => storage::AuthenticationUpdate::ErrorUpdate {
            connector_authentication_id: error.connector_transaction_id,
            authentication_status: common_enums::AuthenticationStatus::Failed,
            error_message: error
                .reason
                .map(|reason| format!("message: {}, reason: {}", error.message, reason))
                .or(Some(error.message)),
            error_code: Some(error.code),
        },
    };
    state
        .store
        .update_authentication_by_merchant_id_authentication_id(
            authentication,
            authentication_update,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error while updating authentication")
}

impl ForeignFrom<common_enums::AuthenticationStatus> for common_enums::AttemptStatus {
    fn foreign_from(from: common_enums::AuthenticationStatus) -> Self {
        match from {
            common_enums::AuthenticationStatus::Started
            | common_enums::AuthenticationStatus::Pending => Self::AuthenticationPending,
            common_enums::AuthenticationStatus::Success => Self::AuthenticationSuccessful,
            common_enums::AuthenticationStatus::Failed => Self::AuthenticationFailed,
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub async fn create_new_authentication(
    state: &SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    authentication_connector: String,
    token: String,
    profile_id: common_utils::id_type::ProfileId,
    payment_id: common_utils::id_type::PaymentId,
    merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
    organization_id: common_utils::id_type::OrganizationId,
    force_3ds_challenge: Option<bool>,
    psd2_sca_exemption_type: Option<common_enums::ScaExemptionType>,
) -> RouterResult<storage::Authentication> {
    let authentication_id = common_utils::id_type::AuthenticationId::generate_authentication_id(
        consts::AUTHENTICATION_ID_PREFIX,
    );
    let authentication_client_secret = Some(common_utils::generate_id_with_default_len(&format!(
        "{}_secret",
        authentication_id.get_string_repr()
    )));
    let new_authorization = storage::AuthenticationNew {
        authentication_id: authentication_id.clone(),
        merchant_id,
        authentication_connector: Some(authentication_connector),
        connector_authentication_id: None,
        payment_method_id: format!("eph_{token}"),
        authentication_type: None,
        authentication_status: common_enums::AuthenticationStatus::Started,
        authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus::Unused,
        error_message: None,
        error_code: None,
        connector_metadata: None,
        maximum_supported_version: None,
        threeds_server_transaction_id: None,
        cavv: None,
        authentication_flow_type: None,
        message_version: None,
        eci: None,
        trans_status: None,
        acquirer_bin: None,
        acquirer_merchant_id: None,
        three_ds_method_data: None,
        three_ds_method_url: None,
        acs_url: None,
        challenge_request: None,
        challenge_request_key: None,
        acs_reference_number: None,
        acs_trans_id: None,
        acs_signed_content: None,
        profile_id,
        payment_id: Some(payment_id),
        merchant_connector_id: Some(merchant_connector_id),
        ds_trans_id: None,
        directory_server_id: None,
        acquirer_country_code: None,
        service_details: None,
        organization_id,
        authentication_client_secret,
        force_3ds_challenge,
        psd2_sca_exemption_type,
        return_url: None,
        amount: None,
        currency: None,
        billing_address: None,
        shipping_address: None,
        browser_info: None,
        email: None,
        profile_acquirer_id: None,
        challenge_code: None,
        challenge_cancel: None,
        challenge_code_reason: None,
        message_extension: None,
        customer_details: None,
    };
    state
        .store
        .insert_authentication(new_authorization)
        .await
        .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError {
            message: format!(
                "Authentication with authentication_id {} already exists",
                authentication_id.get_string_repr()
            ),
        })
}

pub async fn do_auth_connector_call<F, Req, Res>(
    state: &SessionState,
    authentication_connector_name: String,
    router_data: RouterData<F, Req, Res>,
) -> RouterResult<RouterData<F, Req, Res>>
where
    Req: std::fmt::Debug + Clone + 'static,
    Res: std::fmt::Debug + Clone + 'static,
    F: std::fmt::Debug + Clone + 'static,
    dyn api::Connector + Sync: services::api::ConnectorIntegration<F, Req, Res>,
    dyn api::ConnectorV2 + Sync:
        services::api::ConnectorIntegrationV2<F, ExternalAuthenticationFlowData, Req, Res>,
{
    let connector_data =
        api::AuthenticationConnectorData::get_connector_by_name(&authentication_connector_name)?;
    let connector_integration: services::BoxedExternalAuthenticationConnectorIntegrationInterface<
        F,
        Req,
        Res,
    > = connector_data.connector.get_connector_integration();
    let router_data = execute_connector_processing_step(
        state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_payment_failed_response()?;
    Ok(router_data)
}

pub async fn get_authentication_connector_data(
    state: &SessionState,
    key_store: &domain::MerchantKeyStore,
    business_profile: &domain::Profile,
    authentication_connector: Option<String>,
) -> RouterResult<(
    common_enums::AuthenticationConnectors,
    payments::helpers::MerchantConnectorAccountType,
)> {
    let authentication_connector = if let Some(authentication_connector) = authentication_connector
    {
        api_models::enums::convert_authentication_connector(&authentication_connector).ok_or(
            errors::ApiErrorResponse::UnprocessableEntity {
                message: format!(
                "Invalid authentication_connector found in request : {authentication_connector}",
            ),
            },
        )?
    } else {
        let authentication_details = business_profile
            .authentication_connector_details
            .clone()
            .get_required_value("authentication_details")
            .change_context(errors::ApiErrorResponse::UnprocessableEntity {
                message: "authentication_connector_details is not available in business profile"
                    .into(),
            })
            .attach_printable("authentication_connector_details not configured by the merchant")?;

        authentication_details
            .authentication_connectors
            .first()
            .ok_or(errors::ApiErrorResponse::UnprocessableEntity {
                message: format!(
                    "No authentication_connector found for profile_id {:?}",
                    business_profile.get_id()
                ),
            })
            .attach_printable(
                "No authentication_connector found from merchant_account.authentication_details",
            )?
            .to_owned()
    };

    let profile_id = business_profile.get_id();
    let authentication_connector_mca = payments::helpers::get_merchant_connector_account(
        state,
        &business_profile.merchant_id,
        None,
        key_store,
        profile_id,
        authentication_connector.to_string().as_str(),
        None,
    )
    .await?;
    Ok((authentication_connector, authentication_connector_mca))
}


// File: crates/router/src/core/payouts/transformers.rs

use std::collections::HashMap;

use common_utils::link_utils::EnabledPaymentMethod;

#[cfg(all(feature = "v1", feature = "olap"))]
use crate::types::transformers::ForeignInto;
#[cfg(feature = "olap")]
use crate::types::{api::payments, domain, storage};
use crate::{
    settings::PayoutRequiredFields,
    types::{api, transformers::ForeignFrom},
};

#[cfg(all(feature = "v2", feature = "olap"))]
impl
    ForeignFrom<(
        storage::Payouts,
        storage::PayoutAttempt,
        Option<domain::Customer>,
        Option<payments::Address>,
    )> for api::PayoutCreateResponse
{
    fn foreign_from(
        item: (
            storage::Payouts,
            storage::PayoutAttempt,
            Option<domain::Customer>,
            Option<payments::Address>,
        ),
    ) -> Self {
        todo!()
    }
}

#[cfg(all(feature = "v1", feature = "olap"))]
impl
    ForeignFrom<(
        storage::Payouts,
        storage::PayoutAttempt,
        Option<domain::Customer>,
        Option<payments::Address>,
    )> for api::PayoutCreateResponse
{
    fn foreign_from(
        item: (
            storage::Payouts,
            storage::PayoutAttempt,
            Option<domain::Customer>,
            Option<payments::Address>,
        ),
    ) -> Self {
        let (payout, payout_attempt, customer, address) = item;
        let attempt = api::PayoutAttemptResponse {
            attempt_id: payout_attempt.payout_attempt_id,
            status: payout_attempt.status,
            amount: payout.amount,
            currency: Some(payout.destination_currency),
            connector: payout_attempt.connector.clone(),
            error_code: payout_attempt.error_code.clone(),
            error_message: payout_attempt.error_message.clone(),
            payment_method: payout.payout_type,
            payout_method_type: None,
            connector_transaction_id: payout_attempt.connector_payout_id,
            cancellation_reason: None,
            unified_code: None,
            unified_message: None,
        };
        Self {
            payout_id: payout.payout_id,
            merchant_id: payout.merchant_id,
            merchant_connector_id: payout_attempt.merchant_connector_id,
            merchant_order_reference_id: payout_attempt.merchant_order_reference_id.clone(),
            amount: payout.amount,
            currency: payout.destination_currency,
            connector: payout_attempt.connector,
            payout_type: payout.payout_type,
            auto_fulfill: payout.auto_fulfill,
            customer_id: customer.as_ref().map(|cust| cust.customer_id.clone()),
            customer: customer.as_ref().map(|cust| cust.foreign_into()),
            return_url: payout.return_url,
            business_country: payout_attempt.business_country,
            business_label: payout_attempt.business_label,
            description: payout.description,
            entity_type: payout.entity_type,
            recurring: payout.recurring,
            metadata: payout.metadata,
            status: payout_attempt.status,
            error_message: payout_attempt.error_message,
            error_code: payout_attempt.error_code,
            profile_id: payout.profile_id,
            created: Some(payout.created_at),
            connector_transaction_id: attempt.connector_transaction_id.clone(),
            priority: payout.priority,
            billing: address,
            payout_method_data: payout_attempt.additional_payout_method_data.map(From::from),
            client_secret: None,
            payout_link: None,
            unified_code: attempt.unified_code.clone(),
            unified_message: attempt.unified_message.clone(),
            attempts: Some(vec![attempt]),
            email: customer
                .as_ref()
                .and_then(|customer| customer.email.clone()),
            name: customer.as_ref().and_then(|customer| customer.name.clone()),
            phone: customer
                .as_ref()
                .and_then(|customer| customer.phone.clone()),
            phone_country_code: customer
                .as_ref()
                .and_then(|customer| customer.phone_country_code.clone()),
            payout_method_id: payout.payout_method_id,
        }
    }
}

#[cfg(feature = "v1")]
impl
    ForeignFrom<(
        &PayoutRequiredFields,
        Vec<EnabledPaymentMethod>,
        api::RequiredFieldsOverrideRequest,
    )> for Vec<api::PayoutEnabledPaymentMethodsInfo>
{
    fn foreign_from(
        (payout_required_fields, enabled_payout_methods, value_overrides): (
            &PayoutRequiredFields,
            Vec<EnabledPaymentMethod>,
            api::RequiredFieldsOverrideRequest,
        ),
    ) -> Self {
        let value_overrides = value_overrides.flat_struct();

        enabled_payout_methods
            .into_iter()
            .map(|enabled_payout_method| {
                let payment_method = enabled_payout_method.payment_method;
                let payment_method_types_info = enabled_payout_method
                    .payment_method_types
                    .into_iter()
                    .filter_map(|pmt| {
                        payout_required_fields
                            .0
                            .get(&payment_method)
                            .and_then(|pmt_info| {
                                pmt_info.0.get(&pmt).map(|connector_fields| {
                                    let mut required_fields = HashMap::new();

                                    for required_field_final in connector_fields.fields.values() {
                                        required_fields.extend(required_field_final.common.clone());
                                    }

                                    for (key, value) in &value_overrides {
                                        required_fields.entry(key.to_string()).and_modify(
                                            |required_field| {
                                                required_field.value =
                                                    Some(masking::Secret::new(value.to_string()));
                                            },
                                        );
                                    }
                                    api::PaymentMethodTypeInfo {
                                        payment_method_type: pmt,
                                        required_fields: if required_fields.is_empty() {
                                            None
                                        } else {
                                            Some(required_fields)
                                        },
                                    }
                                })
                            })
                            .or(Some(api::PaymentMethodTypeInfo {
                                payment_method_type: pmt,
                                required_fields: None,
                            }))
                    })
                    .collect();

                api::PayoutEnabledPaymentMethodsInfo {
                    payment_method,
                    payment_method_types_info,
                }
            })
            .collect()
    }
}


// File: crates/router/src/core/payouts/validator.rs

use std::collections::HashSet;

use actix_web::http::header;
#[cfg(feature = "olap")]
use common_utils::errors::CustomResult;
use common_utils::{
    id_type::{self, GenerateId},
    validation::validate_domain_against_allowed_domains,
};
use diesel_models::generic_link::PayoutLink;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::payment_methods::PaymentMethod;
use router_env::{instrument, tracing, which as router_env_which, Env};
use url::Url;

use super::helpers;
#[cfg(feature = "v1")]
use crate::core::payment_methods::cards::get_pm_list_context;
use crate::{
    core::{
        errors::{self, RouterResult},
        utils as core_utils,
    },
    db::StorageInterface,
    errors::StorageError,
    routes::SessionState,
    types::{api::payouts, domain, storage},
    utils,
    utils::OptionExt,
};

#[instrument(skip(db))]
pub async fn validate_uniqueness_of_payout_id_against_merchant_id(
    db: &dyn StorageInterface,
    payout_id: &id_type::PayoutId,
    merchant_id: &id_type::MerchantId,
    storage_scheme: storage::enums::MerchantStorageScheme,
) -> RouterResult<Option<storage::Payouts>> {
    let maybe_payouts = db
        .find_optional_payout_by_merchant_id_payout_id(merchant_id, payout_id, storage_scheme)
        .await;
    match maybe_payouts {
        Err(err) => {
            let storage_err = err.current_context();
            match storage_err {
                StorageError::ValueNotFound(_) => Ok(None),
                _ => Err(err
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed while finding payout_attempt, database error")),
            }
        }
        Ok(payout) => Ok(payout),
    }
}

#[cfg(feature = "v2")]
pub async fn validate_create_request(
    _state: &SessionState,
    _platform: &domain::Platform,
    _req: &payouts::PayoutCreateRequest,
) -> RouterResult<(
    String,
    Option<payouts::PayoutMethodData>,
    String,
    Option<domain::Customer>,
    Option<PaymentMethod>,
)> {
    todo!()
}

/// Validates the request on below checks
/// - merchant_id passed is same as the one in merchant_account table
/// - payout_id is unique against merchant_id
/// - payout_token provided is legitimate
#[cfg(feature = "v1")]
pub async fn validate_create_request(
    state: &SessionState,
    platform: &domain::Platform,
    req: &payouts::PayoutCreateRequest,
) -> RouterResult<(
    id_type::PayoutId,
    Option<payouts::PayoutMethodData>,
    id_type::ProfileId,
    Option<domain::Customer>,
    Option<PaymentMethod>,
)> {
    if req.payout_method_id.is_some() && req.confirm != Some(true) {
        return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
            message: "Confirm must be true for recurring payouts".to_string(),
        }));
    }
    let merchant_id = platform.get_processor().get_account().get_id();

    if let Some(payout_link) = &req.payout_link {
        if *payout_link {
            validate_payout_link_request(req)?;
        }
    };

    // Merchant ID
    let predicate = req.merchant_id.as_ref().map(|mid| mid != merchant_id);
    utils::when(predicate.unwrap_or(false), || {
        Err(report!(errors::ApiErrorResponse::InvalidDataFormat {
            field_name: "merchant_id".to_string(),
            expected_format: "merchant_id from merchant account".to_string(),
        })
        .attach_printable("invalid merchant_id in request"))
    })?;

    // Payout ID
    let db: &dyn StorageInterface = &*state.store;
    let payout_id = match req.payout_id.as_ref() {
        Some(provided_payout_id) => provided_payout_id.clone(),
        None => id_type::PayoutId::generate(),
    };

    match validate_uniqueness_of_payout_id_against_merchant_id(
        db,
        &payout_id,
        merchant_id,
        platform.get_processor().get_account().storage_scheme,
    )
    .await
    .attach_printable_lazy(|| {
        format!(
            "Unique violation while checking payout_id: {payout_id:?} against merchant_id: {merchant_id:?}"
        )
    })? {
        Some(_) => Err(report!(errors::ApiErrorResponse::DuplicatePayout {
            payout_id: payout_id.clone()
        })),
        None => Ok(()),
    }?;

    // Fetch customer details (merge of loose fields + customer object) and create DB entry
    let customer_in_request = helpers::get_customer_details_from_request(req);
    let customer = if customer_in_request.customer_id.is_some()
        || customer_in_request.name.is_some()
        || customer_in_request.email.is_some()
        || customer_in_request.phone.is_some()
        || customer_in_request.phone_country_code.is_some()
    {
        helpers::get_or_create_customer_details(state, &customer_in_request, platform).await?
    } else {
        None
    };

    #[cfg(feature = "v1")]
    let profile_id = core_utils::get_profile_id_from_business_details(
        req.business_country,
        req.business_label.as_ref(),
        platform,
        req.profile_id.as_ref(),
        &*state.store,
        false,
    )
    .await?;

    #[cfg(feature = "v2")]
    // Profile id will be mandatory in v2 in the request / headers
    let profile_id = req
        .profile_id
        .clone()
        .ok_or(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "profile_id",
        })
        .attach_printable("Profile id is a mandatory parameter")?;

    let payment_method: Option<PaymentMethod> =
        match (req.payout_token.as_ref(), req.payout_method_id.clone()) {
            (Some(_), Some(_)) => Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                message: "Only one of payout_method_id or payout_token should be provided."
                    .to_string(),
            })),
            (None, Some(payment_method_id)) => match customer.as_ref() {
                Some(customer) => {
                    let payment_method = db
                        .find_payment_method(
                            platform.get_processor().get_key_store(),
                            &payment_method_id,
                            platform.get_processor().get_account().storage_scheme,
                        )
                        .await
                        .change_context(errors::ApiErrorResponse::PaymentMethodNotFound)
                        .attach_printable("Unable to find payment method")?;

                    utils::when(payment_method.customer_id != customer.customer_id, || {
                        Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                        message: "Payment method does not belong to this customer_id".to_string(),
                    })
                    .attach_printable(
                        "customer_id in payment_method does not match with customer_id in request",
                    ))
                    })?;
                    Ok(Some(payment_method))
                }
                None => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
                    field_name: "customer_id when payment_method_id is passed",
                })),
            },
            _ => Ok(None),
        }?;

    // payout_token
    let payout_method_data = match (
        req.payout_token.as_ref(),
        customer.as_ref(),
        payment_method.as_ref(),
    ) {
        (Some(_), None, _) => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "customer or customer_id when payout_token is provided"
        })),
        (Some(payout_token), Some(customer), _) => {
            helpers::make_payout_method_data(
                state,
                req.payout_method_data.as_ref(),
                Some(payout_token),
                &customer.customer_id,
                platform.get_processor().get_account().get_id(),
                req.payout_type,
                platform.get_processor().get_key_store(),
                None,
                platform.get_processor().get_account().storage_scheme,
            )
            .await
        }
        (_, Some(_), Some(payment_method)) => {
            // Check if we have a stored transfer_method_id first
            if payment_method
                .get_common_mandate_reference()
                .ok()
                .and_then(|common_mandate_ref| common_mandate_ref.payouts)
                .map(|payouts_mandate_ref| !payouts_mandate_ref.0.is_empty())
                .unwrap_or(false)
            {
                Ok(None)
            } else {
                // No transfer_method_id available, proceed with vault fetch for raw card details
                match get_pm_list_context(
                    state,
                    payment_method
                        .payment_method
                        .as_ref()
                        .get_required_value("payment_method_id")?,
                    platform.get_processor().get_key_store(),
                    payment_method,
                    None,
                    false,
                    true,
                    platform,
                )
                .await?
                {
                    Some(pm) => match (pm.card_details, pm.bank_transfer_details) {
                        (Some(card), _) => Ok(Some(payouts::PayoutMethodData::Card(
                            api_models::payouts::CardPayout {
                                card_number: card.card_number.get_required_value("card_number")?,
                                card_holder_name: card.card_holder_name,
                                expiry_month: card
                                    .expiry_month
                                    .get_required_value("expiry_month")?,
                                expiry_year: card.expiry_year.get_required_value("expiry_year")?,
                                card_network: card.card_network.clone(),
                            },
                        ))),
                        (_, Some(bank)) => Ok(Some(payouts::PayoutMethodData::Bank(bank))),
                        _ => Ok(None),
                    },
                    None => Ok(None),
                }
            }
        }
        _ => Ok(None),
    }?;

    Ok((
        payout_id,
        payout_method_data,
        profile_id,
        customer,
        payment_method,
    ))
}

pub fn validate_payout_link_request(
    req: &payouts::PayoutCreateRequest,
) -> Result<(), errors::ApiErrorResponse> {
    if req.confirm.unwrap_or(false) {
        return Err(errors::ApiErrorResponse::InvalidRequestData {
            message: "cannot confirm a payout while creating a payout link".to_string(),
        });
    }

    if req.customer_id.is_none() {
        return Err(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "customer or customer_id when payout_link is true",
        });
    }

    Ok(())
}

#[cfg(feature = "olap")]
pub(super) fn validate_payout_list_request(
    req: &payouts::PayoutListConstraints,
) -> CustomResult<(), errors::ApiErrorResponse> {
    use common_utils::consts::PAYOUTS_LIST_MAX_LIMIT_GET;

    utils::when(
        req.limit > PAYOUTS_LIST_MAX_LIMIT_GET || req.limit < 1,
        || {
            Err(errors::ApiErrorResponse::InvalidRequestData {
                message: format!("limit should be in between 1 and {PAYOUTS_LIST_MAX_LIMIT_GET}"),
            })
        },
    )?;
    Ok(())
}

#[cfg(feature = "olap")]
pub(super) fn validate_payout_list_request_for_joins(
    limit: u32,
) -> CustomResult<(), errors::ApiErrorResponse> {
    use common_utils::consts::PAYOUTS_LIST_MAX_LIMIT_POST;

    utils::when(!(1..=PAYOUTS_LIST_MAX_LIMIT_POST).contains(&limit), || {
        Err(errors::ApiErrorResponse::InvalidRequestData {
            message: format!("limit should be in between 1 and {PAYOUTS_LIST_MAX_LIMIT_POST}"),
        })
    })?;
    Ok(())
}

pub fn validate_payout_link_render_request_and_get_allowed_domains(
    request_headers: &header::HeaderMap,
    payout_link: &PayoutLink,
) -> RouterResult<HashSet<String>> {
    let link_id = payout_link.link_id.to_owned();
    let link_data = payout_link.link_data.to_owned();

    let is_test_mode_enabled = link_data.test_mode.unwrap_or(false);

    match (router_env_which(), is_test_mode_enabled) {
        // Throw error in case test_mode was enabled in production
        (Env::Production, true) => Err(report!(errors::ApiErrorResponse::LinkConfigurationError {
            message: "test_mode cannot be true for rendering payout_links in production"
                .to_string()
        })),
        // Skip all validations when test mode is enabled in non prod env
        (_, true) => Ok(HashSet::new()),
        // Otherwise, perform validations
        (_, false) => {
            // Fetch destination is "iframe"
            match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) {
                Some("iframe") => Ok(()),
                Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden {
                    resource: "payout_link".to_string(),
                }))
                .attach_printable_lazy(|| {
                    format!(
                        "Access to payout_link [{link_id}] is forbidden when requested through {requestor}",

                    )
                }),
                None => Err(report!(errors::ApiErrorResponse::AccessForbidden {
                    resource: "payout_link".to_string(),
                }))
                .attach_printable_lazy(|| {
                    format!(
                        "Access to payout_link [{link_id}] is forbidden when sec-fetch-dest is not present in request headers",

                    )
                }),
            }?;

            // Validate origin / referer
            let domain_in_req = {
                let origin_or_referer = request_headers
                    .get("origin")
                    .or_else(|| request_headers.get("referer"))
                    .and_then(|v| v.to_str().ok())
                    .ok_or_else(|| {
                        report!(errors::ApiErrorResponse::AccessForbidden {
                            resource: "payout_link".to_string(),
                        })
                    })
                    .attach_printable_lazy(|| {
                        format!(
                            "Access to payout_link [{link_id}] is forbidden when origin or referer is not present in request headers",

                        )
                    })?;

                let url = Url::parse(origin_or_referer)
                    .map_err(|_| {
                        report!(errors::ApiErrorResponse::AccessForbidden {
                            resource: "payout_link".to_string(),
                        })
                    })
                    .attach_printable_lazy(|| {
                        format!("Invalid URL found in request headers {origin_or_referer}")
                    })?;

                url.host_str()
                    .and_then(|host| url.port().map(|port| format!("{host}:{port}")))
                    .or_else(|| url.host_str().map(String::from))
                    .ok_or_else(|| {
                        report!(errors::ApiErrorResponse::AccessForbidden {
                            resource: "payout_link".to_string(),
                        })
                    })
                    .attach_printable_lazy(|| {
                        format!("host or port not found in request headers {url:?}")
                    })?
            };

            if validate_domain_against_allowed_domains(
                &domain_in_req,
                link_data.allowed_domains.clone(),
            ) {
                Ok(link_data.allowed_domains)
            } else {
                Err(report!(errors::ApiErrorResponse::AccessForbidden {
                    resource: "payout_link".to_string(),
                }))
                .attach_printable_lazy(|| {
                    format!(
                        "Access to payout_link [{link_id}] is forbidden from requestor - {domain_in_req}",

                    )
                })
            }
        }
    }
}


// File: crates/router/src/core/payouts/access_token.rs

use common_utils::ext_traits::AsyncExt;
use error_stack::ResultExt;
use hyperswitch_interfaces::api::{ConnectorAccessTokenSuffix, ConnectorCommon};

use crate::{
    consts,
    core::{
        errors::{self, RouterResult},
        payments,
    },
    routes::{metrics, SessionState},
    services,
    types::{self, api as api_types, domain, storage::enums},
};

/// After we get the access token, check if there was an error and if the flow should proceed further
/// Everything is well, continue with the flow
/// There was an error, cannot proceed further
#[cfg(feature = "payouts")]
pub async fn create_access_token<F: Clone + 'static>(
    state: &SessionState,
    connector_data: &api_types::ConnectorData,
    platform: &domain::Platform,
    router_data: &mut types::PayoutsRouterData<F>,
    payout_type: Option<enums::PayoutType>,
) -> RouterResult<()> {
    let connector_access_token =
        add_access_token_for_payout(state, connector_data, platform, router_data, payout_type)
            .await?;

    if connector_access_token.connector_supports_access_token {
        match connector_access_token.access_token_result {
            Ok(access_token) => {
                router_data.access_token = access_token;
            }
            Err(connector_error) => {
                router_data.response = Err(connector_error);
            }
        }
    }

    Ok(())
}

#[cfg(feature = "payouts")]
pub async fn add_access_token_for_payout<F: Clone + 'static>(
    state: &SessionState,
    connector: &api_types::ConnectorData,
    platform: &domain::Platform,
    router_data: &types::PayoutsRouterData<F>,
    payout_type: Option<enums::PayoutType>,
) -> RouterResult<types::AddAccessTokenResult> {
    if connector
        .connector_name
        .supports_access_token_for_payout(payout_type)
    {
        let store = &*state.store;

        let key = connector
            .connector
            .get_access_token_key(router_data, connector.connector.id().to_string())
            .change_context(errors::ApiErrorResponse::InternalServerError)?;

        let old_access_token = store
            .get_access_token(key.clone())
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("DB error when accessing the access token")?;

        let res = match old_access_token {
            Some(access_token) => Ok(Some(access_token)),
            None => {
                let cloned_router_data = router_data.clone();
                let refresh_token_request_data = types::AccessTokenRequestData::try_from(
                    router_data.connector_auth_type.clone(),
                )
                .attach_printable(
                    "Could not create access token request, invalid connector account credentials",
                )?;

                let refresh_token_response_data: Result<types::AccessToken, types::ErrorResponse> =
                    Err(types::ErrorResponse::default());
                let refresh_token_router_data = payments::helpers::router_data_type_conversion::<
                    _,
                    api_types::AccessTokenAuth,
                    _,
                    _,
                    _,
                    _,
                >(
                    cloned_router_data,
                    refresh_token_request_data,
                    refresh_token_response_data,
                );
                refresh_connector_auth(state, connector, platform, &refresh_token_router_data)
                    .await?
                    .async_map(|access_token| async {
                        //Store the access token in db
                        let store = &*state.store;
                        // This error should not be propagated, we don't want payments to fail once we have
                        // the access token, the next request will create new access token
                        let _ = store
                            .set_access_token(key, access_token.clone())
                            .await
                            .change_context(errors::ApiErrorResponse::InternalServerError)
                            .attach_printable("DB error when setting the access token");
                        Some(access_token)
                    })
                    .await
            }
        };

        Ok(types::AddAccessTokenResult {
            access_token_result: res,
            connector_supports_access_token: true,
        })
    } else {
        Ok(types::AddAccessTokenResult {
            access_token_result: Err(types::ErrorResponse::default()),
            connector_supports_access_token: false,
        })
    }
}

#[cfg(feature = "payouts")]
pub async fn refresh_connector_auth(
    state: &SessionState,
    connector: &api_types::ConnectorData,
    _platform: &domain::Platform,
    router_data: &types::RouterData<
        api_types::AccessTokenAuth,
        types::AccessTokenRequestData,
        types::AccessToken,
    >,
) -> RouterResult<Result<types::AccessToken, types::ErrorResponse>> {
    let connector_integration: services::BoxedAccessTokenConnectorIntegrationInterface<
        api_types::AccessTokenAuth,
        types::AccessTokenRequestData,
        types::AccessToken,
    > = connector.connector.get_connector_integration();

    let access_token_router_data_result = services::execute_connector_processing_step(
        state,
        connector_integration,
        router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await;

    let access_token_router_data = match access_token_router_data_result {
        Ok(router_data) => Ok(router_data.response),
        Err(connector_error) => {
            // If we receive a timeout error from the connector, then
            // the error has to be handled gracefully by updating the payment status to failed.
            // further payment flow will not be continued
            if connector_error.current_context().is_connector_timeout() {
                let error_response = types::ErrorResponse {
                    code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(),
                    message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(),
                    reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()),
                    status_code: 504,
                    attempt_status: None,
                    connector_transaction_id: None,
                    network_advice_code: None,
                    network_decline_code: None,
                    network_error_message: None,
                    connector_metadata: None,
                };

                Ok(Err(error_response))
            } else {
                Err(connector_error
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Could not refresh access token"))
            }
        }
    }?;

    metrics::ACCESS_TOKEN_CREATION.add(
        1,
        router_env::metric_attributes!(("connector", connector.connector_name.to_string())),
    );
    Ok(access_token_router_data)
}