// File: crates/router/src/core/connector_onboarding.rs

use api_models::{connector_onboarding as api, enums};
use masking::Secret;

use crate::{
    core::errors::{ApiErrorResponse, RouterResponse, RouterResult},
    routes::app::ReqState,
    services::{authentication as auth, ApplicationResponse},
    types as oss_types,
    utils::connector_onboarding as utils,
    SessionState,
};

pub mod paypal;

#[async_trait::async_trait]
pub trait AccessToken {
    async fn access_token(state: &SessionState) -> RouterResult<oss_types::AccessToken>;
}

pub async fn get_action_url(
    state: SessionState,
    user_from_token: auth::UserFromToken,
    request: api::ActionUrlRequest,
    _req_state: ReqState,
) -> RouterResponse<api::ActionUrlResponse> {
    utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id)
        .await?;

    let connector_onboarding_conf = state.conf.connector_onboarding.get_inner();
    let is_enabled = utils::is_enabled(request.connector, connector_onboarding_conf);
    let tracking_id =
        utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector)
            .await?;

    match (is_enabled, request.connector) {
        (Some(true), enums::Connector::Paypal) => {
            let action_url = Box::pin(paypal::get_action_url_from_paypal(
                state,
                tracking_id,
                request.return_url,
            ))
            .await?;
            Ok(ApplicationResponse::Json(api::ActionUrlResponse::PayPal(
                api::PayPalActionUrlResponse { action_url },
            )))
        }
        _ => Err(ApiErrorResponse::FlowNotSupported {
            flow: "Connector onboarding".to_string(),
            connector: request.connector.to_string(),
        }
        .into()),
    }
}

pub async fn sync_onboarding_status(
    state: SessionState,
    user_from_token: auth::UserFromToken,
    request: api::OnboardingSyncRequest,
    _req_state: ReqState,
) -> RouterResponse<api::OnboardingStatus> {
    utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id)
        .await?;

    let connector_onboarding_conf = state.conf.connector_onboarding.get_inner();
    let is_enabled = utils::is_enabled(request.connector, connector_onboarding_conf);
    let tracking_id =
        utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector)
            .await?;

    match (is_enabled, request.connector) {
        (Some(true), enums::Connector::Paypal) => {
            let status = Box::pin(paypal::sync_merchant_onboarding_status(
                state.clone(),
                tracking_id,
            ))
            .await?;
            if let api::OnboardingStatus::PayPal(api::PayPalOnboardingStatus::Success(
                ref paypal_onboarding_data,
            )) = status
            {
                let connector_onboarding_conf = state.conf.connector_onboarding.get_inner();
                let auth_details = oss_types::ConnectorAuthType::SignatureKey {
                    api_key: connector_onboarding_conf.paypal.client_secret.clone(),
                    key1: connector_onboarding_conf.paypal.client_id.clone(),
                    api_secret: Secret::new(
                        paypal_onboarding_data.payer_id.get_string_repr().to_owned(),
                    ),
                };
                let update_mca_data = paypal::update_mca(
                    &state,
                    user_from_token.merchant_id,
                    request.connector_id.to_owned(),
                    auth_details,
                )
                .await?;

                return Ok(ApplicationResponse::Json(api::OnboardingStatus::PayPal(
                    api::PayPalOnboardingStatus::ConnectorIntegrated(Box::new(update_mca_data)),
                )));
            }
            Ok(ApplicationResponse::Json(status))
        }
        _ => Err(ApiErrorResponse::FlowNotSupported {
            flow: "Connector onboarding".to_string(),
            connector: request.connector.to_string(),
        }
        .into()),
    }
}

pub async fn reset_tracking_id(
    state: SessionState,
    user_from_token: auth::UserFromToken,
    request: api::ResetTrackingIdRequest,
    _req_state: ReqState,
) -> RouterResponse<()> {
    utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id)
        .await?;
    utils::set_tracking_id_in_configs(&state, &request.connector_id, request.connector).await?;

    Ok(ApplicationResponse::StatusOk)
}


// File: crates/router/src/core/api_locking.rs

use std::fmt::Debug;

use actix_web::rt::time as actix_time;
use error_stack::{report, ResultExt};
use redis_interface::{self as redis, RedisKey};
use router_env::{instrument, logger, tracing};

use super::errors::{self, RouterResult};
use crate::routes::{app::SessionStateInfo, lock_utils};

pub const API_LOCK_PREFIX: &str = "API_LOCK";

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LockStatus {
    // status when the lock is acquired by the caller
    Acquired, // [#2129] pick up request_id from AppState and populate here
    // status when the lock is acquired by some other caller
    Busy,
}

#[derive(Clone, Debug)]
pub enum LockAction {
    // Sleep until the lock is acquired
    Hold { input: LockingInput },
    // Sleep until all locks are acquired
    HoldMultiple { inputs: Vec<LockingInput> },
    // Queue it but return response as 2xx, could be used for webhooks
    QueueWithOk,
    // Return Error
    Drop,
    // Locking Not applicable
    NotApplicable,
}

#[derive(Clone, Debug)]
pub struct LockingInput {
    pub unique_locking_key: String,
    pub api_identifier: lock_utils::ApiIdentifier,
    pub override_lock_retries: Option<u32>,
}

impl LockingInput {
    fn get_redis_locking_key(&self, merchant_id: &common_utils::id_type::MerchantId) -> String {
        format!(
            "{}_{}_{}_{}",
            API_LOCK_PREFIX,
            merchant_id.get_string_repr(),
            self.api_identifier,
            self.unique_locking_key
        )
    }
}

impl LockAction {
    #[instrument(skip_all)]
    pub async fn perform_locking_action<A>(
        self,
        state: &A,
        merchant_id: common_utils::id_type::MerchantId,
    ) -> RouterResult<()>
    where
        A: SessionStateInfo,
    {
        match self {
            Self::HoldMultiple { inputs } => {
                let lock_retries = inputs
                    .iter()
                    .find_map(|input| input.override_lock_retries)
                    .unwrap_or(state.conf().lock_settings.lock_retries);
                let request_id = state.get_request_id();
                let redis_lock_expiry_seconds =
                    state.conf().lock_settings.redis_lock_expiry_seconds;
                let redis_conn = state
                    .store()
                    .get_redis_conn()
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;
                let redis_key_values = inputs
                    .iter()
                    .map(|input| input.get_redis_locking_key(&merchant_id))
                    .map(|key| (RedisKey::from(key.as_str()), request_id.clone()))
                    .collect::<Vec<_>>();
                let delay_between_retries_in_milliseconds = state
                    .conf()
                    .lock_settings
                    .delay_between_retries_in_milliseconds;
                for _retry in 0..lock_retries {
                    let results: Vec<redis::SetGetReply<_>> = redis_conn
                        .set_multiple_keys_if_not_exists_and_get_values(
                            &redis_key_values,
                            Some(i64::from(redis_lock_expiry_seconds)),
                        )
                        .await
                        .change_context(errors::ApiErrorResponse::InternalServerError)?;
                    let lock_acquired = results.iter().all(|res| {
                        // each redis value must match the request_id
                        // if even 1 does match, the lock is not acquired
                        *res.get_value() == request_id
                    });
                    if lock_acquired {
                        logger::info!("Lock acquired for locking inputs {:?}", inputs);
                        return Ok(());
                    } else {
                        actix_time::sleep(tokio::time::Duration::from_millis(u64::from(
                            delay_between_retries_in_milliseconds,
                        )))
                        .await;
                    }
                }
                Err(report!(errors::ApiErrorResponse::ResourceBusy))
            }
            Self::Hold { input } => {
                let redis_conn = state
                    .store()
                    .get_redis_conn()
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;

                let redis_locking_key = input.get_redis_locking_key(&merchant_id);
                let delay_between_retries_in_milliseconds = state
                    .conf()
                    .lock_settings
                    .delay_between_retries_in_milliseconds;
                let redis_lock_expiry_seconds =
                    state.conf().lock_settings.redis_lock_expiry_seconds;
                let lock_retries = input
                    .override_lock_retries
                    .unwrap_or(state.conf().lock_settings.lock_retries);
                for _retry in 0..lock_retries {
                    let redis_lock_result = redis_conn
                        .set_key_if_not_exists_with_expiry(
                            &redis_locking_key.as_str().into(),
                            state.get_request_id(),
                            Some(i64::from(redis_lock_expiry_seconds)),
                        )
                        .await;

                    match redis_lock_result {
                        Ok(redis::SetnxReply::KeySet) => {
                            logger::info!("Lock acquired for locking input {:?}", input);
                            tracing::Span::current()
                                .record("redis_lock_acquired", redis_locking_key);
                            return Ok(());
                        }
                        Ok(redis::SetnxReply::KeyNotSet) => {
                            logger::info!(
                                "Lock busy by other request when tried for locking input {:?}",
                                input
                            );
                            actix_time::sleep(tokio::time::Duration::from_millis(u64::from(
                                delay_between_retries_in_milliseconds,
                            )))
                            .await;
                        }
                        Err(err) => {
                            return Err(err)
                                .change_context(errors::ApiErrorResponse::InternalServerError)
                        }
                    }
                }

                Err(report!(errors::ApiErrorResponse::ResourceBusy))
            }
            Self::QueueWithOk | Self::Drop | Self::NotApplicable => Ok(()),
        }
    }

    #[instrument(skip_all)]
    pub async fn free_lock_action<A>(
        self,
        state: &A,
        merchant_id: common_utils::id_type::MerchantId,
    ) -> RouterResult<()>
    where
        A: SessionStateInfo,
    {
        match self {
            Self::HoldMultiple { inputs } => {
                let redis_conn = state
                    .store()
                    .get_redis_conn()
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;

                let redis_locking_keys = inputs
                    .iter()
                    .map(|input| RedisKey::from(input.get_redis_locking_key(&merchant_id).as_str()))
                    .collect::<Vec<_>>();
                let request_id = state.get_request_id();
                let values = redis_conn
                    .get_multiple_keys::<String>(&redis_locking_keys)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;

                let invalid_request_id_list = values
                    .iter()
                    .filter(|redis_value| **redis_value != request_id)
                    .flatten()
                    .collect::<Vec<_>>();

                if !invalid_request_id_list.is_empty() {
                    logger::error!(
                        "The request_id which acquired the lock is not equal to the request_id requesting for releasing the lock.
                        Current request_id: {:?},
                        Redis request_ids : {:?}",
                        request_id,
                        invalid_request_id_list
                    );
                    Err(errors::ApiErrorResponse::InternalServerError)
                        .attach_printable("The request_id which acquired the lock is not equal to the request_id requesting for releasing the lock")
                } else {
                    Ok(())
                }?;
                let delete_result = redis_conn
                    .delete_multiple_keys(&redis_locking_keys)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;
                let is_key_not_deleted = delete_result
                    .into_iter()
                    .any(|delete_reply| delete_reply.is_key_not_deleted());
                if is_key_not_deleted {
                    Err(errors::ApiErrorResponse::InternalServerError).attach_printable(
                        "Status release lock called but key is not found in redis",
                    )
                } else {
                    logger::info!("Lock freed for locking inputs {:?}", inputs);
                    Ok(())
                }
            }
            Self::Hold { input } => {
                let redis_conn = state
                    .store()
                    .get_redis_conn()
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;

                let redis_locking_key = input.get_redis_locking_key(&merchant_id);

                match redis_conn
                    .get_key::<Option<String>>(&redis_locking_key.as_str().into())
                    .await
                {
                    Ok(val) => {
                        if val == state.get_request_id() {
                            match redis_conn
                                .delete_key(&redis_locking_key.as_str().into())
                                .await
                            {
                                Ok(redis::types::DelReply::KeyDeleted) => {
                                    logger::info!("Lock freed for locking input {:?}", input);
                                    tracing::Span::current()
                                        .record("redis_lock_released", redis_locking_key);
                                    Ok(())
                                }
                                Ok(redis::types::DelReply::KeyNotDeleted) => {
                                    Err(errors::ApiErrorResponse::InternalServerError)
                                        .attach_printable(
                                        "Status release lock called but key is not found in redis",
                                    )
                                }
                                Err(error) => Err(error)
                                    .change_context(errors::ApiErrorResponse::InternalServerError),
                            }
                        } else {
                            Err(errors::ApiErrorResponse::InternalServerError)
                                .attach_printable("The request_id which acquired the lock is not equal to the request_id requesting for releasing the lock")
                        }
                    }
                    Err(error) => {
                        Err(error).change_context(errors::ApiErrorResponse::InternalServerError)
                    }
                }
            }
            Self::QueueWithOk | Self::Drop | Self::NotApplicable => Ok(()),
        }
    }
}

pub trait GetLockingInput {
    fn get_locking_input<F>(&self, flow: F) -> LockAction
    where
        F: router_env::types::FlowMetric,
        lock_utils::ApiIdentifier: From<F>;
}


// File: crates/router/src/core/chat.rs

use api_models::chat as chat_api;
use common_utils::{
    consts,
    crypto::{DecodeMessage, GcmAes256},
    errors::CustomResult,
    request::{Method, RequestBuilder, RequestContent},
};
use error_stack::ResultExt;
use external_services::http_client;
use hyperswitch_domain_models::chat as chat_domain;
use masking::ExposeInterface;
use router_env::{
    instrument, logger,
    tracing::{self, Instrument},
};

use crate::{
    db::errors::chat::ChatErrors,
    routes::{app::SessionStateInfo, SessionState},
    services::{authentication as auth, authorization::roles, ApplicationResponse},
    utils,
};

#[instrument(skip_all, fields(?session_id))]
pub async fn get_data_from_hyperswitch_ai_workflow(
    state: SessionState,
    user_from_token: auth::UserFromToken,
    req: chat_api::ChatRequest,
    session_id: Option<&str>,
) -> CustomResult<ApplicationResponse<chat_api::ChatResponse>, ChatErrors> {
    let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id(
        &state,
        &user_from_token.role_id,
        &user_from_token.org_id,
        user_from_token
            .tenant_id
            .as_ref()
            .unwrap_or(&state.tenant.tenant_id),
    )
    .await
    .change_context(ChatErrors::InternalServerError)
    .attach_printable("Failed to retrieve role information")?;
    let url = format!(
        "{}/webhook",
        state.conf.chat.get_inner().hyperswitch_ai_host
    );
    let request_id = state
        .get_request_id()
        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

    let request_body = chat_domain::HyperswitchAiDataRequest {
        query: chat_domain::GetDataMessage {
            message: req.message.clone(),
        },
        org_id: user_from_token.org_id.clone(),
        merchant_id: user_from_token.merchant_id.clone(),
        profile_id: user_from_token.profile_id.clone(),
        entity_type: role_info.get_entity_type(),
    };
    logger::info!("Request for AI service: {:?}", request_body);

    let mut request_builder = RequestBuilder::new()
        .method(Method::Post)
        .url(&url)
        .attach_default_headers()
        .header(consts::X_REQUEST_ID, &request_id)
        .set_body(RequestContent::Json(Box::new(request_body.clone())));

    if let Some(session_id) = session_id {
        request_builder = request_builder.header(consts::X_CHAT_SESSION_ID, session_id);
    }

    let request = request_builder.build();

    let response = http_client::send_request(
        &state.conf.proxy,
        request,
        Some(consts::REQUEST_TIME_OUT_FOR_AI_SERVICE),
    )
    .await
    .change_context(ChatErrors::InternalServerError)
    .attach_printable("Error when sending request to AI service")?
    .json::<chat_api::ChatResponse>()
    .await
    .change_context(ChatErrors::InternalServerError)
    .attach_printable("Error when deserializing response from AI service")?;

    let response_to_return = response.clone();
    tokio::spawn(
        async move {
            let new_hyperswitch_ai_interaction = utils::chat::construct_hyperswitch_ai_interaction(
                &state,
                &user_from_token,
                &req,
                &response,
                &request_id,
            )
            .await;

            match new_hyperswitch_ai_interaction {
                Ok(interaction) => {
                    let db = state.store.as_ref();
                    if let Err(e) = db.insert_hyperswitch_ai_interaction(interaction).await {
                        logger::error!("Failed to insert hyperswitch_ai_interaction: {:?}", e);
                    }
                }
                Err(e) => {
                    logger::error!("Failed to construct hyperswitch_ai_interaction: {:?}", e);
                }
            }
        }
        .in_current_span(),
    );

    Ok(ApplicationResponse::Json(response_to_return))
}

#[instrument(skip_all)]
pub async fn list_chat_conversations(
    state: SessionState,
    user_from_token: auth::UserFromToken,
    req: chat_api::ChatListRequest,
) -> CustomResult<ApplicationResponse<chat_api::ChatListResponse>, ChatErrors> {
    let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id(
        &state,
        &user_from_token.role_id,
        &user_from_token.org_id,
        user_from_token
            .tenant_id
            .as_ref()
            .unwrap_or(&state.tenant.tenant_id),
    )
    .await
    .change_context(ChatErrors::InternalServerError)
    .attach_printable("Failed to retrieve role information")?;

    if !role_info.is_internal() {
        return Err(error_stack::Report::new(ChatErrors::UnauthorizedAccess)
            .attach_printable("Only internal roles are allowed for this operation"));
    }

    let db = state.store.as_ref();
    let hyperswitch_ai_interactions = db
        .list_hyperswitch_ai_interactions(
            req.merchant_id,
            req.limit.unwrap_or(consts::DEFAULT_LIST_LIMIT),
            req.offset.unwrap_or(consts::DEFAULT_LIST_OFFSET),
        )
        .await
        .change_context(ChatErrors::InternalServerError)
        .attach_printable("Error when fetching hyperswitch_ai_interactions")?;

    let encryption_key = state.conf.chat.get_inner().encryption_key.clone().expose();
    let key = match hex::decode(&encryption_key) {
        Ok(key) => key,
        Err(e) => {
            router_env::logger::error!("Failed to decode encryption key: {}", e);
            encryption_key.as_bytes().to_vec()
        }
    };

    let mut conversations = Vec::new();

    for interaction in hyperswitch_ai_interactions {
        let user_query_encrypted = interaction
            .user_query
            .ok_or(ChatErrors::InternalServerError)
            .attach_printable("Missing user_query field in hyperswitch_ai_interaction")?;
        let response_encrypted = interaction
            .response
            .ok_or(ChatErrors::InternalServerError)
            .attach_printable("Missing response field in hyperswitch_ai_interaction")?;

        let user_query_decrypted_bytes = GcmAes256
            .decode_message(&key, user_query_encrypted.into_inner())
            .change_context(ChatErrors::InternalServerError)
            .attach_printable("Failed to decrypt user query")?;

        let response_decrypted_bytes = GcmAes256
            .decode_message(&key, response_encrypted.into_inner())
            .change_context(ChatErrors::InternalServerError)
            .attach_printable("Failed to decrypt response")?;

        let user_query_decrypted = String::from_utf8(user_query_decrypted_bytes)
            .change_context(ChatErrors::InternalServerError)
            .attach_printable("Failed to convert decrypted user query to string")?;

        let response_decrypted = serde_json::from_slice(&response_decrypted_bytes)
            .change_context(ChatErrors::InternalServerError)
            .attach_printable("Failed to deserialize decrypted response")?;

        conversations.push(chat_api::ChatConversation {
            id: interaction.id,
            session_id: interaction.session_id,
            user_id: interaction.user_id,
            merchant_id: interaction.merchant_id,
            profile_id: interaction.profile_id,
            org_id: interaction.org_id,
            role_id: interaction.role_id,
            user_query: user_query_decrypted.into(),
            response: response_decrypted,
            database_query: interaction.database_query,
            interaction_status: interaction.interaction_status,
            created_at: interaction.created_at,
        });
    }

    return Ok(ApplicationResponse::Json(chat_api::ChatListResponse {
        conversations,
    }));
}


// File: crates/router/src/core/pm_auth.rs

use std::{collections::HashMap, str::FromStr};

use api_models::{
    enums,
    payment_methods::{self, BankAccountAccessCreds},
};
use common_enums::{enums::MerchantStorageScheme, PaymentMethodType};
pub mod helpers;
pub mod transformers;

use common_utils::{
    consts,
    crypto::{HmacSha256, SignMessage},
    ext_traits::{AsyncExt, ValueExt},
    generate_id,
    types::{self as util_types, AmountConvertor},
};
use error_stack::ResultExt;
use helpers::PaymentAuthConnectorDataExt;
use hyperswitch_domain_models::payments::PaymentIntent;
use masking::{ExposeInterface, PeekInterface, Secret};
use pm_auth::{
    connector::plaid::transformers::PlaidAuthType,
    types::{
        self as pm_auth_types,
        api::{
            auth_service::{BankAccountCredentials, ExchangeToken, LinkToken},
            BoxedConnectorIntegration, PaymentAuthConnectorData,
        },
    },
};

use crate::{
    core::{
        errors::{self, ApiErrorResponse, RouterResponse, RouterResult, StorageErrorExt},
        payment_methods::cards,
        payments::helpers as oss_helpers,
        pm_auth::helpers as pm_auth_helpers,
    },
    db::StorageInterface,
    logger,
    routes::SessionState,
    services::{pm_auth as pm_auth_services, ApplicationResponse},
    types::{self, domain, storage, transformers::ForeignTryFrom},
};

#[cfg(feature = "v1")]
pub async fn create_link_token(
    state: SessionState,
    platform: domain::Platform,
    payload: api_models::pm_auth::LinkTokenCreateRequest,
    headers: Option<hyperswitch_domain_models::payments::HeaderPayload>,
) -> RouterResponse<api_models::pm_auth::LinkTokenCreateResponse> {
    let db = &*state.store;

    let redis_conn = db
        .get_redis_conn()
        .change_context(ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get redis connection")?;

    let pm_auth_key = payload.payment_id.get_pm_auth_key();

    redis_conn
        .exists::<Vec<u8>>(&pm_auth_key.as_str().into())
        .await
        .change_context(ApiErrorResponse::InvalidRequestData {
            message: "Incorrect payment_id provided in request".to_string(),
        })
        .attach_printable("Corresponding pm_auth_key does not exist in redis")?
        .then_some(())
        .ok_or(ApiErrorResponse::InvalidRequestData {
            message: "Incorrect payment_id provided in request".to_string(),
        })
        .attach_printable("Corresponding pm_auth_key does not exist in redis")?;

    let pm_auth_configs = redis_conn
        .get_and_deserialize_key::<Vec<api_models::pm_auth::PaymentMethodAuthConnectorChoice>>(
            &pm_auth_key.as_str().into(),
            "Vec<PaymentMethodAuthConnectorChoice>",
        )
        .await
        .change_context(ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get payment method auth choices from redis")?;

    let selected_config = pm_auth_configs
        .into_iter()
        .find(|config| {
            config.payment_method == payload.payment_method
                && config.payment_method_type == payload.payment_method_type
        })
        .ok_or(ApiErrorResponse::GenericNotFoundError {
            message: "payment method auth connector name not found".to_string(),
        })?;

    let connector_name = selected_config.connector_name.as_str();

    let connector = PaymentAuthConnectorData::get_connector_by_name(connector_name)?;
    let connector_integration: BoxedConnectorIntegration<
        '_,
        LinkToken,
        pm_auth_types::LinkTokenRequest,
        pm_auth_types::LinkTokenResponse,
    > = connector.connector.get_connector_integration();

    let payment_intent = oss_helpers::verify_payment_intent_time_and_client_secret(
        &state,
        &platform,
        payload.client_secret,
    )
    .await?;

    let billing_country = payment_intent
        .as_ref()
        .async_map(|pi| async {
            oss_helpers::get_address_by_id(
                &state,
                pi.billing_address_id.clone(),
                platform.get_processor().get_key_store(),
                &pi.payment_id,
                platform.get_processor().get_account().get_id(),
                platform.get_processor().get_account().storage_scheme,
            )
            .await
        })
        .await
        .transpose()?
        .flatten()
        .and_then(|address| address.country)
        .map(|country| country.to_string());

    #[cfg(feature = "v1")]
    let merchant_connector_account = state
        .store
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            platform.get_processor().get_account().get_id(),
            &selected_config.mca_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .change_context(ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: platform
                .get_processor()
                .get_account()
                .get_id()
                .get_string_repr()
                .to_owned(),
        })?;

    #[cfg(feature = "v2")]
    let merchant_connector_account = {
        let _ = billing_country;
        todo!()
    };

    let auth_type = helpers::get_connector_auth_type(merchant_connector_account)?;

    let router_data = pm_auth_types::LinkTokenRouterData {
        flow: std::marker::PhantomData,
        merchant_id: Some(platform.get_processor().get_account().get_id().clone()),
        connector: Some(connector_name.to_string()),
        request: pm_auth_types::LinkTokenRequest {
            client_name: "HyperSwitch".to_string(),
            country_codes: Some(vec![billing_country.ok_or(
                ApiErrorResponse::MissingRequiredField {
                    field_name: "billing_country",
                },
            )?]),
            language: payload.language,
            user_info: payment_intent.and_then(|pi| pi.customer_id),
            client_platform: headers
                .as_ref()
                .and_then(|header| header.x_client_platform.clone()),
            android_package_name: headers.as_ref().and_then(|header| header.x_app_id.clone()),
            redirect_uri: headers
                .as_ref()
                .and_then(|header| header.x_redirect_uri.clone()),
        },
        response: Ok(pm_auth_types::LinkTokenResponse {
            link_token: "".to_string(),
        }),
        connector_http_status_code: None,
        connector_auth_type: auth_type,
    };

    let connector_resp = pm_auth_services::execute_connector_processing_step(
        &state,
        connector_integration,
        &router_data,
        &connector.connector_name,
    )
    .await
    .change_context(ApiErrorResponse::InternalServerError)
    .attach_printable("Failed while calling link token creation connector api")?;

    let link_token_resp =
        connector_resp
            .response
            .map_err(|err| ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: connector.connector_name.to_string(),
                status_code: err.status_code,
                reason: err.reason,
            })?;

    let response = api_models::pm_auth::LinkTokenCreateResponse {
        link_token: link_token_resp.link_token,
        connector: connector.connector_name.to_string(),
    };

    Ok(ApplicationResponse::Json(response))
}

#[cfg(feature = "v2")]
pub async fn create_link_token(
    _state: SessionState,
    _platform: domain::Platform,
    _payload: api_models::pm_auth::LinkTokenCreateRequest,
    _headers: Option<hyperswitch_domain_models::payments::HeaderPayload>,
) -> RouterResponse<api_models::pm_auth::LinkTokenCreateResponse> {
    todo!()
}

impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType {
    type Error = errors::ConnectorError;

    fn foreign_try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
        match auth_type {
            types::ConnectorAuthType::BodyKey { api_key, key1 } => {
                Ok::<Self, errors::ConnectorError>(Self {
                    client_id: api_key.to_owned(),
                    secret: key1.to_owned(),
                })
            }
            _ => Err(errors::ConnectorError::FailedToObtainAuthType),
        }
    }
}

pub async fn exchange_token_core(
    state: SessionState,
    platform: domain::Platform,
    payload: api_models::pm_auth::ExchangeTokenCreateRequest,
) -> RouterResponse<()> {
    let db = &*state.store;

    let config = get_selected_config_from_redis(db, &payload).await?;

    let connector_name = config.connector_name.as_str();

    let connector = PaymentAuthConnectorData::get_connector_by_name(connector_name)?;

    #[cfg(feature = "v1")]
    let merchant_connector_account = state
        .store
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            platform.get_processor().get_account().get_id(),
            &config.mca_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .change_context(ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: platform
                .get_processor()
                .get_account()
                .get_id()
                .get_string_repr()
                .to_owned(),
        })?;

    #[cfg(feature = "v2")]
    let merchant_connector_account: domain::MerchantConnectorAccount = {
        let _ = platform.get_processor().get_account();
        let _ = connector;
        let _ = platform.get_processor().get_key_store();
        todo!()
    };

    let auth_type = helpers::get_connector_auth_type(merchant_connector_account.clone())?;

    let access_token = get_access_token_from_exchange_api(
        &connector,
        connector_name,
        &payload,
        &auth_type,
        &state,
    )
    .await?;

    let bank_account_details_resp = get_bank_account_creds(
        connector,
        &platform,
        connector_name,
        &access_token,
        auth_type,
        &state,
        None,
    )
    .await?;

    Box::pin(store_bank_details_in_payment_methods(
        payload,
        platform,
        state,
        bank_account_details_resp,
        (connector_name, access_token),
        merchant_connector_account.get_id(),
    ))
    .await?;

    Ok(ApplicationResponse::StatusOk)
}

#[cfg(feature = "v1")]
async fn store_bank_details_in_payment_methods(
    payload: api_models::pm_auth::ExchangeTokenCreateRequest,
    platform: domain::Platform,
    state: SessionState,
    bank_account_details_resp: pm_auth_types::BankAccountCredentialsResponse,
    connector_details: (&str, Secret<String>),
    mca_id: common_utils::id_type::MerchantConnectorAccountId,
) -> RouterResult<()> {
    let db = &*state.clone().store;
    let (connector_name, access_token) = connector_details;

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

    let customer_id = payment_intent
        .customer_id
        .ok_or(ApiErrorResponse::CustomerNotFound)?;

    let payment_methods = db
        .find_payment_method_by_customer_id_merchant_id_list(
            platform.get_processor().get_key_store(),
            &customer_id,
            platform.get_processor().get_account().get_id(),
            None,
        )
        .await
        .change_context(ApiErrorResponse::InternalServerError)?;

    let mut hash_to_payment_method: HashMap<
        String,
        (
            domain::PaymentMethod,
            payment_methods::PaymentMethodDataBankCreds,
        ),
    > = HashMap::new();
    let key_manager_state = (&state).into();
    for pm in payment_methods {
        if pm.get_payment_method_type() == Some(enums::PaymentMethod::BankDebit)
            && pm.payment_method_data.is_some()
        {
            let bank_details_pm_data = pm
                .payment_method_data
                .clone()
                .map(|x| x.into_inner().expose())
                .map(|v| v.parse_value("PaymentMethodsData"))
                .transpose()
                .unwrap_or_else(|error| {
                    logger::error!(?error);
                    None
                })
                .and_then(|pmd| match pmd {
                    payment_methods::PaymentMethodsData::BankDetails(bank_creds) => {
                        Some(bank_creds)
                    }
                    _ => None,
                })
                .ok_or(ApiErrorResponse::InternalServerError)
                .attach_printable("Unable to parse PaymentMethodsData")?;

            hash_to_payment_method.insert(
                bank_details_pm_data.hash.clone(),
                (pm, bank_details_pm_data),
            );
        }
    }

    let pm_auth_key = state
        .conf
        .payment_method_auth
        .get_inner()
        .pm_auth_key
        .clone()
        .expose();

    let mut update_entries: Vec<(domain::PaymentMethod, storage::PaymentMethodUpdate)> = Vec::new();
    let mut new_entries: Vec<domain::PaymentMethod> = Vec::new();

    for creds in bank_account_details_resp.credentials {
        let (account_number, hash_string) = match creds.account_details {
            pm_auth_types::PaymentMethodTypeDetails::Ach(ach) => (
                ach.account_number.clone(),
                format!(
                    "{}-{}-{}",
                    ach.account_number.peek(),
                    ach.routing_number.peek(),
                    PaymentMethodType::Ach,
                ),
            ),
            pm_auth_types::PaymentMethodTypeDetails::Bacs(bacs) => (
                bacs.account_number.clone(),
                format!(
                    "{}-{}-{}",
                    bacs.account_number.peek(),
                    bacs.sort_code.peek(),
                    PaymentMethodType::Bacs
                ),
            ),
            pm_auth_types::PaymentMethodTypeDetails::Sepa(sepa) => (
                sepa.iban.clone(),
                format!("{}-{}", sepa.iban.expose(), PaymentMethodType::Sepa),
            ),
        };

        let generated_hash = hex::encode(
            HmacSha256::sign_message(&HmacSha256, pm_auth_key.as_bytes(), hash_string.as_bytes())
                .change_context(ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to sign the message")?,
        );

        let contains_account = hash_to_payment_method.get(&generated_hash);
        let mut pmd = payment_methods::PaymentMethodDataBankCreds {
            mask: account_number
                .peek()
                .chars()
                .rev()
                .take(4)
                .collect::<String>()
                .chars()
                .rev()
                .collect::<String>(),
            hash: generated_hash,
            account_type: creds.account_type,
            account_name: creds.account_name,
            payment_method_type: creds.payment_method_type,
            connector_details: vec![payment_methods::BankAccountConnectorDetails {
                connector: connector_name.to_string(),
                mca_id: mca_id.clone(),
                access_token: BankAccountAccessCreds::AccessToken(access_token.clone()),
                account_id: creds.account_id,
            }],
        };

        if let Some((pm, details)) = contains_account {
            pmd.connector_details.extend(
                details
                    .connector_details
                    .clone()
                    .into_iter()
                    .filter(|conn| conn.mca_id != mca_id),
            );

            let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd);

            let encrypted_data = cards::create_encrypted_data(
                &key_manager_state,
                platform.get_processor().get_key_store(),
                payment_method_data,
            )
            .await
            .change_context(ApiErrorResponse::InternalServerError)
            .attach_printable("Unable to encrypt customer details")?;

            let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
                payment_method_data: Some(encrypted_data.into()),
                last_modified_by: None,
            };

            update_entries.push((pm.clone(), pm_update));
        } else {
            let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd);
            let encrypted_data = cards::create_encrypted_data(
                &key_manager_state,
                platform.get_processor().get_key_store(),
                Some(payment_method_data),
            )
            .await
            .change_context(ApiErrorResponse::InternalServerError)
            .attach_printable("Unable to encrypt customer details")?;

            let pm_id = generate_id(consts::ID_LENGTH, "pm");

            let now = common_utils::date_time::now();

            let pm_new = domain::PaymentMethod {
                customer_id: customer_id.clone(),
                merchant_id: platform.get_processor().get_account().get_id().clone(),
                payment_method_id: pm_id,
                payment_method: Some(enums::PaymentMethod::BankDebit),
                payment_method_type: Some(creds.payment_method_type),
                status: enums::PaymentMethodStatus::Active,
                payment_method_issuer: None,
                scheme: None,
                metadata: None,
                payment_method_data: Some(encrypted_data),
                payment_method_issuer_code: None,
                accepted_currency: None,
                token: None,
                cardholder_name: None,
                issuer_name: None,
                issuer_country: None,
                payer_country: None,
                is_stored: None,
                swift_code: None,
                direct_debit_token: None,
                created_at: now,
                last_modified: now,
                locker_id: None,
                last_used_at: now,
                connector_mandate_details: None,
                customer_acceptance: None,
                network_transaction_id: None,
                client_secret: None,
                payment_method_billing_address: None,
                updated_by: None,
                version: common_types::consts::API_VERSION,
                network_token_requestor_reference_id: None,
                network_token_locker_id: None,
                network_token_payment_method_data: None,
                vault_source_details: Default::default(),
                created_by: None,
                last_modified_by: None,
            };

            new_entries.push(pm_new);
        };
    }

    store_in_db(
        platform.get_processor().get_key_store(),
        update_entries,
        new_entries,
        db,
        platform.get_processor().get_account().storage_scheme,
    )
    .await?;

    Ok(())
}

#[cfg(feature = "v2")]
async fn store_bank_details_in_payment_methods(
    _payload: api_models::pm_auth::ExchangeTokenCreateRequest,
    _platform: domain::Platform,
    _state: SessionState,
    _bank_account_details_resp: pm_auth_types::BankAccountCredentialsResponse,
    _connector_details: (&str, Secret<String>),
    _mca_id: common_utils::id_type::MerchantConnectorAccountId,
) -> RouterResult<()> {
    todo!()
}

async fn store_in_db(
    key_store: &domain::MerchantKeyStore,
    update_entries: Vec<(domain::PaymentMethod, storage::PaymentMethodUpdate)>,
    new_entries: Vec<domain::PaymentMethod>,
    db: &dyn StorageInterface,
    storage_scheme: MerchantStorageScheme,
) -> RouterResult<()> {
    let update_entries_futures = update_entries
        .into_iter()
        .map(|(pm, pm_update)| db.update_payment_method(key_store, pm, pm_update, storage_scheme))
        .collect::<Vec<_>>();

    let new_entries_futures = new_entries
        .into_iter()
        .map(|pm_new| db.insert_payment_method(key_store, pm_new, storage_scheme))
        .collect::<Vec<_>>();

    let update_futures = futures::future::join_all(update_entries_futures);
    let new_futures = futures::future::join_all(new_entries_futures);

    let (update, new) = tokio::join!(update_futures, new_futures);

    let _ = update
        .into_iter()
        .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}")));

    let _ = new
        .into_iter()
        .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}")));

    Ok(())
}

pub async fn get_bank_account_creds(
    connector: PaymentAuthConnectorData,
    platform: &domain::Platform,
    connector_name: &str,
    access_token: &Secret<String>,
    auth_type: pm_auth_types::ConnectorAuthType,
    state: &SessionState,
    bank_account_id: Option<Secret<String>>,
) -> RouterResult<pm_auth_types::BankAccountCredentialsResponse> {
    let connector_integration_bank_details: BoxedConnectorIntegration<
        '_,
        BankAccountCredentials,
        pm_auth_types::BankAccountCredentialsRequest,
        pm_auth_types::BankAccountCredentialsResponse,
    > = connector.connector.get_connector_integration();

    let router_data_bank_details = pm_auth_types::BankDetailsRouterData {
        flow: std::marker::PhantomData,
        merchant_id: Some(platform.get_processor().get_account().get_id().clone()),
        connector: Some(connector_name.to_string()),
        request: pm_auth_types::BankAccountCredentialsRequest {
            access_token: access_token.clone(),
            optional_ids: bank_account_id
                .map(|id| pm_auth_types::BankAccountOptionalIDs { ids: vec![id] }),
        },
        response: Ok(pm_auth_types::BankAccountCredentialsResponse {
            credentials: Vec::new(),
        }),
        connector_http_status_code: None,
        connector_auth_type: auth_type,
    };

    let bank_details_resp = pm_auth_services::execute_connector_processing_step(
        state,
        connector_integration_bank_details,
        &router_data_bank_details,
        &connector.connector_name,
    )
    .await
    .change_context(ApiErrorResponse::InternalServerError)
    .attach_printable("Failed while calling bank account details connector api")?;

    let bank_account_details_resp =
        bank_details_resp
            .response
            .map_err(|err| ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: connector.connector_name.to_string(),
                status_code: err.status_code,
                reason: err.reason,
            })?;

    Ok(bank_account_details_resp)
}

async fn get_access_token_from_exchange_api(
    connector: &PaymentAuthConnectorData,
    connector_name: &str,
    payload: &api_models::pm_auth::ExchangeTokenCreateRequest,
    auth_type: &pm_auth_types::ConnectorAuthType,
    state: &SessionState,
) -> RouterResult<Secret<String>> {
    let connector_integration: BoxedConnectorIntegration<
        '_,
        ExchangeToken,
        pm_auth_types::ExchangeTokenRequest,
        pm_auth_types::ExchangeTokenResponse,
    > = connector.connector.get_connector_integration();

    let router_data = pm_auth_types::ExchangeTokenRouterData {
        flow: std::marker::PhantomData,
        merchant_id: None,
        connector: Some(connector_name.to_string()),
        request: pm_auth_types::ExchangeTokenRequest {
            public_token: payload.public_token.clone(),
        },
        response: Ok(pm_auth_types::ExchangeTokenResponse {
            access_token: "".to_string(),
        }),
        connector_http_status_code: None,
        connector_auth_type: auth_type.clone(),
    };

    let resp = pm_auth_services::execute_connector_processing_step(
        state,
        connector_integration,
        &router_data,
        &connector.connector_name,
    )
    .await
    .change_context(ApiErrorResponse::InternalServerError)
    .attach_printable("Failed while calling exchange token connector api")?;

    let exchange_token_resp =
        resp.response
            .map_err(|err| ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: connector.connector_name.to_string(),
                status_code: err.status_code,
                reason: err.reason,
            })?;

    let access_token = exchange_token_resp.access_token;
    Ok(Secret::new(access_token))
}

async fn get_selected_config_from_redis(
    db: &dyn StorageInterface,
    payload: &api_models::pm_auth::ExchangeTokenCreateRequest,
) -> RouterResult<api_models::pm_auth::PaymentMethodAuthConnectorChoice> {
    let redis_conn = db
        .get_redis_conn()
        .change_context(ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get redis connection")?;

    let pm_auth_key = payload.payment_id.get_pm_auth_key();

    redis_conn
        .exists::<Vec<u8>>(&pm_auth_key.as_str().into())
        .await
        .change_context(ApiErrorResponse::InvalidRequestData {
            message: "Incorrect payment_id provided in request".to_string(),
        })
        .attach_printable("Corresponding pm_auth_key does not exist in redis")?
        .then_some(())
        .ok_or(ApiErrorResponse::InvalidRequestData {
            message: "Incorrect payment_id provided in request".to_string(),
        })
        .attach_printable("Corresponding pm_auth_key does not exist in redis")?;

    let pm_auth_configs = redis_conn
        .get_and_deserialize_key::<Vec<api_models::pm_auth::PaymentMethodAuthConnectorChoice>>(
            &pm_auth_key.as_str().into(),
            "Vec<PaymentMethodAuthConnectorChoice>",
        )
        .await
        .change_context(ApiErrorResponse::GenericNotFoundError {
            message: "payment method auth connector name not found".to_string(),
        })
        .attach_printable("Failed to get payment method auth choices from redis")?;

    let selected_config = pm_auth_configs
        .iter()
        .find(|conf| {
            conf.payment_method == payload.payment_method
                && conf.payment_method_type == payload.payment_method_type
        })
        .ok_or(ApiErrorResponse::GenericNotFoundError {
            message: "payment method auth connector name not found".to_string(),
        })?
        .clone();

    Ok(selected_config)
}

#[cfg(feature = "v2")]
pub async fn retrieve_payment_method_from_auth_service(
    state: &SessionState,
    key_store: &domain::MerchantKeyStore,
    auth_token: &payment_methods::BankAccountTokenData,
    payment_intent: &PaymentIntent,
    _customer: &Option<domain::Customer>,
) -> RouterResult<Option<(domain::PaymentMethodData, enums::PaymentMethod)>> {
    todo!()
}

#[cfg(feature = "v1")]
pub async fn retrieve_payment_method_from_auth_service(
    state: &SessionState,
    key_store: &domain::MerchantKeyStore,
    auth_token: &payment_methods::BankAccountTokenData,
    payment_intent: &PaymentIntent,
    _customer: &Option<domain::Customer>,
) -> RouterResult<Option<(domain::PaymentMethodData, enums::PaymentMethod)>> {
    let db = state.store.as_ref();

    let connector = PaymentAuthConnectorData::get_connector_by_name(
        auth_token.connector_details.connector.as_str(),
    )?;
    let merchant_account = db
        .find_merchant_account_by_merchant_id(&payment_intent.merchant_id, key_store)
        .await
        .to_not_found_response(ApiErrorResponse::MerchantAccountNotFound)?;

    #[cfg(feature = "v1")]
    let mca = db
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            &payment_intent.merchant_id,
            &auth_token.connector_details.mca_id,
            key_store,
        )
        .await
        .to_not_found_response(ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: auth_token
                .connector_details
                .mca_id
                .get_string_repr()
                .to_string()
                .clone(),
        })
        .attach_printable(
            "error while fetching merchant_connector_account from merchant_id and connector name",
        )?;

    let auth_type = pm_auth_helpers::get_connector_auth_type(mca)?;

    let BankAccountAccessCreds::AccessToken(access_token) =
        &auth_token.connector_details.access_token;

    let platform = domain::Platform::new(
        merchant_account.clone(),
        key_store.clone(),
        merchant_account.clone(),
        key_store.clone(),
    );
    let bank_account_creds = get_bank_account_creds(
        connector,
        &platform,
        &auth_token.connector_details.connector,
        access_token,
        auth_type,
        state,
        Some(auth_token.connector_details.account_id.clone()),
    )
    .await?;

    let bank_account = bank_account_creds
        .credentials
        .iter()
        .find(|acc| {
            acc.payment_method_type == auth_token.payment_method_type
                && acc.payment_method == auth_token.payment_method
        })
        .ok_or(ApiErrorResponse::InternalServerError)
        .attach_printable("Bank account details not found")?;

    if let (Some(balance), Some(currency)) = (bank_account.balance, payment_intent.currency) {
        let required_conversion = util_types::FloatMajorUnitForConnector;
        let converted_amount = required_conversion
            .convert_back(balance, currency)
            .change_context(ApiErrorResponse::InternalServerError)
            .attach_printable("Could not convert FloatMajorUnit to MinorUnit")?;

        if converted_amount < payment_intent.amount {
            return Err((ApiErrorResponse::PreconditionFailed {
                message: "selected bank account has insufficient balance".to_string(),
            })
            .into());
        }
    }

    let mut bank_type = None;
    if let Some(account_type) = bank_account.account_type.clone() {
        bank_type = common_enums::BankType::from_str(account_type.as_str())
            .map_err(|error| logger::error!(%error,"unable to parse account_type {account_type:?}"))
            .ok();
    }

    let payment_method_data = match &bank_account.account_details {
        pm_auth_types::PaymentMethodTypeDetails::Ach(ach) => {
            domain::PaymentMethodData::BankDebit(domain::BankDebitData::AchBankDebit {
                account_number: ach.account_number.clone(),
                routing_number: ach.routing_number.clone(),
                bank_name: None,
                bank_type,
                bank_holder_type: None,
                card_holder_name: None,
                bank_account_holder_name: None,
            })
        }
        pm_auth_types::PaymentMethodTypeDetails::Bacs(bacs) => {
            domain::PaymentMethodData::BankDebit(domain::BankDebitData::BacsBankDebit {
                account_number: bacs.account_number.clone(),
                sort_code: bacs.sort_code.clone(),
                bank_account_holder_name: None,
            })
        }
        pm_auth_types::PaymentMethodTypeDetails::Sepa(sepa) => {
            domain::PaymentMethodData::BankDebit(domain::BankDebitData::SepaBankDebit {
                iban: sepa.iban.clone(),
                bank_account_holder_name: None,
            })
        }
    };

    Ok(Some((payment_method_data, enums::PaymentMethod::BankDebit)))
}