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

pub mod validator;
use actix_web::http::header;
use api_models::{
    admin::PaymentLinkConfig,
    payments::{PaymentLinkData, PaymentLinkStatusWrap},
};
use common_utils::{
    consts::{DEFAULT_LOCALE, DEFAULT_SESSION_EXPIRY},
    ext_traits::{OptionExt, ValueExt},
    types::{AmountConvertor, StringMajorUnitForCore},
};
use error_stack::{report, ResultExt};
use futures::future;
use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData};
use masking::{PeekInterface, Secret};
use router_env::logger;
use time::PrimitiveDateTime;

use super::{
    errors::{self, RouterResult, StorageErrorExt},
    payments::helpers,
};
use crate::{
    consts::{
        self, DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY,
        DEFAULT_ENABLE_BUTTON_ONLY_ON_FORM_READY, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
        DEFAULT_HIDE_CARD_NICKNAME_FIELD, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG,
        DEFAULT_SDK_LAYOUT, DEFAULT_SHOW_CARD_FORM,
    },
    errors::RouterResponse,
    get_payment_link_config_value, get_payment_link_config_value_based_on_priority,
    routes::SessionState,
    services,
    types::{
        api::payment_link::PaymentLinkResponseExt,
        domain,
        storage::{enums as storage_enums, payment_link::PaymentLink},
        transformers::{ForeignFrom, ForeignInto},
    },
};

pub async fn retrieve_payment_link(
    state: SessionState,
    payment_link_id: String,
) -> RouterResponse<api_models::payments::RetrievePaymentLinkResponse> {
    let db = &*state.store;
    let payment_link_config = db
        .find_payment_link_by_payment_link_id(&payment_link_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;

    let session_expiry = payment_link_config.fulfilment_time.unwrap_or_else(|| {
        common_utils::date_time::now()
            .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY))
    });

    let status = check_payment_link_status(session_expiry);

    let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from((
        payment_link_config,
        status,
    ));
    Ok(services::ApplicationResponse::Json(response))
}

#[cfg(feature = "v2")]
pub async fn form_payment_link_data(
    state: &SessionState,
    platform: domain::Platform,
    merchant_id: common_utils::id_type::MerchantId,
    payment_id: common_utils::id_type::PaymentId,
) -> RouterResult<(PaymentLink, PaymentLinkData, PaymentLinkConfig)> {
    todo!()
}

#[cfg(feature = "v1")]
pub async fn form_payment_link_data(
    state: &SessionState,
    platform: domain::Platform,
    merchant_id: common_utils::id_type::MerchantId,
    payment_id: common_utils::id_type::PaymentId,
) -> RouterResult<(PaymentLink, PaymentLinkData, PaymentLinkConfig)> {
    let db = &*state.store;

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

    let payment_link_id = payment_intent
        .payment_link_id
        .get_required_value("payment_link_id")
        .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?;

    let merchant_name_from_merchant_account = platform
        .get_processor()
        .get_account()
        .merchant_name
        .clone()
        .map(|merchant_name| merchant_name.into_inner().peek().to_owned())
        .unwrap_or_default();

    let payment_link = db
        .find_payment_link_by_payment_link_id(&payment_link_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;

    let payment_link_config =
        if let Some(pl_config_value) = payment_link.payment_link_config.clone() {
            extract_payment_link_config(pl_config_value)?
        } else {
            PaymentLinkConfig {
                theme: DEFAULT_BACKGROUND_COLOR.to_string(),
                logo: DEFAULT_MERCHANT_LOGO.to_string(),
                seller_name: merchant_name_from_merchant_account,
                sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
                display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY,
                enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
                hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD,
                show_card_form_by_default: DEFAULT_SHOW_CARD_FORM,
                allowed_domains: DEFAULT_ALLOWED_DOMAINS,
                transaction_details: None,
                background_image: None,
                details_layout: None,
                branding_visibility: None,
                payment_button_text: None,
                custom_message_for_card_terms: None,
                custom_message_for_payment_method_types: None,
                payment_button_colour: None,
                skip_status_screen: None,
                background_colour: None,
                payment_button_text_colour: None,
                sdk_ui_rules: None,
                payment_link_ui_rules: None,
                enable_button_only_on_form_ready: DEFAULT_ENABLE_BUTTON_ONLY_ON_FORM_READY,
                payment_form_header_text: None,
                payment_form_label_type: None,
                show_card_terms: None,
                is_setup_mandate_flow: None,
                color_icon_card_cvc_error: None,
            }
        };

    let profile_id = payment_link
        .profile_id
        .clone()
        .or(payment_intent.profile_id)
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Profile id missing in payment link and payment intent")?;

    let business_profile = db
        .find_business_profile_by_profile_id(platform.get_processor().get_key_store(), &profile_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
            id: profile_id.get_string_repr().to_owned(),
        })?;

    let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() {
        payment_create_return_url
    } else {
        business_profile
            .return_url
            .ok_or(errors::ApiErrorResponse::MissingRequiredField {
                field_name: "return_url",
            })?
    };

    let (currency, client_secret) = validate_sdk_requirements(
        payment_intent.currency,
        payment_intent.client_secret.clone(),
    )?;

    let required_conversion_type = StringMajorUnitForCore;

    let amount = required_conversion_type
        .convert(payment_intent.amount, currency)
        .change_context(errors::ApiErrorResponse::AmountConversionFailed {
            amount_type: "StringMajorUnit",
        })?;

    let order_details = validate_order_details(payment_intent.order_details.clone(), currency)?;

    let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| {
        payment_intent
            .created_at
            .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY))
    });

    // converting first letter of merchant name to upperCase
    let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
    let payment_link_status = check_payment_link_status(session_expiry);

    let is_payment_link_terminal_state = check_payment_link_invalid_conditions(
        payment_intent.status,
        &[
            storage_enums::IntentStatus::Cancelled,
            storage_enums::IntentStatus::Failed,
            storage_enums::IntentStatus::Processing,
            storage_enums::IntentStatus::RequiresCapture,
            storage_enums::IntentStatus::RequiresMerchantAction,
            storage_enums::IntentStatus::Succeeded,
            storage_enums::IntentStatus::PartiallyCaptured,
            storage_enums::IntentStatus::RequiresCustomerAction,
        ],
    );

    let attempt_id = payment_intent.active_attempt.get_id().clone();
    let payment_attempt = db
        .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
            &payment_intent.payment_id,
            &merchant_id,
            &attempt_id.clone(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
    if is_payment_link_terminal_state
        || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
    {
        let status = match payment_link_status {
            api_models::payments::PaymentLinkStatus::Active => {
                logger::info!("displaying status page as the requested payment link has reached terminal state with payment status as {:?}", payment_intent.status);
                PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
            }
            api_models::payments::PaymentLinkStatus::Expired => {
                if is_payment_link_terminal_state {
                    logger::info!("displaying status page as the requested payment link has reached terminal state with payment status as {:?}", payment_intent.status);
                    PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
                } else {
                    logger::info!(
                        "displaying status page as the requested payment link has expired"
                    );
                    PaymentLinkStatusWrap::PaymentLinkStatus(
                        api_models::payments::PaymentLinkStatus::Expired,
                    )
                }
            }
        };

        let attempt_id = payment_intent.active_attempt.get_id().clone();
        let payment_attempt = db
            .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
                &payment_intent.payment_id,
                &merchant_id,
                &attempt_id.clone(),
                platform.get_processor().get_account().storage_scheme,
            )
            .await
            .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
        let payment_details = api_models::payments::PaymentLinkStatusDetails {
            amount,
            currency,
            payment_id: payment_intent.payment_id,
            merchant_name,
            merchant_logo: payment_link_config.logo.clone(),
            created: payment_link.created_at,
            status,
            error_code: payment_attempt.error_code,
            error_message: payment_attempt.error_message,
            redirect: false,
            theme: payment_link_config.theme.clone(),
            return_url: return_url.clone(),
            locale: Some(state.clone().locale),
            transaction_details: payment_link_config.transaction_details.clone(),
            unified_code: payment_attempt.unified_code,
            unified_message: payment_attempt.unified_message,
            capture_method: payment_attempt.capture_method,
            setup_future_usage_applied: payment_attempt.setup_future_usage_applied,
        };

        return Ok((
            payment_link,
            PaymentLinkData::PaymentLinkStatusDetails(Box::new(payment_details)),
            payment_link_config,
        ));
    };

    let payment_link_details = api_models::payments::PaymentLinkDetails {
        amount,
        currency,
        payment_id: payment_intent.payment_id,
        merchant_name,
        order_details,
        return_url,
        session_expiry,
        pub_key: platform
            .get_processor()
            .get_account()
            .publishable_key
            .to_owned(),
        client_secret,
        merchant_logo: payment_link_config.logo.clone(),
        max_items_visible_after_collapse: 3,
        theme: payment_link_config.theme.clone(),
        merchant_description: payment_intent.description,
        sdk_layout: payment_link_config.sdk_layout.clone(),
        display_sdk_only: payment_link_config.display_sdk_only,
        hide_card_nickname_field: payment_link_config.hide_card_nickname_field,
        show_card_form_by_default: payment_link_config.show_card_form_by_default,
        locale: Some(state.clone().locale),
        transaction_details: payment_link_config.transaction_details.clone(),
        background_image: payment_link_config.background_image.clone(),
        details_layout: payment_link_config.details_layout,
        branding_visibility: payment_link_config.branding_visibility,
        payment_button_text: payment_link_config.payment_button_text.clone(),
        custom_message_for_card_terms: payment_link_config.custom_message_for_card_terms.clone(),
        custom_message_for_payment_method_types: payment_link_config
            .custom_message_for_payment_method_types
            .clone(),
        payment_button_colour: payment_link_config.payment_button_colour.clone(),
        skip_status_screen: payment_link_config.skip_status_screen,
        background_colour: payment_link_config.background_colour.clone(),
        payment_button_text_colour: payment_link_config.payment_button_text_colour.clone(),
        sdk_ui_rules: payment_link_config.sdk_ui_rules.clone(),
        status: payment_intent.status,
        enable_button_only_on_form_ready: payment_link_config.enable_button_only_on_form_ready,
        payment_form_header_text: payment_link_config.payment_form_header_text.clone(),
        payment_form_label_type: payment_link_config.payment_form_label_type,
        show_card_terms: payment_link_config.show_card_terms,
        is_setup_mandate_flow: payment_link_config.is_setup_mandate_flow,
        color_icon_card_cvc_error: payment_link_config.color_icon_card_cvc_error.clone(),
        capture_method: payment_attempt.capture_method,
        setup_future_usage_applied: payment_attempt.setup_future_usage_applied,
    };

    Ok((
        payment_link,
        PaymentLinkData::PaymentLinkDetails(Box::new(payment_link_details)),
        payment_link_config,
    ))
}

pub async fn initiate_secure_payment_link_flow(
    state: SessionState,
    platform: domain::Platform,
    merchant_id: common_utils::id_type::MerchantId,
    payment_id: common_utils::id_type::PaymentId,
    request_headers: &header::HeaderMap,
) -> RouterResponse<services::PaymentLinkFormData> {
    let (payment_link, payment_link_details, payment_link_config) =
        form_payment_link_data(&state, platform, merchant_id, payment_id).await?;

    validator::validate_secure_payment_link_render_request(
        request_headers,
        &payment_link,
        &payment_link_config,
    )?;

    let css_script = get_payment_link_css_script(&payment_link_config)?;

    match payment_link_details {
        PaymentLinkData::PaymentLinkStatusDetails(ref status_details) => {
            let js_script = get_js_script(&payment_link_details)?;
            let payment_link_error_data = services::PaymentLinkStatusData {
                js_script,
                css_script,
            };
            logger::info!(
                "payment link data, for building payment link status page {:?}",
                status_details
            );
            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
                services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
            )))
        }
        PaymentLinkData::PaymentLinkDetails(link_details) => {
            let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails {
                enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method,
                hide_card_nickname_field: payment_link_config.hide_card_nickname_field,
                show_card_form_by_default: payment_link_config.show_card_form_by_default,
                payment_link_details: *link_details.to_owned(),
                payment_button_text: payment_link_config.payment_button_text,
                custom_message_for_card_terms: payment_link_config.custom_message_for_card_terms,
                custom_message_for_payment_method_types: payment_link_config
                    .custom_message_for_payment_method_types,
                payment_button_colour: payment_link_config.payment_button_colour,
                skip_status_screen: payment_link_config.skip_status_screen,
                background_colour: payment_link_config.background_colour,
                payment_button_text_colour: payment_link_config.payment_button_text_colour,
                sdk_ui_rules: payment_link_config.sdk_ui_rules,
                enable_button_only_on_form_ready: payment_link_config
                    .enable_button_only_on_form_ready,
                payment_form_header_text: payment_link_config.payment_form_header_text,
                payment_form_label_type: payment_link_config.payment_form_label_type,
                show_card_terms: payment_link_config.show_card_terms,
                color_icon_card_cvc_error: payment_link_config.color_icon_card_cvc_error,
            };
            let payment_details_str = serde_json::to_string(&secure_payment_link_details)
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to serialize PaymentLinkData")?;
            let url_encoded_str = urlencoding::encode(&payment_details_str);
            let js_script = format!("window.__PAYMENT_DETAILS = '{url_encoded_str}';");
            let html_meta_tags = get_meta_tags_html(&link_details);
            let payment_link_data = services::PaymentLinkFormData {
                js_script,
                sdk_url: state.conf.payment_link.sdk_url.clone(),
                css_script,
                html_meta_tags,
            };
            let allowed_domains = payment_link_config
                .allowed_domains
                .clone()
                .ok_or(report!(errors::ApiErrorResponse::InternalServerError))
                .attach_printable_lazy(|| {
                    format!(
                        "Invalid list of allowed_domains found - {:?}",
                        payment_link_config.allowed_domains.clone()
                    )
                })?;

            if allowed_domains.is_empty() {
                return Err(report!(errors::ApiErrorResponse::InternalServerError))
                    .attach_printable_lazy(|| {
                        format!(
                            "Invalid list of allowed_domains found - {:?}",
                            payment_link_config.allowed_domains.clone()
                        )
                    });
            }

            let link_data = GenericLinks {
                allowed_domains,
                data: GenericLinksData::SecurePaymentLink(payment_link_data),
                locale: DEFAULT_LOCALE.to_string(),
            };
            logger::info!(
                "payment link data, for building secure payment link {:?}",
                link_data
            );

            Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
                link_data,
            )))
        }
    }
}

pub async fn initiate_payment_link_flow(
    state: SessionState,
    platform: domain::Platform,
    merchant_id: common_utils::id_type::MerchantId,
    payment_id: common_utils::id_type::PaymentId,
) -> RouterResponse<services::PaymentLinkFormData> {
    let (_, payment_details, payment_link_config) =
        form_payment_link_data(&state, platform, merchant_id, payment_id).await?;

    let css_script = get_payment_link_css_script(&payment_link_config)?;
    let js_script = get_js_script(&payment_details)?;

    match payment_details {
        PaymentLinkData::PaymentLinkStatusDetails(status_details) => {
            let payment_link_error_data = services::PaymentLinkStatusData {
                js_script,
                css_script,
            };
            logger::info!(
                "payment link data, for building payment link status page {:?}",
                status_details
            );
            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
                services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
            )))
        }
        PaymentLinkData::PaymentLinkDetails(payment_details) => {
            let html_meta_tags = get_meta_tags_html(&payment_details);
            let payment_link_data = services::PaymentLinkFormData {
                js_script,
                sdk_url: state.conf.payment_link.sdk_url.clone(),
                css_script,
                html_meta_tags,
            };
            logger::info!(
                "payment link data, for building open payment link {:?}",
                payment_link_data
            );
            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
                services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
            )))
        }
    }
}

pub fn get_js_script(payment_details: &PaymentLinkData) -> RouterResult<String> {
    payment_link::get_js_script(payment_details)
        .change_context(errors::ApiErrorResponse::InternalServerError)
}

pub fn get_payment_link_css_script(
    payment_link_config: &PaymentLinkConfig,
) -> RouterResult<String> {
    payment_link::get_css_script(payment_link_config)
        .change_context(errors::ApiErrorResponse::InternalServerError)
}

pub fn get_meta_tags_html(payment_details: &api_models::payments::PaymentLinkDetails) -> String {
    payment_link::get_meta_tags_html(payment_details)
}

fn validate_sdk_requirements(
    currency: Option<api_models::enums::Currency>,
    client_secret: Option<String>,
) -> Result<(api_models::enums::Currency, String), errors::ApiErrorResponse> {
    let currency = currency.ok_or(errors::ApiErrorResponse::MissingRequiredField {
        field_name: "currency",
    })?;

    let client_secret = client_secret.ok_or(errors::ApiErrorResponse::MissingRequiredField {
        field_name: "client_secret",
    })?;
    Ok((currency, client_secret))
}

pub async fn list_payment_link(
    state: SessionState,
    merchant: domain::MerchantAccount,
    constraints: api_models::payments::PaymentLinkListConstraints,
) -> RouterResponse<Vec<api_models::payments::RetrievePaymentLinkResponse>> {
    let db = state.store.as_ref();
    let payment_link = db
        .list_payment_link_by_merchant_id(merchant.get_id(), constraints)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to retrieve payment link")?;
    let payment_link_list = future::try_join_all(payment_link.into_iter().map(|payment_link| {
        api_models::payments::RetrievePaymentLinkResponse::from_db_payment_link(payment_link)
    }))
    .await?;
    Ok(services::ApplicationResponse::Json(payment_link_list))
}

pub fn check_payment_link_status(
    payment_link_expiry: PrimitiveDateTime,
) -> api_models::payments::PaymentLinkStatus {
    let curr_time = common_utils::date_time::now();

    if curr_time > payment_link_expiry {
        api_models::payments::PaymentLinkStatus::Expired
    } else {
        api_models::payments::PaymentLinkStatus::Active
    }
}

fn validate_order_details(
    order_details: Option<Vec<Secret<serde_json::Value>>>,
    currency: api_models::enums::Currency,
) -> Result<
    Option<Vec<api_models::payments::OrderDetailsWithStringAmount>>,
    error_stack::Report<errors::ApiErrorResponse>,
> {
    let required_conversion_type = StringMajorUnitForCore;
    let order_details = order_details
        .map(|order_details| {
            order_details
                .iter()
                .map(|data| {
                    data.to_owned()
                        .parse_value("OrderDetailsWithAmount")
                        .change_context(errors::ApiErrorResponse::InvalidDataValue {
                            field_name: "OrderDetailsWithAmount",
                        })
                        .attach_printable("Unable to parse OrderDetailsWithAmount")
                })
                .collect::<Result<Vec<api_models::payments::OrderDetailsWithAmount>, _>>()
        })
        .transpose()?;

    let updated_order_details = match order_details {
        Some(mut order_details) => {
            let mut order_details_amount_string_array: Vec<
                api_models::payments::OrderDetailsWithStringAmount,
            > = Vec::new();
            for order in order_details.iter_mut() {
                let mut order_details_amount_string : api_models::payments::OrderDetailsWithStringAmount = Default::default();
                if order.product_img_link.is_none() {
                    order_details_amount_string.product_img_link =
                        Some(DEFAULT_PRODUCT_IMG.to_string())
                } else {
                    order_details_amount_string
                        .product_img_link
                        .clone_from(&order.product_img_link)
                };
                order_details_amount_string.amount = required_conversion_type
                    .convert(order.amount, currency)
                    .change_context(errors::ApiErrorResponse::AmountConversionFailed {
                        amount_type: "StringMajorUnit",
                    })?;
                order_details_amount_string.product_name =
                    capitalize_first_char(&order.product_name.clone());
                order_details_amount_string.quantity = order.quantity;
                order_details_amount_string_array.push(order_details_amount_string)
            }
            Some(order_details_amount_string_array)
        }
        None => None,
    };
    Ok(updated_order_details)
}

pub fn extract_payment_link_config(
    pl_config: serde_json::Value,
) -> Result<PaymentLinkConfig, error_stack::Report<errors::ApiErrorResponse>> {
    serde_json::from_value::<PaymentLinkConfig>(pl_config).change_context(
        errors::ApiErrorResponse::InvalidDataValue {
            field_name: "payment_link_config",
        },
    )
}

pub fn get_payment_link_config_based_on_priority(
    payment_create_link_config: Option<api_models::payments::PaymentCreatePaymentLinkConfig>,
    business_link_config: Option<diesel_models::business_profile::BusinessPaymentLinkConfig>,
    merchant_name: String,
    default_domain_name: String,
    payment_link_config_id: Option<String>,
) -> Result<(PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>> {
    let (domain_name, business_theme_configs, allowed_domains, branding_visibility) =
        if let Some(business_config) = business_link_config {
            (
                business_config
                    .domain_name
                    .clone()
                    .map(|d_name| {
                        logger::info!("domain name set to custom domain https://{:?}", d_name);
                        format!("https://{d_name}")
                    })
                    .unwrap_or_else(|| default_domain_name.clone()),
                payment_link_config_id
                    .and_then(|id| {
                        business_config
                            .business_specific_configs
                            .as_ref()
                            .and_then(|specific_configs| specific_configs.get(&id).cloned())
                    })
                    .or(business_config.default_config),
                business_config.allowed_domains,
                business_config.branding_visibility,
            )
        } else {
            (default_domain_name, None, None, None)
        };

    let (
        theme,
        logo,
        seller_name,
        sdk_layout,
        display_sdk_only,
        enabled_saved_payment_method,
        hide_card_nickname_field,
        show_card_form_by_default,
        enable_button_only_on_form_ready,
    ) = get_payment_link_config_value!(
        payment_create_link_config,
        business_theme_configs,
        (theme, DEFAULT_BACKGROUND_COLOR.to_string()),
        (logo, DEFAULT_MERCHANT_LOGO.to_string()),
        (seller_name, merchant_name.clone()),
        (sdk_layout, DEFAULT_SDK_LAYOUT.to_owned()),
        (display_sdk_only, DEFAULT_DISPLAY_SDK_ONLY),
        (
            enabled_saved_payment_method,
            DEFAULT_ENABLE_SAVED_PAYMENT_METHOD
        ),
        (hide_card_nickname_field, DEFAULT_HIDE_CARD_NICKNAME_FIELD),
        (show_card_form_by_default, DEFAULT_SHOW_CARD_FORM),
        (
            enable_button_only_on_form_ready,
            DEFAULT_ENABLE_BUTTON_ONLY_ON_FORM_READY
        )
    );

    let (
        details_layout,
        background_image,
        payment_button_text,
        custom_message_for_card_terms,
        custom_message_for_payment_method_types,
        payment_button_colour,
        skip_status_screen,
        background_colour,
        payment_button_text_colour,
        sdk_ui_rules,
        payment_link_ui_rules,
        payment_form_header_text,
        payment_form_label_type,
        show_card_terms,
        is_setup_mandate_flow,
        color_icon_card_cvc_error,
    ) = get_payment_link_config_value!(
        payment_create_link_config,
        business_theme_configs,
        (details_layout),
        (background_image, |background_image| background_image
            .foreign_into()),
        (payment_button_text),
        (custom_message_for_card_terms),
        (custom_message_for_payment_method_types),
        (payment_button_colour),
        (skip_status_screen),
        (background_colour),
        (payment_button_text_colour),
        (sdk_ui_rules),
        (payment_link_ui_rules),
        (payment_form_header_text),
        (payment_form_label_type),
        (show_card_terms),
        (is_setup_mandate_flow),
        (color_icon_card_cvc_error),
    );

    let payment_link_config =
        PaymentLinkConfig {
            theme,
            logo,
            seller_name,
            sdk_layout,
            display_sdk_only,
            enabled_saved_payment_method,
            hide_card_nickname_field,
            show_card_form_by_default,
            allowed_domains,
            branding_visibility,
            skip_status_screen,
            transaction_details: payment_create_link_config.as_ref().and_then(
                |payment_link_config| payment_link_config.theme_config.transaction_details.clone(),
            ),
            details_layout,
            background_image,
            payment_button_text,
            custom_message_for_card_terms,
            custom_message_for_payment_method_types,
            payment_button_colour,
            background_colour,
            payment_button_text_colour,
            sdk_ui_rules,
            payment_link_ui_rules,
            enable_button_only_on_form_ready,
            payment_form_header_text,
            payment_form_label_type,
            show_card_terms,
            is_setup_mandate_flow,
            color_icon_card_cvc_error,
        };

    Ok((payment_link_config, domain_name))
}

fn capitalize_first_char(s: &str) -> String {
    if let Some(first_char) = s.chars().next() {
        let capitalized = first_char.to_uppercase();
        let mut result = capitalized.to_string();
        if let Some(remaining) = s.get(1..) {
            result.push_str(remaining);
        }
        result
    } else {
        s.to_owned()
    }
}

fn check_payment_link_invalid_conditions(
    intent_status: storage_enums::IntentStatus,
    not_allowed_statuses: &[storage_enums::IntentStatus],
) -> bool {
    not_allowed_statuses.contains(&intent_status)
}

#[cfg(feature = "v2")]
pub async fn get_payment_link_status(
    _state: SessionState,
    _platform: domain::Platform,
    _merchant_id: common_utils::id_type::MerchantId,
    _payment_id: common_utils::id_type::PaymentId,
) -> RouterResponse<services::PaymentLinkFormData> {
    todo!()
}

#[cfg(feature = "v1")]
pub async fn get_payment_link_status(
    state: SessionState,
    platform: domain::Platform,
    merchant_id: common_utils::id_type::MerchantId,
    payment_id: common_utils::id_type::PaymentId,
) -> RouterResponse<services::PaymentLinkFormData> {
    let db = &*state.store;

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

    let attempt_id = payment_intent.active_attempt.get_id().clone();
    let payment_attempt = db
        .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
            &payment_intent.payment_id,
            &merchant_id,
            &attempt_id.clone(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    let payment_link_id = payment_intent
        .payment_link_id
        .get_required_value("payment_link_id")
        .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?;

    let merchant_name_from_merchant_account = platform
        .get_processor()
        .get_account()
        .merchant_name
        .clone()
        .map(|merchant_name| merchant_name.into_inner().peek().to_owned())
        .unwrap_or_default();

    let payment_link = db
        .find_payment_link_by_payment_link_id(&payment_link_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;

    let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config {
        extract_payment_link_config(pl_config_value)?
    } else {
        PaymentLinkConfig {
            theme: DEFAULT_BACKGROUND_COLOR.to_string(),
            logo: DEFAULT_MERCHANT_LOGO.to_string(),
            seller_name: merchant_name_from_merchant_account,
            sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
            display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY,
            enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
            hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD,
            show_card_form_by_default: DEFAULT_SHOW_CARD_FORM,
            allowed_domains: DEFAULT_ALLOWED_DOMAINS,
            transaction_details: None,
            background_image: None,
            details_layout: None,
            branding_visibility: None,
            payment_button_text: None,
            custom_message_for_card_terms: None,
            custom_message_for_payment_method_types: None,
            payment_button_colour: None,
            skip_status_screen: None,
            background_colour: None,
            payment_button_text_colour: None,
            sdk_ui_rules: None,
            payment_link_ui_rules: None,
            enable_button_only_on_form_ready: DEFAULT_ENABLE_BUTTON_ONLY_ON_FORM_READY,
            payment_form_header_text: None,
            payment_form_label_type: None,
            show_card_terms: None,
            is_setup_mandate_flow: None,
            color_icon_card_cvc_error: None,
        }
    };

    let currency =
        payment_intent
            .currency
            .ok_or(errors::ApiErrorResponse::MissingRequiredField {
                field_name: "currency",
            })?;

    let required_conversion_type = StringMajorUnitForCore;

    let amount = required_conversion_type
        .convert(payment_attempt.get_total_amount(), currency)
        .change_context(errors::ApiErrorResponse::AmountConversionFailed {
            amount_type: "StringMajorUnit",
        })?;

    // converting first letter of merchant name to upperCase
    let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
    let css_script = get_payment_link_css_script(&payment_link_config)?;

    let profile_id = payment_link
        .profile_id
        .or(payment_intent.profile_id)
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Profile id missing in payment link and payment intent")?;

    let business_profile = db
        .find_business_profile_by_profile_id(platform.get_processor().get_key_store(), &profile_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
            id: profile_id.get_string_repr().to_owned(),
        })?;

    let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() {
        payment_create_return_url
    } else {
        business_profile
            .return_url
            .ok_or(errors::ApiErrorResponse::MissingRequiredField {
                field_name: "return_url",
            })?
    };
    let (unified_code, unified_message) = if let Some((code, message)) = payment_attempt
        .unified_code
        .as_ref()
        .zip(payment_attempt.unified_message.as_ref())
    {
        (code.to_owned(), message.to_owned())
    } else {
        (
            consts::DEFAULT_UNIFIED_ERROR_CODE.to_owned(),
            consts::DEFAULT_UNIFIED_ERROR_MESSAGE.to_owned(),
        )
    };
    let unified_translated_message = helpers::get_unified_translation(
        &state,
        unified_code.to_owned(),
        unified_message.to_owned(),
        state.locale.clone(),
    )
    .await
    .or(Some(unified_message));

    let payment_details = api_models::payments::PaymentLinkStatusDetails {
        amount,
        currency,
        payment_id: payment_intent.payment_id,
        merchant_name,
        merchant_logo: payment_link_config.logo.clone(),
        created: payment_link.created_at,
        status: PaymentLinkStatusWrap::IntentStatus(payment_intent.status),
        error_code: payment_attempt.error_code,
        error_message: payment_attempt.error_message,
        redirect: true,
        theme: payment_link_config.theme.clone(),
        return_url,
        locale: Some(state.locale.clone()),
        transaction_details: payment_link_config.transaction_details,
        unified_code: Some(unified_code),
        unified_message: unified_translated_message,
        capture_method: payment_attempt.capture_method,
        setup_future_usage_applied: payment_attempt.setup_future_usage_applied,
    };
    let js_script = get_js_script(&PaymentLinkData::PaymentLinkStatusDetails(Box::new(
        payment_details,
    )))?;
    let payment_link_status_data = services::PaymentLinkStatusData {
        js_script,
        css_script,
    };
    Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
        services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_status_data),
    )))
}


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

use api_models::recon as recon_api;
#[cfg(feature = "email")]
use common_utils::{ext_traits::AsyncExt, types::user::ThemeLineage};
use error_stack::ResultExt;
#[cfg(feature = "email")]
use masking::{ExposeInterface, PeekInterface, Secret};

#[cfg(feature = "email")]
use crate::{
    consts, services::email::types as email_types, types::domain, utils::user::theme as theme_utils,
};
use crate::{
    core::errors::{self, RouterResponse, UserErrors, UserResponse},
    services::{api as service_api, authentication},
    types::{
        api::{self as api_types, enums},
        storage,
        transformers::ForeignTryFrom,
    },
    utils::user as user_utils,
    SessionState,
};

#[allow(unused_variables)]
pub async fn send_recon_request(
    state: SessionState,
    auth_data: authentication::AuthenticationDataWithUser,
) -> RouterResponse<recon_api::ReconStatusResponse> {
    #[cfg(not(feature = "email"))]
    return Ok(service_api::ApplicationResponse::Json(
        recon_api::ReconStatusResponse {
            recon_status: enums::ReconStatus::NotRequested,
        },
    ));

    #[cfg(feature = "email")]
    {
        let user_in_db = &auth_data.user;
        let merchant_id = auth_data.merchant_account.get_id().clone();

        let theme = theme_utils::get_most_specific_theme_using_lineage(
            &state.clone(),
            ThemeLineage::Merchant {
                tenant_id: state.tenant.tenant_id.clone(),
                org_id: auth_data.merchant_account.get_org_id().clone(),
                merchant_id: merchant_id.clone(),
            },
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable(format!(
            "Failed to fetch theme for merchant_id = {merchant_id:?}",
        ))?;

        let user_email = user_in_db.email.clone();
        let email_contents = email_types::ProFeatureRequest {
            feature_name: consts::RECON_FEATURE_TAG.to_string(),
            merchant_id: merchant_id.clone(),
            user_name: domain::UserName::new(user_in_db.name.clone())
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to form username")?,
            user_email: domain::UserEmail::from_pii_email(user_email.clone())
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to convert recipient's email to UserEmail")?,
            recipient_email: domain::UserEmail::from_pii_email(
                state.conf.email.recon_recipient_email.clone(),
            )
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to convert recipient's email to UserEmail")?,
            subject: format!(
                "{} {}",
                consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST,
                user_email.expose().peek()
            ),
            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
            theme_config: theme
                .map(|theme| theme.email_config())
                .unwrap_or(state.conf.theme.email_config.clone()),
        };
        state
            .email_client
            .compose_and_send_email(
                user_utils::get_base_url(&state),
                Box::new(email_contents),
                state.conf.proxy.https_url.as_ref(),
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to compose and send email for ProFeatureRequest [Recon]")
            .async_and_then(|_| async {
                let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
                    recon_status: enums::ReconStatus::Requested,
                };
                let db = &*state.store;

                let response = db
                    .update_merchant(
                        auth_data.merchant_account,
                        updated_merchant_account,
                        &auth_data.key_store,
                    )
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable_lazy(|| {
                        format!("Failed while updating merchant's recon status: {merchant_id:?}")
                    })?;

                Ok(service_api::ApplicationResponse::Json(
                    recon_api::ReconStatusResponse {
                        recon_status: response.recon_status,
                    },
                ))
            })
            .await
    }
}

pub async fn generate_recon_token(
    state: SessionState,
    user_with_role: authentication::UserFromTokenWithRoleInfo,
) -> RouterResponse<recon_api::ReconTokenResponse> {
    let user = user_with_role.user;
    let token = authentication::ReconToken::new_token(
        user.user_id.clone(),
        user.merchant_id.clone(),
        &state.conf,
        user.org_id.clone(),
        user.profile_id.clone(),
        user.tenant_id,
        user_with_role.role_info,
    )
    .await
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable_lazy(|| {
        format!(
            "Failed to create recon token for params [user_id, org_id, mid, pid] [{}, {:?}, {:?}, {:?}]",
            user.user_id, user.org_id, user.merchant_id, user.profile_id,
        )
    })?;

    Ok(service_api::ApplicationResponse::Json(
        recon_api::ReconTokenResponse {
            token: token.into(),
        },
    ))
}

pub async fn recon_merchant_account_update(
    state: SessionState,
    auth: authentication::AuthenticationData,
    req: recon_api::ReconUpdateMerchantRequest,
) -> RouterResponse<api_types::MerchantAccountResponse> {
    let db = &*state.store;

    let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
        recon_status: req.recon_status,
    };
    let merchant_id = auth.merchant_account.get_id().clone();

    let updated_merchant_account = db
        .update_merchant(
            auth.merchant_account.clone(),
            updated_merchant_account,
            &auth.key_store,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!("Failed while updating merchant's recon status: {merchant_id:?}")
        })?;

    #[cfg(feature = "email")]
    {
        let user_email = &req.user_email.clone();

        let theme = theme_utils::get_most_specific_theme_using_lineage(
            &state.clone(),
            ThemeLineage::Merchant {
                tenant_id: state.tenant.tenant_id.clone(),
                org_id: auth.merchant_account.get_org_id().clone(),
                merchant_id: merchant_id.clone(),
            },
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable(format!(
            "Failed to fetch theme for merchant_id = {merchant_id:?}",
        ))?;

        let email_contents = email_types::ReconActivation {
            recipient_email: domain::UserEmail::from_pii_email(user_email.clone())
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable(
                    "Failed to convert recipient's email to UserEmail from pii::Email",
                )?,
            user_name: domain::UserName::new(Secret::new("HyperSwitch User".to_string()))
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to form username")?,
            subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST,
            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
            theme_config: theme
                .map(|theme| theme.email_config())
                .unwrap_or(state.conf.theme.email_config.clone()),
        };
        if req.recon_status == enums::ReconStatus::Active {
            let _ = state
                .email_client
                .compose_and_send_email(
                    user_utils::get_base_url(&state),
                    Box::new(email_contents),
                    state.conf.proxy.https_url.as_ref(),
                )
                .await
                .inspect_err(|err| {
                    router_env::logger::error!(
                        "Failed to compose and send email notifying them of recon activation: {}",
                        err
                    )
                })
                .change_context(UserErrors::InternalServerError)
                .attach_printable("Failed to compose and send email for ReconActivation");
        }
    }

    Ok(service_api::ApplicationResponse::Json(
        api_types::MerchantAccountResponse::foreign_try_from(updated_merchant_account)
            .change_context(errors::ApiErrorResponse::InvalidDataValue {
                field_name: "merchant_account",
            })?,
    ))
}

pub async fn verify_recon_token(
    state: SessionState,
    user_with_role: authentication::UserFromTokenWithRoleInfo,
) -> UserResponse<recon_api::VerifyTokenResponse> {
    let user = user_with_role.user;
    let user_in_db = user
        .get_user_from_db(&state)
        .await
        .attach_printable_lazy(|| {
            format!(
                "Failed to fetch the user from DB for user_id - {}",
                user.user_id
            )
        })?;

    let acl = user_with_role.role_info.get_recon_acl();
    let optional_acl_str = serde_json::to_string(&acl)
        .inspect_err(|err| router_env::logger::error!("Failed to serialize acl to string: {}", err))
        .change_context(UserErrors::InternalServerError)
        .attach_printable("Failed to serialize acl to string. Using empty ACL")
        .ok();

    Ok(service_api::ApplicationResponse::Json(
        recon_api::VerifyTokenResponse {
            merchant_id: user.merchant_id.to_owned(),
            user_email: user_in_db.0.email,
            acl: optional_acl_str,
        },
    ))
}


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

use api_models::poll::PollResponse;
use common_utils::ext_traits::StringExt;
use error_stack::ResultExt;
use router_env::{instrument, tracing};

use super::errors;
use crate::{
    core::errors::RouterResponse, services::ApplicationResponse, types::domain, SessionState,
};

#[instrument(skip_all)]
pub async fn retrieve_poll_status(
    state: SessionState,
    req: crate::types::api::PollId,
    platform: domain::Platform,
) -> RouterResponse<PollResponse> {
    let redis_conn = state
        .store
        .get_redis_conn()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get redis connection")?;
    let request_poll_id = req.poll_id;
    // prepend 'poll_{merchant_id}_' to restrict access to only fetching Poll IDs, as this is a freely passed string in the request
    let poll_id = super::utils::get_poll_id(
        platform.get_processor().get_account().get_id(),
        request_poll_id.clone(),
    );
    let redis_value = redis_conn
        .get_key::<Option<String>>(&poll_id.as_str().into())
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!(
                "Error while fetching the value for {} from redis",
                poll_id.clone()
            )
        })?
        .ok_or(errors::ApiErrorResponse::PollNotFound {
            id: request_poll_id.clone(),
        })?;
    let status = redis_value
        .parse_enum("PollStatus")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error while parsing PollStatus")?;
    let poll_response = PollResponse {
        poll_id: request_poll_id,
        status,
    };
    Ok(ApplicationResponse::Json(poll_response))
}


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

#[cfg(feature = "v1")]
pub mod incoming;
#[cfg(feature = "v2")]
mod incoming_v2;
#[cfg(feature = "v1")]
mod network_tokenization_incoming;
#[cfg(feature = "v1")]
mod outgoing;
#[cfg(feature = "v2")]
mod outgoing_v2;
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
pub mod recovery_incoming;
pub mod types;
pub mod utils;
#[cfg(feature = "olap")]
pub mod webhook_events;

#[cfg(feature = "v1")]
pub(crate) use self::{
    incoming::{incoming_webhooks_wrapper, network_token_incoming_webhooks_wrapper},
    outgoing::{
        create_event_and_trigger_outgoing_webhook, get_outgoing_webhook_request,
        trigger_webhook_and_raise_event,
    },
};
#[cfg(feature = "v2")]
pub(crate) use self::{
    incoming_v2::incoming_webhooks_wrapper, outgoing_v2::create_event_and_trigger_outgoing_webhook,
};

const MERCHANT_ID: &str = "merchant_id";


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

use common_utils::date_time;
#[cfg(feature = "email")]
use diesel_models::{api_keys::ApiKey, enums as storage_enums};
use error_stack::{report, ResultExt};
use masking::{PeekInterface, StrongSecret};
use router_env::{instrument, tracing};

use crate::{
    configs::settings,
    consts,
    core::errors::{self, RouterResponse, StorageErrorExt},
    db::domain,
    routes::{metrics, SessionState},
    services::{authentication, ApplicationResponse},
    types::{api, storage, transformers::ForeignInto},
};

#[cfg(feature = "email")]
const API_KEY_EXPIRY_TAG: &str = "API_KEY";
#[cfg(feature = "email")]
const API_KEY_EXPIRY_NAME: &str = "API_KEY_EXPIRY";
#[cfg(feature = "email")]
const API_KEY_EXPIRY_RUNNER: diesel_models::ProcessTrackerRunner =
    diesel_models::ProcessTrackerRunner::ApiKeyExpiryWorkflow;

static HASH_KEY: once_cell::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> =
    once_cell::sync::OnceCell::new();

impl settings::ApiKeys {
    pub fn get_hash_key(
        &self,
    ) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> {
        HASH_KEY.get_or_try_init(|| {
            <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from(
                hex::decode(self.hash_key.peek())
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("API key hash key has invalid hexadecimal data")?
                    .as_slice(),
            )
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("The API hashing key has incorrect length")
            .map(StrongSecret::new)
        })
    }
}

// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
// of plaintext API key being stored in the data store.
pub struct PlaintextApiKey(StrongSecret<String>);

#[derive(Debug, PartialEq, Eq)]
pub struct HashedApiKey(String);

impl PlaintextApiKey {
    const HASH_KEY_LEN: usize = 32;

    const PREFIX_LEN: usize = 12;

    pub fn new(length: usize) -> Self {
        let env = router_env::env::prefix_for_env();
        let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
        Self(format!("{env}_{key}").into())
    }

    pub fn new_key_id() -> common_utils::id_type::ApiKeyId {
        let env = router_env::env::prefix_for_env();
        common_utils::id_type::ApiKeyId::generate_key_id(env)
    }

    pub fn prefix(&self) -> String {
        self.0.peek().chars().take(Self::PREFIX_LEN).collect()
    }

    pub fn peek(&self) -> &str {
        self.0.peek()
    }

    pub fn keyed_hash(&self, key: &[u8; Self::HASH_KEY_LEN]) -> HashedApiKey {
        /*
        Decisions regarding API key hashing algorithm chosen:

        - Since API key hash verification would be done for each request, there is a requirement
          for the hashing to be quick.
        - Password hashing algorithms would not be suitable for this purpose as they're designed to
          prevent brute force attacks, considering that the same password could be shared  across
          multiple sites by the user.
        - Moreover, password hash verification happens once per user session, so the delay involved
          is negligible, considering the security benefits it provides.
          While with API keys (assuming uniqueness of keys across the application), the delay
          involved in hashing (with the use of a password hashing algorithm) becomes significant,
          considering that it must be done per request.
        - Since we are the only ones generating API keys and are able to guarantee their uniqueness,
          a simple hash algorithm is sufficient for this purpose.

        Hash algorithms considered:
        - Password hashing algorithms: Argon2id and PBKDF2
        - Simple hashing algorithms: HMAC-SHA256, HMAC-SHA512, BLAKE3

        After benchmarking the simple hashing algorithms, we decided to go with the BLAKE3 keyed
        hashing algorithm, with a randomly generated key for the hash key.
        */

        HashedApiKey(
            blake3::keyed_hash(key, self.0.peek().as_bytes())
                .to_hex()
                .to_string(),
        )
    }
}

#[instrument(skip_all)]
pub async fn create_api_key(
    state: SessionState,
    api_key: api::CreateApiKeyRequest,
    key_store: domain::MerchantKeyStore,
) -> RouterResponse<api::CreateApiKeyResponse> {
    let api_key_config = state.conf.api_keys.get_inner();
    let store = state.store.as_ref();

    let merchant_id = key_store.merchant_id.clone();

    let hash_key = api_key_config.get_hash_key()?;
    let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
    let api_key = storage::ApiKeyNew {
        key_id: PlaintextApiKey::new_key_id(),
        merchant_id: merchant_id.to_owned(),
        name: api_key.name,
        description: api_key.description,
        hashed_api_key: plaintext_api_key.keyed_hash(hash_key.peek()).into(),
        prefix: plaintext_api_key.prefix(),
        created_at: date_time::now(),
        expires_at: api_key.expiration.into(),
        last_used: None,
    };

    let api_key = store
        .insert_api_key(api_key)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to insert new API key")?;

    let state_inner = state.clone();
    let hashed_api_key = api_key.hashed_api_key.clone();
    let merchant_id_inner = merchant_id.clone();
    let key_id = api_key.key_id.clone();
    let expires_at = api_key.expires_at;

    authentication::decision::spawn_tracked_job(
        async move {
            authentication::decision::add_api_key(
                &state_inner,
                hashed_api_key.into_inner().into(),
                merchant_id_inner,
                key_id,
                expires_at.map(authentication::decision::convert_expiry),
            )
            .await
        },
        authentication::decision::ADD,
    );

    metrics::API_KEY_CREATED.add(
        1,
        router_env::metric_attributes!(("merchant", merchant_id.clone())),
    );

    // Add process to process_tracker for email reminder, only if expiry is set to future date
    // If the `api_key` is set to expire in less than 7 days, the merchant is not notified about it's expiry
    #[cfg(feature = "email")]
    {
        if api_key.expires_at.is_some() {
            let expiry_reminder_days = state.conf.api_keys.get_inner().expiry_reminder_days.clone();

            add_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to insert API key expiry reminder to process tracker")?;
        }
    }

    Ok(ApplicationResponse::Json(
        (api_key, plaintext_api_key).foreign_into(),
    ))
}

// Add api_key_expiry task to the process_tracker table.
// Construct ProcessTrackerNew struct with all required fields, and schedule the first email.
// After first email has been sent, update the schedule_time based on retry_count in execute_workflow().
// A task is not scheduled if the time for the first email is in the past.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn add_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    api_key: &ApiKey,
    expiry_reminder_days: Vec<u8>,
) -> Result<(), errors::ProcessTrackerError> {
    let current_time = date_time::now();

    let schedule_time = expiry_reminder_days
        .first()
        .and_then(|expiry_reminder_day| {
            api_key.expires_at.map(|expires_at| {
                expires_at.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day)))
            })
        })
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to obtain initial process tracker schedule time")?;

    if schedule_time <= current_time {
        return Ok(());
    }

    let api_key_expiry_tracker = storage::ApiKeyExpiryTrackingData {
        key_id: api_key.key_id.clone(),
        merchant_id: api_key.merchant_id.clone(),
        api_key_name: api_key.name.clone(),
        prefix: api_key.prefix.clone(),
        // We need API key expiry too, because we need to decide on the schedule_time in
        // execute_workflow() where we won't be having access to the Api key object.
        api_key_expiry: api_key.expires_at,
        expiry_reminder_days: expiry_reminder_days.clone(),
    };

    let process_tracker_id = generate_task_id_for_api_key_expiry_workflow(&api_key.key_id);
    let process_tracker_entry = storage::ProcessTrackerNew::new(
        process_tracker_id,
        API_KEY_EXPIRY_NAME,
        API_KEY_EXPIRY_RUNNER,
        [API_KEY_EXPIRY_TAG],
        api_key_expiry_tracker,
        None,
        schedule_time,
        common_types::consts::API_VERSION,
    )
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed to construct API key expiry process tracker task")?;

    store
        .insert_process(process_tracker_entry)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!(
                "Failed while inserting API key expiry reminder to process_tracker: {:?}",
                api_key.key_id
            )
        })?;
    metrics::TASKS_ADDED_COUNT.add(1, router_env::metric_attributes!(("flow", "ApiKeyExpiry")));

    Ok(())
}

#[instrument(skip_all)]
pub async fn retrieve_api_key(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    key_id: common_utils::id_type::ApiKeyId,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
    let store = state.store.as_ref();
    let api_key = store
        .find_api_key_by_merchant_id_key_id_optional(&merchant_id, &key_id)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
        .attach_printable("Failed to retrieve API key")?
        .ok_or(report!(errors::ApiErrorResponse::ApiKeyNotFound))?; // If retrieve returned `None`

    Ok(ApplicationResponse::Json(api_key.foreign_into()))
}

#[instrument(skip_all)]
pub async fn update_api_key(
    state: SessionState,
    api_key: api::UpdateApiKeyRequest,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
    let merchant_id = api_key.merchant_id.clone();
    let key_id = api_key.key_id.clone();
    let store = state.store.as_ref();

    let api_key = store
        .update_api_key(
            merchant_id.to_owned(),
            key_id.to_owned(),
            api_key.foreign_into(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    let state_inner = state.clone();
    let hashed_api_key = api_key.hashed_api_key.clone();
    let key_id_inner = api_key.key_id.clone();
    let expires_at = api_key.expires_at;

    authentication::decision::spawn_tracked_job(
        async move {
            authentication::decision::add_api_key(
                &state_inner,
                hashed_api_key.into_inner().into(),
                merchant_id.clone(),
                key_id_inner,
                expires_at.map(authentication::decision::convert_expiry),
            )
            .await
        },
        authentication::decision::ADD,
    );

    #[cfg(feature = "email")]
    {
        let expiry_reminder_days = state.conf.api_keys.get_inner().expiry_reminder_days.clone();

        let task_id = generate_task_id_for_api_key_expiry_workflow(&key_id);
        // In order to determine how to update the existing process in the process_tracker table,
        // we need access to the current entry in the table.
        let existing_process_tracker_task = store
            .find_process_by_id(task_id.as_str())
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
            .attach_printable(
                "Failed to retrieve API key expiry reminder task from process tracker",
            )?;

        // If process exist
        if existing_process_tracker_task.is_some() {
            if api_key.expires_at.is_some() {
                // Process exist in process, update the process with new schedule_time
                update_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable(
                        "Failed to update API key expiry reminder task in process tracker",
                    )?;
            }
            // If an expiry is set to 'never'
            else {
                // Process exist in process, revoke it
                revoke_api_key_expiry_task(store, &key_id)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable(
                        "Failed to revoke API key expiry reminder task in process tracker",
                    )?;
            }
        }
        // This case occurs if the expiry for an API key is set to 'never' during its creation. If so,
        // process in tracker was not created.
        else if api_key.expires_at.is_some() {
            // Process doesn't exist in process_tracker table, so create new entry with
            // schedule_time based on new expiry set.
            add_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable(
                    "Failed to insert API key expiry reminder task to process tracker",
                )?;
        }
    }

    Ok(ApplicationResponse::Json(api_key.foreign_into()))
}

// Update api_key_expiry task in the process_tracker table.
// Construct Update variant of ProcessTrackerUpdate with new tracking_data.
// A task is not scheduled if the time for the first email is in the past.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn update_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    api_key: &ApiKey,
    expiry_reminder_days: Vec<u8>,
) -> Result<(), errors::ProcessTrackerError> {
    let current_time = date_time::now();

    let schedule_time = expiry_reminder_days
        .first()
        .and_then(|expiry_reminder_day| {
            api_key.expires_at.map(|expires_at| {
                expires_at.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day)))
            })
        });

    if let Some(schedule_time) = schedule_time {
        if schedule_time <= current_time {
            return Ok(());
        }
    }

    let task_id = generate_task_id_for_api_key_expiry_workflow(&api_key.key_id);

    let task_ids = vec![task_id.clone()];

    let updated_tracking_data = &storage::ApiKeyExpiryTrackingData {
        key_id: api_key.key_id.clone(),
        merchant_id: api_key.merchant_id.clone(),
        api_key_name: api_key.name.clone(),
        prefix: api_key.prefix.clone(),
        api_key_expiry: api_key.expires_at,
        expiry_reminder_days,
    };

    let updated_api_key_expiry_workflow_model = serde_json::to_value(updated_tracking_data)
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!("unable to serialize API key expiry tracker: {updated_tracking_data:?}")
        })?;

    let updated_process_tracker_data = storage::ProcessTrackerUpdate::Update {
        name: None,
        retry_count: Some(0),
        schedule_time,
        tracking_data: Some(updated_api_key_expiry_workflow_model),
        business_status: Some(String::from(
            diesel_models::process_tracker::business_status::PENDING,
        )),
        status: Some(storage_enums::ProcessTrackerStatus::New),
        updated_at: Some(current_time),
    };
    store
        .process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn revoke_api_key(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    key_id: &common_utils::id_type::ApiKeyId,
) -> RouterResponse<api::RevokeApiKeyResponse> {
    let store = state.store.as_ref();

    let api_key = store
        .find_api_key_by_merchant_id_key_id_optional(&merchant_id, key_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    let revoked = store
        .revoke_api_key(&merchant_id, key_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    if let Some(api_key) = api_key {
        let hashed_api_key = api_key.hashed_api_key;
        let state = state.clone();

        authentication::decision::spawn_tracked_job(
            async move {
                authentication::decision::revoke_api_key(&state, hashed_api_key.into_inner().into())
                    .await
            },
            authentication::decision::REVOKE,
        );
    }

    metrics::API_KEY_REVOKED.add(1, &[]);

    #[cfg(feature = "email")]
    {
        let task_id = generate_task_id_for_api_key_expiry_workflow(key_id);
        // In order to determine how to update the existing process in the process_tracker table,
        // we need access to the current entry in the table.
        let existing_process_tracker_task = store
            .find_process_by_id(task_id.as_str())
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
            .attach_printable(
                "Failed to retrieve API key expiry reminder task from process tracker",
            )?;

        // If process exist, then revoke it
        if existing_process_tracker_task.is_some() {
            revoke_api_key_expiry_task(store, key_id)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable(
                    "Failed to revoke API key expiry reminder task in process tracker",
                )?;
        }
    }

    Ok(ApplicationResponse::Json(api::RevokeApiKeyResponse {
        merchant_id: merchant_id.to_owned(),
        key_id: key_id.to_owned(),
        revoked,
    }))
}

// Function to revoke api_key_expiry task in the process_tracker table when API key is revoked.
// Construct StatusUpdate variant of ProcessTrackerUpdate by setting status to 'finish'.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn revoke_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    key_id: &common_utils::id_type::ApiKeyId,
) -> Result<(), errors::ProcessTrackerError> {
    let task_id = generate_task_id_for_api_key_expiry_workflow(key_id);
    let task_ids = vec![task_id];
    let updated_process_tracker_data = storage::ProcessTrackerUpdate::StatusUpdate {
        status: storage_enums::ProcessTrackerStatus::Finish,
        business_status: Some(String::from(diesel_models::business_status::REVOKED)),
    };

    store
        .process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn list_api_keys(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    limit: Option<i64>,
    offset: Option<i64>,
) -> RouterResponse<Vec<api::RetrieveApiKeyResponse>> {
    let store = state.store.as_ref();
    let api_keys = store
        .list_api_keys_by_merchant_id(&merchant_id, limit, offset)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to list merchant API keys")?;
    let api_keys = api_keys
        .into_iter()
        .map(ForeignInto::foreign_into)
        .collect();

    Ok(ApplicationResponse::Json(api_keys))
}

#[cfg(feature = "email")]
fn generate_task_id_for_api_key_expiry_workflow(
    key_id: &common_utils::id_type::ApiKeyId,
) -> String {
    format!(
        "{API_KEY_EXPIRY_RUNNER}_{API_KEY_EXPIRY_NAME}_{}",
        key_id.get_string_repr()
    )
}

impl From<&str> for PlaintextApiKey {
    fn from(s: &str) -> Self {
        Self(s.to_owned().into())
    }
}

impl From<String> for PlaintextApiKey {
    fn from(s: String) -> Self {
        Self(s.into())
    }
}

impl From<HashedApiKey> for storage::HashedApiKey {
    fn from(hashed_api_key: HashedApiKey) -> Self {
        hashed_api_key.0.into()
    }
}

impl From<storage::HashedApiKey> for HashedApiKey {
    fn from(hashed_api_key: storage::HashedApiKey) -> Self {
        Self(hashed_api_key.into_inner())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_hashing_and_verification() {
        let settings = settings::Settings::new().expect("invalid settings");

        let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
        let hash_key = settings.api_keys.get_inner().get_hash_key().unwrap();
        let hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());

        assert_ne!(
            plaintext_api_key.0.peek().as_bytes(),
            hashed_api_key.0.as_bytes()
        );

        let new_hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());
        assert_eq!(hashed_api_key, new_hashed_api_key)
    }
}


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

pub mod chat;
pub mod customers_error_response;
pub mod error_handlers;
pub mod transformers;
#[cfg(feature = "olap")]
pub mod user;
pub mod utils;

use std::fmt::Display;

pub use ::payment_methods::core::errors::VaultError;
use actix_web::{body::BoxBody, ResponseError};
pub use common_utils::errors::{CustomResult, ParsingError, ValidationError};
use diesel_models::errors as storage_errors;
pub use hyperswitch_domain_models::errors::api_error_response::{
    ApiErrorResponse, ErrorType, NotImplementedMessage,
};
pub use hyperswitch_interfaces::errors::ConnectorError;
pub use redis_interface::errors::RedisError;
use scheduler::errors as sch_errors;
use storage_impl::errors as storage_impl_errors;
#[cfg(feature = "olap")]
pub use user::*;

pub use self::{
    customers_error_response::CustomersErrorResponse,
    sch_errors::*,
    storage_errors::*,
    storage_impl_errors::*,
    utils::{ConnectorErrorExt, StorageErrorExt},
};
use crate::services;
pub type RouterResult<T> = CustomResult<T, ApiErrorResponse>;
pub type RouterResponse<T> = CustomResult<services::ApplicationResponse<T>, ApiErrorResponse>;

pub type ApplicationResult<T> = error_stack::Result<T, ApplicationError>;
pub type ApplicationResponse<T> = ApplicationResult<services::ApplicationResponse<T>>;

pub type CustomerResponse<T> =
    CustomResult<services::ApplicationResponse<T>, CustomersErrorResponse>;

macro_rules! impl_error_display {
    ($st: ident, $arg: tt) => {
        impl Display for $st {
            fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(
                    fmt,
                    "{{ error_type: {:?}, error_description: {} }}",
                    self, $arg
                )
            }
        }
    };
}

#[macro_export]
macro_rules! capture_method_not_supported {
    ($connector:expr, $capture_method:expr) => {
        Err(errors::ConnectorError::NotSupported {
            message: format!("{} for selected payment method", $capture_method),
            connector: $connector,
        }
        .into())
    };
    ($connector:expr, $capture_method:expr, $payment_method_type:expr) => {
        Err(errors::ConnectorError::NotSupported {
            message: format!("{} for {}", $capture_method, $payment_method_type),
            connector: $connector,
        }
        .into())
    };
}

#[macro_export]
macro_rules! unimplemented_payment_method {
    ($payment_method:expr, $connector:expr) => {
        errors::ConnectorError::NotImplemented(format!(
            "{} through {}",
            $payment_method, $connector
        ))
    };
    ($payment_method:expr, $flow:expr, $connector:expr) => {
        errors::ConnectorError::NotImplemented(format!(
            "{} {} through {}",
            $payment_method, $flow, $connector
        ))
    };
}

macro_rules! impl_error_type {
    ($name: ident, $arg: tt) => {
        #[derive(Debug)]
        pub struct $name;

        impl_error_display!($name, $arg);

        impl std::error::Error for $name {}
    };
}

impl_error_type!(EncryptionError, "Encryption error");

impl From<ring::error::Unspecified> for EncryptionError {
    fn from(_: ring::error::Unspecified) -> Self {
        Self
    }
}

pub fn http_not_implemented() -> actix_web::HttpResponse<BoxBody> {
    ApiErrorResponse::NotImplemented {
        message: NotImplementedMessage::Default,
    }
    .error_response()
}

#[derive(Debug, thiserror::Error)]
pub enum HealthCheckOutGoing {
    #[error("Outgoing call failed with error: {message}")]
    OutGoingFailed { message: String },
}

#[derive(Debug, thiserror::Error)]
pub enum AwsKmsError {
    #[error("Failed to base64 decode input data")]
    Base64DecodingFailed,
    #[error("Failed to AWS KMS decrypt input data")]
    DecryptionFailed,
    #[error("Missing plaintext AWS KMS decryption output")]
    MissingPlaintextDecryptionOutput,
    #[error("Failed to UTF-8 decode decryption output")]
    Utf8DecodingFailed,
}

#[derive(Debug, thiserror::Error, serde::Serialize)]
pub enum WebhooksFlowError {
    #[error("Merchant webhook config not found")]
    MerchantConfigNotFound,
    #[error("Webhook details for merchant not configured")]
    MerchantWebhookDetailsNotFound,
    #[error("Merchant does not have a webhook URL configured")]
    MerchantWebhookUrlNotConfigured,
    #[error("Webhook event updation failed")]
    WebhookEventUpdationFailed,
    #[error("Outgoing webhook body signing failed")]
    OutgoingWebhookSigningFailed,
    #[error("Webhook api call to merchant failed")]
    CallToMerchantFailed,
    #[error("Webhook not received by merchant")]
    NotReceivedByMerchant,
    #[error("Dispute webhook status validation failed")]
    DisputeWebhookValidationFailed,
    #[error("Outgoing webhook body encoding failed")]
    OutgoingWebhookEncodingFailed,
    #[error("Failed to update outgoing webhook process tracker task")]
    OutgoingWebhookProcessTrackerTaskUpdateFailed,
    #[error("Failed to schedule retry attempt for outgoing webhook")]
    OutgoingWebhookRetrySchedulingFailed,
    #[error("Outgoing webhook response encoding failed")]
    OutgoingWebhookResponseEncodingFailed,
    #[error("ID generation failed")]
    IdGenerationFailed,
}

impl WebhooksFlowError {
    pub(crate) fn is_webhook_delivery_retryable_error(&self) -> bool {
        match self {
            Self::MerchantConfigNotFound
            | Self::MerchantWebhookDetailsNotFound
            | Self::MerchantWebhookUrlNotConfigured
            | Self::OutgoingWebhookResponseEncodingFailed => false,

            Self::WebhookEventUpdationFailed
            | Self::OutgoingWebhookSigningFailed
            | Self::CallToMerchantFailed
            | Self::NotReceivedByMerchant
            | Self::DisputeWebhookValidationFailed
            | Self::OutgoingWebhookEncodingFailed
            | Self::OutgoingWebhookProcessTrackerTaskUpdateFailed
            | Self::OutgoingWebhookRetrySchedulingFailed
            | Self::IdGenerationFailed => true,
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ApplePayDecryptionError {
    #[error("Failed to base64 decode input data")]
    Base64DecodingFailed,
    #[error("Failed to decrypt input data")]
    DecryptionFailed,
    #[error("Certificate parsing failed")]
    CertificateParsingFailed,
    #[error("Certificate parsing failed")]
    MissingMerchantId,
    #[error("Key Deserialization failure")]
    KeyDeserializationFailed,
    #[error("Failed to Derive a shared secret key")]
    DerivingSharedSecretKeyFailed,
}

#[derive(Debug, thiserror::Error)]
pub enum PazeDecryptionError {
    #[error("Failed to base64 decode input data")]
    Base64DecodingFailed,
    #[error("Failed to decrypt input data")]
    DecryptionFailed,
    #[error("Certificate parsing failed")]
    CertificateParsingFailed,
}

#[derive(Debug, thiserror::Error)]
pub enum GooglePayDecryptionError {
    #[error("Invalid expiration time")]
    InvalidExpirationTime,
    #[error("Failed to base64 decode input data")]
    Base64DecodingFailed,
    #[error("Failed to decrypt input data")]
    DecryptionFailed,
    #[error("Failed to deserialize input data")]
    DeserializationFailed,
    #[error("Certificate parsing failed")]
    CertificateParsingFailed,
    #[error("Key deserialization failure")]
    KeyDeserializationFailed,
    #[error("Failed to derive a shared ephemeral key")]
    DerivingSharedEphemeralKeyFailed,
    #[error("Failed to derive a shared secret key")]
    DerivingSharedSecretKeyFailed,
    #[error("Failed to parse the tag")]
    ParsingTagError,
    #[error("HMAC verification failed")]
    HmacVerificationFailed,
    #[error("Failed to derive Elliptic Curve key")]
    DerivingEcKeyFailed,
    #[error("Failed to Derive Public key")]
    DerivingPublicKeyFailed,
    #[error("Failed to Derive Elliptic Curve group")]
    DerivingEcGroupFailed,
    #[error("Failed to allocate memory for big number")]
    BigNumAllocationFailed,
    #[error("Failed to get the ECDSA signature")]
    EcdsaSignatureFailed,
    #[error("Failed to verify the signature")]
    SignatureVerificationFailed,
    #[error("Invalid signature is provided")]
    InvalidSignature,
    #[error("Failed to parse the Signed Key")]
    SignedKeyParsingFailure,
    #[error("The Signed Key is expired")]
    SignedKeyExpired,
    #[error("Failed to parse the ECDSA signature")]
    EcdsaSignatureParsingFailed,
    #[error("Invalid intermediate signature is provided")]
    InvalidIntermediateSignature,
    #[error("Invalid protocol version")]
    InvalidProtocolVersion,
    #[error("Decrypted Token has expired")]
    DecryptedTokenExpired,
    #[error("Failed to parse the given value")]
    ParsingFailed,
}

#[cfg(feature = "detailed_errors")]
pub mod error_stack_parsing {

    #[derive(serde::Deserialize)]
    pub struct NestedErrorStack<'a> {
        context: std::borrow::Cow<'a, str>,
        attachments: Vec<std::borrow::Cow<'a, str>>,
        sources: Vec<NestedErrorStack<'a>>,
    }

    #[derive(serde::Serialize, Debug)]
    struct LinearErrorStack<'a> {
        context: std::borrow::Cow<'a, str>,
        #[serde(skip_serializing_if = "Vec::is_empty")]
        attachments: Vec<std::borrow::Cow<'a, str>>,
    }

    #[derive(serde::Serialize, Debug)]
    pub struct VecLinearErrorStack<'a>(Vec<LinearErrorStack<'a>>);

    impl<'a> From<Vec<NestedErrorStack<'a>>> for VecLinearErrorStack<'a> {
        fn from(value: Vec<NestedErrorStack<'a>>) -> Self {
            let multi_layered_errors: Vec<_> = value
                .into_iter()
                .flat_map(|current_error| {
                    [LinearErrorStack {
                        context: current_error.context,
                        attachments: current_error.attachments,
                    }]
                    .into_iter()
                    .chain(Into::<VecLinearErrorStack<'a>>::into(current_error.sources).0)
                })
                .collect();
            Self(multi_layered_errors)
        }
    }
}
#[cfg(feature = "detailed_errors")]
pub use error_stack_parsing::*;

#[derive(Debug, Clone, thiserror::Error)]
pub enum RoutingError {
    #[error("Merchant routing algorithm not found in cache")]
    CacheMiss,
    #[error("Final connector selection failed")]
    ConnectorSelectionFailed,
    #[error("[DSL] Missing required field in payment data: '{field_name}'")]
    DslMissingRequiredField { field_name: String },
    #[error("The lock on the DSL cache is most probably poisoned")]
    DslCachePoisoned,
    #[error("Expected DSL to be saved in DB but did not find")]
    DslMissingInDb,
    #[error("Unable to parse DSL from JSON")]
    DslParsingError,
    #[error("Failed to initialize DSL backend")]
    DslBackendInitError,
    #[error("Error updating merchant with latest dsl cache contents")]
    DslMerchantUpdateError,
    #[error("Error executing the DSL")]
    DslExecutionError,
    #[error("Final connector selection failed")]
    DslFinalConnectorSelectionFailed,
    #[error("[DSL] Received incorrect selection algorithm as DSL output")]
    DslIncorrectSelectionAlgorithm,
    #[error("there was an error saving/retrieving values from the kgraph cache")]
    KgraphCacheFailure,
    #[error("failed to refresh the kgraph cache")]
    KgraphCacheRefreshFailed,
    #[error("there was an error during the kgraph analysis phase")]
    KgraphAnalysisError,
    #[error("'profile_id' was not provided")]
    ProfileIdMissing,
    #[error("the profile was not found in the database")]
    ProfileNotFound,
    #[error("failed to fetch the fallback config for the merchant")]
    FallbackConfigFetchFailed,
    #[error("Invalid connector name received: '{0}'")]
    InvalidConnectorName(String),
    #[error("The routing algorithm in merchant account had invalid structure")]
    InvalidRoutingAlgorithmStructure,
    #[error("Volume split failed")]
    VolumeSplitFailed,
    #[error("Unable to parse metadata")]
    MetadataParsingError,
    #[error("Unable to retrieve success based routing config")]
    SuccessBasedRoutingConfigError,
    #[error("Params not found in success based routing config")]
    SuccessBasedRoutingParamsNotFoundError,
    #[error("Unable to calculate success based routing config from dynamic routing service")]
    SuccessRateCalculationError,
    #[error("Success rate client from dynamic routing gRPC service not initialized")]
    SuccessRateClientInitializationError,
    #[error("Elimination client from dynamic routing gRPC service not initialized")]
    EliminationClientInitializationError,
    #[error("Unable to analyze elimination routing config from dynamic routing service")]
    EliminationRoutingCalculationError,
    #[error("Params not found in elimination based routing config")]
    EliminationBasedRoutingParamsNotFoundError,
    #[error("Unable to retrieve elimination based routing config")]
    EliminationRoutingConfigError,
    #[error(
        "Invalid elimination based connector label received from dynamic routing service: '{0}'"
    )]
    InvalidEliminationBasedConnectorLabel(String),
    #[error("Unable to convert from '{from}' to '{to}'")]
    GenericConversionError { from: String, to: String },
    #[error("Invalid success based connector label received from dynamic routing service: '{0}'")]
    InvalidSuccessBasedConnectorLabel(String),
    #[error("unable to find '{field}'")]
    GenericNotFoundError { field: String },
    #[error("Unable to deserialize from '{from}' to '{to}'")]
    DeserializationError { from: String, to: String },
    #[error("Unable to retrieve contract based routing config")]
    ContractBasedRoutingConfigError,
    #[error("Params not found in contract based routing config")]
    ContractBasedRoutingParamsNotFoundError,
    #[error("Unable to calculate contract score from dynamic routing service: '{err}'")]
    ContractScoreCalculationError { err: String },
    #[error("Unable to update contract score on dynamic routing service")]
    ContractScoreUpdationError,
    #[error("contract routing client from dynamic routing gRPC service not initialized")]
    ContractRoutingClientInitializationError,
    #[error("Invalid contract based connector label received from dynamic routing service: '{0}'")]
    InvalidContractBasedConnectorLabel(String),
    #[error("Failed to perform routing in open_router")]
    OpenRouterCallFailed,
    #[error("Error from open_router: {0}")]
    OpenRouterError(String),
    #[error("Decision engine responded with validation error: {0}")]
    DecisionEngineValidationError(String),
    #[error("Invalid transaction type")]
    InvalidTransactionType,
    #[error("Routing events error: {message}, status code: {status_code}")]
    RoutingEventsError { message: String, status_code: u16 },
}

#[derive(Debug, Clone, thiserror::Error)]
pub enum ConditionalConfigError {
    #[error("failed to fetch the fallback config for the merchant")]
    FallbackConfigFetchFailed,
    #[error("The lock on the DSL cache is most probably poisoned")]
    DslCachePoisoned,
    #[error("Merchant routing algorithm not found in cache")]
    CacheMiss,
    #[error("Expected DSL to be saved in DB but did not find")]
    DslMissingInDb,
    #[error("Unable to parse DSL from JSON")]
    DslParsingError,
    #[error("Failed to initialize DSL backend")]
    DslBackendInitError,
    #[error("Error executing the DSL")]
    DslExecutionError,
    #[error("Error constructing the Input")]
    InputConstructionError,
}

#[derive(Debug, thiserror::Error)]
pub enum NetworkTokenizationError {
    #[error("Failed to save network token in vault")]
    SaveNetworkTokenFailed,
    #[error("Failed to fetch network token details from vault")]
    FetchNetworkTokenFailed,
    #[error("Failed to encode network token vault request")]
    RequestEncodingFailed,
    #[error("Failed to deserialize network token service response")]
    ResponseDeserializationFailed,
    #[error("Failed to delete network token")]
    DeleteNetworkTokenFailed,
    #[error("Network token service not configured")]
    NetworkTokenizationServiceNotConfigured,
    #[error("Failed while calling Network Token Service API")]
    ApiError,
    #[error("Network Tokenization is not enabled for profile")]
    NetworkTokenizationNotEnabledForProfile,
    #[error("Network Tokenization is not supported for {message}")]
    NotSupported { message: String },
    #[error("Failed to encrypt the NetworkToken payment method details")]
    NetworkTokenDetailsEncryptionFailed,
}

#[derive(Debug, thiserror::Error)]
pub enum BulkNetworkTokenizationError {
    #[error("Failed to validate card details")]
    CardValidationFailed,
    #[error("Failed to validate payment method details")]
    PaymentMethodValidationFailed,
    #[error("Failed to assign a customer to the card")]
    CustomerAssignmentFailed,
    #[error("Failed to perform BIN lookup for the card")]
    BinLookupFailed,
    #[error("Failed to tokenize the card details with the network")]
    NetworkTokenizationFailed,
    #[error("Failed to store the card details in locker")]
    VaultSaveFailed,
    #[error("Failed to create a payment method entry")]
    PaymentMethodCreationFailed,
    #[error("Failed to update the payment method")]
    PaymentMethodUpdationFailed,
}

#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
#[derive(Debug, thiserror::Error)]
pub enum RevenueRecoveryError {
    #[error("Failed to fetch payment intent")]
    PaymentIntentFetchFailed,
    #[error("Failed to fetch payment attempt")]
    PaymentAttemptFetchFailed,
    #[error("Failed to fetch payment attempt")]
    PaymentAttemptIdNotFound,
    #[error("Failed to get revenue recovery invoice webhook")]
    InvoiceWebhookProcessingFailed,
    #[error("Failed to get revenue recovery invoice transaction")]
    TransactionWebhookProcessingFailed,
    #[error("Failed to create payment intent")]
    PaymentIntentCreateFailed,
    #[error("Source verification failed for billing connector")]
    WebhookAuthenticationFailed,
    #[error("Payment merchant connector account not found using account reference id")]
    PaymentMerchantConnectorAccountNotFound,
    #[error("Failed to fetch primitive date_time")]
    ScheduleTimeFetchFailed,
    #[error("Failed to create process tracker")]
    ProcessTrackerCreationError,
    #[error("Failed to get the response from process tracker")]
    ProcessTrackerResponseError,
    #[error("Billing connector psync call failed")]
    BillingConnectorPaymentsSyncFailed,
    #[error("Billing connector invoice sync call failed")]
    BillingConnectorInvoiceSyncFailed,
    #[error("Failed to fetch connector customer ID")]
    CustomerIdNotFound,
    #[error("Failed to get the retry count for payment intent")]
    RetryCountFetchFailed,
    #[error("Failed to get the billing threshold retry count")]
    BillingThresholdRetryCountFetchFailed,
    #[error("Failed to get the retry algorithm type")]
    RetryAlgorithmTypeNotFound,
    #[error("Failed to update the retry algorithm type")]
    RetryAlgorithmUpdationFailed,
    #[error("Failed to create the revenue recovery attempt data")]
    RevenueRecoveryAttemptDataCreateFailed,
    #[error("Failed to insert the revenue recovery payment method data in redis")]
    RevenueRecoveryRedisInsertFailed,
}