// File: crates/api_models/src/analytics/outgoing_webhook_event.rs

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct OutgoingWebhookLogsRequest {
    pub payment_id: common_utils::id_type::PaymentId,
    pub event_id: Option<String>,
    pub refund_id: Option<String>,
    pub dispute_id: Option<String>,
    pub mandate_id: Option<String>,
    pub payment_method_id: Option<String>,
    pub attempt_id: Option<String>,
}


// File: crates/api_models/src/analytics/refunds.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use common_utils::id_type;

use crate::enums::{Currency, RefundStatus};

#[derive(
    Clone,
    Copy,
    Debug,
    Hash,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
)]
// TODO RefundType api_models_oss need to mapped to storage_model
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RefundType {
    InstantRefund,
    RegularRefund,
    RetryRefund,
}

use super::{ForexMetric, NameDescription, TimeRange};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct RefundFilters {
    #[serde(default)]
    pub currency: Vec<Currency>,
    #[serde(default)]
    pub refund_status: Vec<RefundStatus>,
    #[serde(default)]
    pub connector: Vec<String>,
    #[serde(default)]
    pub refund_type: Vec<RefundType>,
    #[serde(default)]
    pub profile_id: Vec<id_type::ProfileId>,
    #[serde(default)]
    pub refund_reason: Vec<String>,
    #[serde(default)]
    pub refund_error_message: Vec<String>,
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RefundDimensions {
    Currency,
    RefundStatus,
    Connector,
    RefundType,
    ProfileId,
    RefundReason,
    RefundErrorMessage,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum RefundMetrics {
    RefundSuccessRate,
    RefundCount,
    RefundSuccessCount,
    RefundProcessedAmount,
    SessionizedRefundSuccessRate,
    SessionizedRefundCount,
    SessionizedRefundSuccessCount,
    SessionizedRefundProcessedAmount,
    SessionizedRefundReason,
    SessionizedRefundErrorMessage,
}

#[derive(Debug, Default, serde::Serialize)]
pub struct ReasonsResult {
    pub reason: String,
    pub count: i64,
    pub percentage: f64,
}

#[derive(Debug, Default, serde::Serialize)]
pub struct ErrorMessagesResult {
    pub error_message: String,
    pub count: i64,
    pub percentage: f64,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum RefundDistributions {
    #[strum(serialize = "refund_reason")]
    SessionizedRefundReason,
    #[strum(serialize = "refund_error_message")]
    SessionizedRefundErrorMessage,
}
impl ForexMetric for RefundMetrics {
    fn is_forex_metric(&self) -> bool {
        matches!(
            self,
            Self::RefundProcessedAmount | Self::SessionizedRefundProcessedAmount
        )
    }
}

pub mod metric_behaviour {
    pub struct RefundSuccessRate;
    pub struct RefundCount;
    pub struct RefundSuccessCount;
    pub struct RefundProcessedAmount;
}

impl From<RefundMetrics> for NameDescription {
    fn from(value: RefundMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<RefundDimensions> for NameDescription {
    fn from(value: RefundDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct RefundMetricsBucketIdentifier {
    pub currency: Option<Currency>,
    pub refund_status: Option<String>,
    pub connector: Option<String>,
    pub refund_type: Option<String>,
    pub profile_id: Option<String>,
    pub refund_reason: Option<String>,
    pub refund_error_message: Option<String>,
    #[serde(rename = "time_range")]
    pub time_bucket: TimeRange,
    #[serde(rename = "time_bucket")]
    #[serde(with = "common_utils::custom_serde::iso8601custom")]
    pub start_time: time::PrimitiveDateTime,
}

impl Hash for RefundMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.currency.hash(state);
        self.refund_status.hash(state);
        self.connector.hash(state);
        self.refund_type.hash(state);
        self.profile_id.hash(state);
        self.refund_reason.hash(state);
        self.refund_error_message.hash(state);
        self.time_bucket.hash(state);
    }
}
impl PartialEq for RefundMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

impl RefundMetricsBucketIdentifier {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        currency: Option<Currency>,
        refund_status: Option<String>,
        connector: Option<String>,
        refund_type: Option<String>,
        profile_id: Option<String>,
        refund_reason: Option<String>,
        refund_error_message: Option<String>,
        normalized_time_range: TimeRange,
    ) -> Self {
        Self {
            currency,
            refund_status,
            connector,
            refund_type,
            profile_id,
            refund_reason,
            refund_error_message,
            time_bucket: normalized_time_range,
            start_time: normalized_time_range.start_time,
        }
    }
}
#[derive(Debug, serde::Serialize)]
pub struct RefundMetricsBucketValue {
    pub successful_refunds: Option<u32>,
    pub total_refunds: Option<u32>,
    pub refund_success_rate: Option<f64>,
    pub refund_count: Option<u64>,
    pub refund_success_count: Option<u64>,
    pub refund_processed_amount: Option<u64>,
    pub refund_processed_amount_in_usd: Option<u64>,
    pub refund_processed_count: Option<u64>,
    pub refund_reason_distribution: Option<Vec<ReasonsResult>>,
    pub refund_error_message_distribution: Option<Vec<ErrorMessagesResult>>,
    pub refund_reason_count: Option<u64>,
    pub refund_error_message_count: Option<u64>,
}
#[derive(Debug, serde::Serialize)]
pub struct RefundMetricsBucketResponse {
    #[serde(flatten)]
    pub values: RefundMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: RefundMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/payments.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use common_utils::id_type;

use super::{ForexMetric, NameDescription, TimeRange};
use crate::enums::{
    AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod,
    PaymentMethodType, RoutingApproach,
};

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct PaymentFilters {
    #[serde(default)]
    pub currency: Vec<Currency>,
    #[serde(default)]
    pub status: Vec<AttemptStatus>,
    #[serde(default)]
    pub connector: Vec<Connector>,
    #[serde(default)]
    pub auth_type: Vec<AuthenticationType>,
    #[serde(default)]
    pub payment_method: Vec<PaymentMethod>,
    #[serde(default)]
    pub payment_method_type: Vec<PaymentMethodType>,
    #[serde(default)]
    pub client_source: Vec<String>,
    #[serde(default)]
    pub client_version: Vec<String>,
    #[serde(default)]
    pub card_network: Vec<CardNetwork>,
    #[serde(default)]
    pub profile_id: Vec<id_type::ProfileId>,
    #[serde(default)]
    pub merchant_id: Vec<id_type::MerchantId>,
    #[serde(default)]
    pub card_last_4: Vec<String>,
    #[serde(default)]
    pub card_issuer: Vec<String>,
    #[serde(default)]
    pub error_reason: Vec<String>,
    #[serde(default)]
    pub first_attempt: Vec<bool>,
    #[serde(default)]
    pub routing_approach: Vec<RoutingApproach>,
    #[serde(default)]
    pub signature_network: Vec<String>,
    #[serde(default)]
    pub is_issuer_regulated: Vec<bool>,
    #[serde(default)]
    pub is_debit_routed: Vec<bool>,
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PaymentDimensions {
    // Do not change the order of these enums
    // Consult the Dashboard FE folks since these also affects the order of metrics on FE
    Connector,
    PaymentMethod,
    PaymentMethodType,
    Currency,
    #[strum(serialize = "authentication_type")]
    #[serde(rename = "authentication_type")]
    AuthType,
    #[strum(serialize = "status")]
    #[serde(rename = "status")]
    PaymentStatus,
    ClientSource,
    ClientVersion,
    ProfileId,
    CardNetwork,
    MerchantId,
    #[strum(serialize = "card_last_4")]
    #[serde(rename = "card_last_4")]
    CardLast4,
    CardIssuer,
    ErrorReason,
    RoutingApproach,
    SignatureNetwork,
    IsIssuerRegulated,
    IsDebitRouted,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum PaymentMetrics {
    PaymentSuccessRate,
    PaymentCount,
    PaymentSuccessCount,
    PaymentProcessedAmount,
    AvgTicketSize,
    RetriesCount,
    ConnectorSuccessRate,
    DebitRouting,
    SessionizedPaymentSuccessRate,
    SessionizedPaymentCount,
    SessionizedPaymentSuccessCount,
    SessionizedPaymentProcessedAmount,
    SessionizedAvgTicketSize,
    SessionizedRetriesCount,
    SessionizedConnectorSuccessRate,
    SessionizedDebitRouting,
    PaymentsDistribution,
    FailureReasons,
}

impl ForexMetric for PaymentMetrics {
    fn is_forex_metric(&self) -> bool {
        matches!(
            self,
            Self::PaymentProcessedAmount
                | Self::AvgTicketSize
                | Self::DebitRouting
                | Self::SessionizedPaymentProcessedAmount
                | Self::SessionizedAvgTicketSize
                | Self::SessionizedDebitRouting,
        )
    }
}
#[derive(Debug, Default, serde::Serialize)]
pub struct ErrorResult {
    pub reason: String,
    pub count: i64,
    pub percentage: f64,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum PaymentDistributions {
    #[strum(serialize = "error_message")]
    PaymentErrorMessage,
}

pub mod metric_behaviour {
    pub struct PaymentSuccessRate;
    pub struct PaymentCount;
    pub struct PaymentSuccessCount;
    pub struct PaymentProcessedAmount;
    pub struct AvgTicketSize;
}

impl From<PaymentMetrics> for NameDescription {
    fn from(value: PaymentMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<PaymentDimensions> for NameDescription {
    fn from(value: PaymentDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct PaymentMetricsBucketIdentifier {
    pub currency: Option<Currency>,
    pub status: Option<AttemptStatus>,
    pub connector: Option<String>,
    #[serde(rename = "authentication_type")]
    pub auth_type: Option<AuthenticationType>,
    pub payment_method: Option<String>,
    pub payment_method_type: Option<String>,
    pub client_source: Option<String>,
    pub client_version: Option<String>,
    pub profile_id: Option<String>,
    pub card_network: Option<String>,
    pub merchant_id: Option<String>,
    pub card_last_4: Option<String>,
    pub card_issuer: Option<String>,
    pub error_reason: Option<String>,
    pub routing_approach: Option<RoutingApproach>,
    pub signature_network: Option<String>,
    pub is_issuer_regulated: Option<bool>,
    pub is_debit_routed: Option<bool>,
    #[serde(rename = "time_range")]
    pub time_bucket: TimeRange,
    // Coz FE sucks
    #[serde(rename = "time_bucket")]
    #[serde(with = "common_utils::custom_serde::iso8601custom")]
    pub start_time: time::PrimitiveDateTime,
}

impl PaymentMetricsBucketIdentifier {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        currency: Option<Currency>,
        status: Option<AttemptStatus>,
        connector: Option<String>,
        auth_type: Option<AuthenticationType>,
        payment_method: Option<String>,
        payment_method_type: Option<String>,
        client_source: Option<String>,
        client_version: Option<String>,
        profile_id: Option<String>,
        card_network: Option<String>,
        merchant_id: Option<String>,
        card_last_4: Option<String>,
        card_issuer: Option<String>,
        error_reason: Option<String>,
        routing_approach: Option<RoutingApproach>,
        signature_network: Option<String>,
        is_issuer_regulated: Option<bool>,
        is_debit_routed: Option<bool>,
        normalized_time_range: TimeRange,
    ) -> Self {
        Self {
            currency,
            status,
            connector,
            auth_type,
            payment_method,
            payment_method_type,
            client_source,
            client_version,
            profile_id,
            card_network,
            merchant_id,
            card_last_4,
            card_issuer,
            error_reason,
            routing_approach,
            signature_network,
            is_issuer_regulated,
            is_debit_routed,
            time_bucket: normalized_time_range,
            start_time: normalized_time_range.start_time,
        }
    }
}

impl Hash for PaymentMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.currency.hash(state);
        self.status.map(|i| i.to_string()).hash(state);
        self.connector.hash(state);
        self.auth_type.map(|i| i.to_string()).hash(state);
        self.payment_method.hash(state);
        self.payment_method_type.hash(state);
        self.client_source.hash(state);
        self.client_version.hash(state);
        self.profile_id.hash(state);
        self.card_network.hash(state);
        self.merchant_id.hash(state);
        self.card_last_4.hash(state);
        self.card_issuer.hash(state);
        self.error_reason.hash(state);
        self.routing_approach
            .clone()
            .map(|i| i.to_string())
            .hash(state);
        self.signature_network.hash(state);
        self.is_issuer_regulated.hash(state);
        self.is_debit_routed.hash(state);
        self.time_bucket.hash(state);
    }
}

impl PartialEq for PaymentMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

#[derive(Debug, serde::Serialize)]
pub struct PaymentMetricsBucketValue {
    pub payment_success_rate: Option<f64>,
    pub payment_count: Option<u64>,
    pub payment_success_count: Option<u64>,
    pub payment_processed_amount: Option<u64>,
    pub payment_processed_amount_in_usd: Option<u64>,
    pub payment_processed_count: Option<u64>,
    pub payment_processed_amount_without_smart_retries: Option<u64>,
    pub payment_processed_amount_without_smart_retries_usd: Option<u64>,
    pub payment_processed_count_without_smart_retries: Option<u64>,
    pub avg_ticket_size: Option<f64>,
    pub payment_error_message: Option<Vec<ErrorResult>>,
    pub retries_count: Option<u64>,
    pub retries_amount_processed: Option<u64>,
    pub connector_success_rate: Option<f64>,
    pub payments_success_rate_distribution: Option<f64>,
    pub payments_success_rate_distribution_without_smart_retries: Option<f64>,
    pub payments_success_rate_distribution_with_only_retries: Option<f64>,
    pub payments_failure_rate_distribution: Option<f64>,
    pub payments_failure_rate_distribution_without_smart_retries: Option<f64>,
    pub payments_failure_rate_distribution_with_only_retries: Option<f64>,
    pub failure_reason_count: Option<u64>,
    pub failure_reason_count_without_smart_retries: Option<u64>,
    pub debit_routed_transaction_count: Option<u64>,
    pub debit_routing_savings: Option<u64>,
    pub debit_routing_savings_in_usd: Option<u64>,
}

#[derive(Debug, serde::Serialize)]
pub struct MetricsBucketResponse {
    #[serde(flatten)]
    pub values: PaymentMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: PaymentMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/auth_events.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use common_enums::{
    AuthenticationConnectors, AuthenticationStatus, Currency, DecoupledAuthenticationType,
    TransactionStatus,
};

use super::{NameDescription, TimeRange};

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct AuthEventFilters {
    #[serde(default)]
    pub authentication_status: Vec<AuthenticationStatus>,
    #[serde(default)]
    pub trans_status: Vec<TransactionStatus>,
    #[serde(default)]
    pub authentication_type: Vec<DecoupledAuthenticationType>,
    #[serde(default)]
    pub error_message: Vec<String>,
    #[serde(default)]
    pub authentication_connector: Vec<AuthenticationConnectors>,
    #[serde(default)]
    pub message_version: Vec<String>,
    #[serde(default)]
    pub platform: Vec<String>,
    #[serde(default)]
    pub acs_reference_number: Vec<String>,
    #[serde(default)]
    pub mcc: Vec<String>,
    #[serde(default)]
    pub currency: Vec<Currency>,
    #[serde(default)]
    pub merchant_country: Vec<String>,
    #[serde(default)]
    pub billing_country: Vec<String>,
    #[serde(default)]
    pub shipping_country: Vec<String>,
    #[serde(default)]
    pub issuer_country: Vec<String>,
    #[serde(default)]
    pub earliest_supported_version: Vec<String>,
    #[serde(default)]
    pub latest_supported_version: Vec<String>,
    #[serde(default)]
    pub whitelist_decision: Vec<bool>,
    #[serde(default)]
    pub device_manufacturer: Vec<String>,
    #[serde(default)]
    pub device_type: Vec<String>,
    #[serde(default)]
    pub device_brand: Vec<String>,
    #[serde(default)]
    pub device_os: Vec<String>,
    #[serde(default)]
    pub device_display: Vec<String>,
    #[serde(default)]
    pub browser_name: Vec<String>,
    #[serde(default)]
    pub browser_version: Vec<String>,
    #[serde(default)]
    pub issuer_id: Vec<String>,
    #[serde(default)]
    pub scheme_name: Vec<String>,
    #[serde(default)]
    pub exemption_requested: Vec<bool>,
    #[serde(default)]
    pub exemption_accepted: Vec<bool>,
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum AuthEventDimensions {
    AuthenticationStatus,
    #[strum(serialize = "trans_status")]
    #[serde(rename = "trans_status")]
    TransactionStatus,
    AuthenticationType,
    ErrorMessage,
    AuthenticationConnector,
    MessageVersion,
    AcsReferenceNumber,
    Platform,
    Mcc,
    Currency,
    MerchantCountry,
    BillingCountry,
    ShippingCountry,
    IssuerCountry,
    EarliestSupportedVersion,
    LatestSupportedVersion,
    WhitelistDecision,
    DeviceManufacturer,
    DeviceType,
    DeviceBrand,
    DeviceOs,
    DeviceDisplay,
    BrowserName,
    BrowserVersion,
    IssuerId,
    SchemeName,
    ExemptionRequested,
    ExemptionAccepted,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AuthEventMetrics {
    AuthenticationCount,
    AuthenticationAttemptCount,
    AuthenticationSuccessCount,
    ChallengeFlowCount,
    FrictionlessFlowCount,
    FrictionlessSuccessCount,
    ChallengeAttemptCount,
    ChallengeSuccessCount,
    AuthenticationErrorMessage,
    AuthenticationFunnel,
    AuthenticationExemptionApprovedCount,
    AuthenticationExemptionRequestedCount,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
pub enum AuthEventFlows {
    IncomingWebhookReceive,
    PaymentsExternalAuthentication,
}

pub mod metric_behaviour {
    pub struct AuthenticationCount;
    pub struct AuthenticationAttemptCount;
    pub struct AuthenticationSuccessCount;
    pub struct ChallengeFlowCount;
    pub struct FrictionlessFlowCount;
    pub struct FrictionlessSuccessCount;
    pub struct ChallengeAttemptCount;
    pub struct ChallengeSuccessCount;
    pub struct AuthenticationErrorMessage;
    pub struct AuthenticationFunnel;
}

impl From<AuthEventMetrics> for NameDescription {
    fn from(value: AuthEventMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<AuthEventDimensions> for NameDescription {
    fn from(value: AuthEventDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct AuthEventMetricsBucketIdentifier {
    pub authentication_status: Option<AuthenticationStatus>,
    pub trans_status: Option<TransactionStatus>,
    pub authentication_type: Option<DecoupledAuthenticationType>,
    pub error_message: Option<String>,
    pub authentication_connector: Option<AuthenticationConnectors>,
    pub message_version: Option<String>,
    pub acs_reference_number: Option<String>,
    pub mcc: Option<String>,
    pub currency: Option<Currency>,
    pub merchant_country: Option<String>,
    pub billing_country: Option<String>,
    pub shipping_country: Option<String>,
    pub issuer_country: Option<String>,
    pub earliest_supported_version: Option<String>,
    pub latest_supported_version: Option<String>,
    pub whitelist_decision: Option<bool>,
    pub device_manufacturer: Option<String>,
    pub device_type: Option<String>,
    pub device_brand: Option<String>,
    pub device_os: Option<String>,
    pub device_display: Option<String>,
    pub browser_name: Option<String>,
    pub browser_version: Option<String>,
    pub issuer_id: Option<String>,
    pub scheme_name: Option<String>,
    pub exemption_requested: Option<bool>,
    pub exemption_accepted: Option<bool>,
    #[serde(rename = "time_range")]
    pub time_bucket: TimeRange,
    #[serde(rename = "time_bucket")]
    #[serde(with = "common_utils::custom_serde::iso8601custom")]
    pub start_time: time::PrimitiveDateTime,
}

impl AuthEventMetricsBucketIdentifier {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        authentication_status: Option<AuthenticationStatus>,
        trans_status: Option<TransactionStatus>,
        authentication_type: Option<DecoupledAuthenticationType>,
        error_message: Option<String>,
        authentication_connector: Option<AuthenticationConnectors>,
        message_version: Option<String>,
        acs_reference_number: Option<String>,
        mcc: Option<String>,
        currency: Option<Currency>,
        merchant_country: Option<String>,
        billing_country: Option<String>,
        shipping_country: Option<String>,
        issuer_country: Option<String>,
        earliest_supported_version: Option<String>,
        latest_supported_version: Option<String>,
        whitelist_decision: Option<bool>,
        device_manufacturer: Option<String>,
        device_type: Option<String>,
        device_brand: Option<String>,
        device_os: Option<String>,
        device_display: Option<String>,
        browser_name: Option<String>,
        browser_version: Option<String>,
        issuer_id: Option<String>,
        scheme_name: Option<String>,
        exemption_requested: Option<bool>,
        exemption_accepted: Option<bool>,
        normalized_time_range: TimeRange,
    ) -> Self {
        Self {
            authentication_status,
            trans_status,
            authentication_type,
            error_message,
            authentication_connector,
            message_version,
            acs_reference_number,
            mcc,
            currency,
            merchant_country,
            billing_country,
            shipping_country,
            issuer_country,
            earliest_supported_version,
            latest_supported_version,
            whitelist_decision,
            device_manufacturer,
            device_type,
            device_brand,
            device_os,
            device_display,
            browser_name,
            browser_version,
            issuer_id,
            scheme_name,
            exemption_requested,
            exemption_accepted,
            time_bucket: normalized_time_range,
            start_time: normalized_time_range.start_time,
        }
    }
}

impl Hash for AuthEventMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.authentication_status.hash(state);
        self.trans_status.hash(state);
        self.authentication_type.hash(state);
        self.authentication_connector.hash(state);
        self.message_version.hash(state);
        self.acs_reference_number.hash(state);
        self.error_message.hash(state);
        self.mcc.hash(state);
        self.currency.hash(state);
        self.merchant_country.hash(state);
        self.billing_country.hash(state);
        self.shipping_country.hash(state);
        self.issuer_country.hash(state);
        self.earliest_supported_version.hash(state);
        self.latest_supported_version.hash(state);
        self.whitelist_decision.hash(state);
        self.device_manufacturer.hash(state);
        self.device_type.hash(state);
        self.device_brand.hash(state);
        self.device_os.hash(state);
        self.device_display.hash(state);
        self.browser_name.hash(state);
        self.browser_version.hash(state);
        self.issuer_id.hash(state);
        self.scheme_name.hash(state);
        self.exemption_requested.hash(state);
        self.exemption_accepted.hash(state);
        self.time_bucket.hash(state);
    }
}

impl PartialEq for AuthEventMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

#[derive(Debug, serde::Serialize)]
pub struct AuthEventMetricsBucketValue {
    pub authentication_count: Option<u64>,
    pub authentication_attempt_count: Option<u64>,
    pub authentication_success_count: Option<u64>,
    pub challenge_flow_count: Option<u64>,
    pub challenge_attempt_count: Option<u64>,
    pub challenge_success_count: Option<u64>,
    pub frictionless_flow_count: Option<u64>,
    pub frictionless_success_count: Option<u64>,
    pub error_message_count: Option<u64>,
    pub authentication_funnel: Option<u64>,
    pub authentication_exemption_approved_count: Option<u64>,
    pub authentication_exemption_requested_count: Option<u64>,
}

#[derive(Debug, serde::Serialize)]
pub struct MetricsBucketResponse {
    #[serde(flatten)]
    pub values: AuthEventMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: AuthEventMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/disputes.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use super::{ForexMetric, NameDescription, TimeRange};
use crate::enums::{Currency, DisputeStage};

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum DisputeMetrics {
    DisputeStatusMetric,
    TotalAmountDisputed,
    TotalDisputeLostAmount,
    SessionizedDisputeStatusMetric,
    SessionizedTotalAmountDisputed,
    SessionizedTotalDisputeLostAmount,
}
impl ForexMetric for DisputeMetrics {
    fn is_forex_metric(&self) -> bool {
        matches!(
            self,
            Self::TotalAmountDisputed | Self::TotalDisputeLostAmount
        )
    }
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum DisputeDimensions {
    // Do not change the order of these enums
    // Consult the Dashboard FE folks since these also affects the order of metrics on FE
    Connector,
    DisputeStage,
    Currency,
}

impl From<DisputeDimensions> for NameDescription {
    fn from(value: DisputeDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<DisputeMetrics> for NameDescription {
    fn from(value: DisputeMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct DisputeFilters {
    #[serde(default)]
    pub dispute_stage: Vec<DisputeStage>,
    #[serde(default)]
    pub connector: Vec<String>,
    #[serde(default)]
    pub currency: Vec<Currency>,
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct DisputeMetricsBucketIdentifier {
    pub dispute_stage: Option<DisputeStage>,
    pub connector: Option<String>,
    pub currency: Option<Currency>,
    #[serde(rename = "time_range")]
    pub time_bucket: TimeRange,
    #[serde(rename = "time_bucket")]
    #[serde(with = "common_utils::custom_serde::iso8601custom")]
    pub start_time: time::PrimitiveDateTime,
}

impl Hash for DisputeMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.dispute_stage.hash(state);
        self.connector.hash(state);
        self.currency.hash(state);
        self.time_bucket.hash(state);
    }
}
impl PartialEq for DisputeMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

impl DisputeMetricsBucketIdentifier {
    pub fn new(
        dispute_stage: Option<DisputeStage>,
        connector: Option<String>,
        currency: Option<Currency>,
        normalized_time_range: TimeRange,
    ) -> Self {
        Self {
            dispute_stage,
            connector,
            currency,
            time_bucket: normalized_time_range,
            start_time: normalized_time_range.start_time,
        }
    }
}

#[derive(Debug, serde::Serialize)]
pub struct DisputeMetricsBucketValue {
    pub disputes_challenged: Option<u64>,
    pub disputes_won: Option<u64>,
    pub disputes_lost: Option<u64>,
    pub disputed_amount: Option<u64>,
    pub dispute_lost_amount: Option<u64>,
    pub total_dispute: Option<u64>,
}
#[derive(Debug, serde::Serialize)]
pub struct DisputeMetricsBucketResponse {
    #[serde(flatten)]
    pub values: DisputeMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: DisputeMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/frm.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use common_enums::enums::FraudCheckStatus;

#[derive(
    Clone,
    Copy,
    Debug,
    Default,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FrmTransactionType {
    #[default]
    PreFrm,
    PostFrm,
}

use super::{NameDescription, TimeRange};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct FrmFilters {
    #[serde(default)]
    pub frm_status: Vec<FraudCheckStatus>,
    #[serde(default)]
    pub frm_name: Vec<String>,
    #[serde(default)]
    pub frm_transaction_type: Vec<FrmTransactionType>,
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FrmDimensions {
    FrmStatus,
    FrmName,
    FrmTransactionType,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum FrmMetrics {
    FrmTriggeredAttempts,
    FrmBlockedRate,
}

pub mod metric_behaviour {
    pub struct FrmTriggeredAttempts;
    pub struct FrmBlockRate;
}

impl From<FrmMetrics> for NameDescription {
    fn from(value: FrmMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<FrmDimensions> for NameDescription {
    fn from(value: FrmDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct FrmMetricsBucketIdentifier {
    pub frm_status: Option<String>,
    pub frm_name: Option<String>,
    pub frm_transaction_type: Option<String>,
    #[serde(rename = "time_range")]
    pub time_bucket: TimeRange,
    #[serde(rename = "time_bucket")]
    #[serde(with = "common_utils::custom_serde::iso8601custom")]
    pub start_time: time::PrimitiveDateTime,
}

impl Hash for FrmMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.frm_status.hash(state);
        self.frm_name.hash(state);
        self.frm_transaction_type.hash(state);
        self.time_bucket.hash(state);
    }
}

impl PartialEq for FrmMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

impl FrmMetricsBucketIdentifier {
    pub fn new(
        frm_status: Option<String>,
        frm_name: Option<String>,
        frm_transaction_type: Option<String>,
        normalized_time_range: TimeRange,
    ) -> Self {
        Self {
            frm_status,
            frm_name,
            frm_transaction_type,
            time_bucket: normalized_time_range,
            start_time: normalized_time_range.start_time,
        }
    }
}

#[derive(Debug, serde::Serialize)]
pub struct FrmMetricsBucketValue {
    pub frm_triggered_attempts: Option<u64>,
    pub frm_blocked_rate: Option<f64>,
}

#[derive(Debug, serde::Serialize)]
pub struct FrmMetricsBucketResponse {
    #[serde(flatten)]
    pub values: FrmMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: FrmMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/routing_events.rs

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct RoutingEventsRequest {
    pub payment_id: common_utils::id_type::PaymentId,
    pub refund_id: Option<String>,
    pub dispute_id: Option<String>,
}


// File: crates/api_models/src/analytics/connector_events.rs

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct ConnectorEventsRequest {
    pub payment_id: common_utils::id_type::PaymentId,
    pub refund_id: Option<String>,
    pub dispute_id: Option<String>,
}


// File: crates/api_models/src/analytics/active_payments.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use super::NameDescription;

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ActivePaymentsMetrics {
    ActivePayments,
}

pub mod metric_behaviour {
    pub struct ActivePayments;
}

impl From<ActivePaymentsMetrics> for NameDescription {
    fn from(value: ActivePaymentsMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct ActivePaymentsMetricsBucketIdentifier {
    pub time_bucket: Option<String>,
}

impl ActivePaymentsMetricsBucketIdentifier {
    pub fn new(time_bucket: Option<String>) -> Self {
        Self { time_bucket }
    }
}

impl Hash for ActivePaymentsMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.time_bucket.hash(state);
    }
}

impl PartialEq for ActivePaymentsMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

#[derive(Debug, serde::Serialize)]
pub struct ActivePaymentsMetricsBucketValue {
    pub active_payments: Option<u64>,
}

#[derive(Debug, serde::Serialize)]
pub struct MetricsBucketResponse {
    #[serde(flatten)]
    pub values: ActivePaymentsMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: ActivePaymentsMetricsBucketIdentifier,
}


// File: crates/api_models/src/analytics/search.rs

use common_utils::{hashing::HashedString, types::TimeRange};
use masking::WithType;
use serde_json::Value;

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct SearchFilters {
    pub payment_method: Option<Vec<String>>,
    pub currency: Option<Vec<String>>,
    pub status: Option<Vec<String>>,
    pub customer_email: Option<Vec<HashedString<common_utils::pii::EmailStrategy>>>,
    pub search_tags: Option<Vec<HashedString<WithType>>>,
    pub connector: Option<Vec<String>>,
    pub payment_method_type: Option<Vec<String>>,
    pub card_network: Option<Vec<String>>,
    pub card_last_4: Option<Vec<String>>,
    pub payment_id: Option<Vec<String>>,
    pub amount: Option<Vec<u64>>,
    pub customer_id: Option<Vec<String>>,
}
impl SearchFilters {
    pub fn is_all_none(&self) -> bool {
        self.payment_method.is_none()
            && self.currency.is_none()
            && self.status.is_none()
            && self.customer_email.is_none()
            && self.search_tags.is_none()
            && self.connector.is_none()
            && self.payment_method_type.is_none()
            && self.card_network.is_none()
            && self.card_last_4.is_none()
            && self.payment_id.is_none()
            && self.amount.is_none()
            && self.customer_id.is_none()
    }
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetGlobalSearchRequest {
    pub query: String,
    #[serde(default)]
    pub filters: Option<SearchFilters>,
    #[serde(default)]
    pub time_range: Option<TimeRange>,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSearchRequest {
    pub offset: i64,
    pub count: i64,
    pub query: String,
    #[serde(default)]
    pub filters: Option<SearchFilters>,
    #[serde(default)]
    pub time_range: Option<TimeRange>,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSearchRequestWithIndex {
    pub index: SearchIndex,
    pub search_req: GetSearchRequest,
}

#[derive(
    Debug, strum::EnumIter, Clone, serde::Deserialize, serde::Serialize, Copy, Eq, PartialEq,
)]
#[serde(rename_all = "snake_case")]
pub enum SearchIndex {
    PaymentAttempts,
    PaymentIntents,
    Refunds,
    Disputes,
    Payouts,
    SessionizerPaymentAttempts,
    SessionizerPaymentIntents,
    SessionizerRefunds,
    SessionizerDisputes,
}

#[derive(Debug, strum::EnumIter, Clone, serde::Deserialize, serde::Serialize, Copy)]
pub enum SearchStatus {
    Success,
    Failure,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSearchResponse {
    pub count: u64,
    pub index: SearchIndex,
    pub hits: Vec<Value>,
    pub status: SearchStatus,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpenMsearchOutput {
    #[serde(default)]
    pub responses: Vec<OpensearchOutput>,
    pub error: Option<OpensearchErrorDetails>,
}

#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum OpensearchOutput {
    Success(OpensearchSuccess),
    Error(OpensearchError),
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchError {
    pub error: OpensearchErrorDetails,
    pub status: u16,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchErrorDetails {
    #[serde(rename = "type")]
    pub error_type: String,
    pub reason: String,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchSuccess {
    pub hits: OpensearchHits,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchHits {
    pub total: OpensearchResultsTotal,
    pub hits: Vec<OpensearchHit>,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchResultsTotal {
    pub value: u64,
}

#[derive(Debug, serde::Deserialize)]
pub struct OpensearchHit {
    #[serde(rename = "_source")]
    pub source: Value,
}


// File: crates/api_models/src/analytics/sdk_events.rs

use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use super::{NameDescription, TimeRange};

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SdkEventsRequest {
    pub payment_id: common_utils::id_type::PaymentId,
    pub time_range: TimeRange,
}

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct SdkEventFilters {
    #[serde(default)]
    pub payment_method: Vec<String>,
    #[serde(default)]
    pub platform: Vec<String>,
    #[serde(default)]
    pub browser_name: Vec<String>,
    #[serde(default)]
    pub source: Vec<String>,
    #[serde(default)]
    pub component: Vec<String>,
    #[serde(default)]
    pub payment_experience: Vec<String>,
}

#[derive(
    Debug,
    serde::Serialize,
    serde::Deserialize,
    strum::AsRefStr,
    PartialEq,
    PartialOrd,
    Eq,
    Ord,
    strum::Display,
    strum::EnumIter,
    Clone,
    Copy,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SdkEventDimensions {
    // Do not change the order of these enums
    // Consult the Dashboard FE folks since these also affects the order of metrics on FE
    PaymentMethod,
    Platform,
    BrowserName,
    Source,
    Component,
    PaymentExperience,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum SdkEventMetrics {
    PaymentAttempts,
    PaymentMethodsCallCount,
    SdkRenderedCount,
    SdkInitiatedCount,
    PaymentMethodSelectedCount,
    PaymentDataFilledCount,
    AveragePaymentTime,
    LoadTime,
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumIter,
    strum::AsRefStr,
)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SdkEventNames {
    OrcaElementsCalled,
    AppRendered,
    PaymentMethodChanged,
    PaymentDataFilled,
    PaymentAttempt,
    PaymentMethodsCall,
    ConfirmCall,
    SessionsCall,
    CustomerPaymentMethodsCall,
    RedirectingUser,
    DisplayBankTransferInfoPage,
    DisplayQrCodeInfoPage,
    AuthenticationCall,
    AuthenticationCallInit,
    ThreeDsMethodCall,
    ThreeDsMethodResult,
    ThreeDsMethod,
    LoaderChanged,
    DisplayThreeDsSdk,
    ThreeDsSdkInit,
    AreqParamsGeneration,
    ChallengePresented,
    ChallengeComplete,
}

pub mod metric_behaviour {
    pub struct PaymentAttempts;
    pub struct PaymentMethodsCallCount;
    pub struct SdkRenderedCount;
    pub struct SdkInitiatedCount;
    pub struct PaymentMethodSelectedCount;
    pub struct PaymentDataFilledCount;
    pub struct AveragePaymentTime;
    pub struct LoadTime;
}

impl From<SdkEventMetrics> for NameDescription {
    fn from(value: SdkEventMetrics) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

impl From<SdkEventDimensions> for NameDescription {
    fn from(value: SdkEventDimensions) -> Self {
        Self {
            name: value.to_string(),
            desc: String::new(),
        }
    }
}

#[derive(Debug, serde::Serialize, Eq)]
pub struct SdkEventMetricsBucketIdentifier {
    pub payment_method: Option<String>,
    pub platform: Option<String>,
    pub browser_name: Option<String>,
    pub source: Option<String>,
    pub component: Option<String>,
    pub payment_experience: Option<String>,
    pub time_bucket: Option<String>,
}

impl SdkEventMetricsBucketIdentifier {
    pub fn new(
        payment_method: Option<String>,
        platform: Option<String>,
        browser_name: Option<String>,
        source: Option<String>,
        component: Option<String>,
        payment_experience: Option<String>,
        time_bucket: Option<String>,
    ) -> Self {
        Self {
            payment_method,
            platform,
            browser_name,
            source,
            component,
            payment_experience,
            time_bucket,
        }
    }
}

impl Hash for SdkEventMetricsBucketIdentifier {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.payment_method.hash(state);
        self.platform.hash(state);
        self.browser_name.hash(state);
        self.source.hash(state);
        self.component.hash(state);
        self.payment_experience.hash(state);
        self.time_bucket.hash(state);
    }
}

impl PartialEq for SdkEventMetricsBucketIdentifier {
    fn eq(&self, other: &Self) -> bool {
        let mut left = DefaultHasher::new();
        self.hash(&mut left);
        let mut right = DefaultHasher::new();
        other.hash(&mut right);
        left.finish() == right.finish()
    }
}

#[derive(Debug, serde::Serialize)]
pub struct SdkEventMetricsBucketValue {
    pub payment_attempts: Option<u64>,
    pub payment_methods_call_count: Option<u64>,
    pub average_payment_time: Option<u64>,
    pub load_time: Option<u64>,
    pub sdk_rendered_count: Option<u64>,
    pub sdk_initiated_count: Option<u64>,
    pub payment_method_selected_count: Option<u64>,
    pub payment_data_filled_count: Option<u64>,
}

#[derive(Debug, serde::Serialize)]
pub struct MetricsBucketResponse {
    #[serde(flatten)]
    pub values: SdkEventMetricsBucketValue,
    #[serde(flatten)]
    pub dimensions: SdkEventMetricsBucketIdentifier,
}


// File: crates/router_env/Cargo.toml

[package]
name = "router_env"
description = "Environment of payment router: logger, basic config, its environment awareness."
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
license.workspace = true

[dependencies]
cargo_metadata = "0.18.1"
config = { version = "0.14.1", features = ["toml"] }
error-stack = "0.4.1"
gethostname = "0.4.3"
opentelemetry = { version = "0.27.1", default-features = false, features = ["internal-logs", "metrics", "trace"] }
opentelemetry-aws = { version = "0.15.0", default-features = false, features = ["internal-logs", "trace"] }
opentelemetry-otlp = { version = "0.27.0", default-features = false, features = ["grpc-tonic", "metrics", "trace"] }
opentelemetry_sdk = { version = "0.27.1", default-features = false, features = ["rt-tokio-current-thread", "metrics", "trace"] }
rustc-hash = "1.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_path_to_error = "0.1.17"
strum = { version = "0.26.3", features = ["derive"] }
time = { version = "0.3.41", default-features = false, features = ["formatting"] }
tokio = { version = "1.48.0" }
tracing = { workspace = true }
actix-web = { version = "4.11.0", optional = true }
tracing-actix-web = { version = "0.7.18", features = ["opentelemetry_0_27", "uuid_v7"], optional = true }
tracing-appender = { version = "0.2.3" }
tracing-attributes = "0.1.28"
tracing-opentelemetry = { version = "0.28.0", default-features = false }
tracing-subscriber = { version = "0.3.19", default-features = true, features = ["env-filter", "json", "registry"] }
uuid = { version = "1.17.0", features = ["v7"], optional = true }
vergen = { version = "8.3.2", optional = true, features = ["cargo", "git", "git2", "rustc"] }

[dev-dependencies]
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }

[build-dependencies]
cargo_metadata = "0.18.1"
vergen = { version = "8.3.2", features = ["cargo", "git", "git2", "rustc"], optional = true }

[features]
default = ["actix_web", "payouts"]
actix_web = ["dep:actix-web", "tracing-actix-web", "dep:uuid"]
log_custom_entries_to_extra = []
log_extra_implicit_fields = []
log_active_span_json = []
payouts = []

[lints]
workspace = true


// File: crates/router_env/build.rs

mod vergen {
    include!("src/vergen.rs");
}

mod cargo_workspace {
    include!("src/cargo_workspace.rs");
}

fn main() {
    vergen::generate_cargo_instructions();

    cargo_workspace::verify_cargo_metadata_format();
    cargo_workspace::set_cargo_workspace_members_env();
}


// File: crates/router_env/README.md

# router_env

Environment of payment router: logger, basic config, its environment awareness.

## Example

```rust
use router_env::logger;
use tracing::{self, instrument};

#[instrument]
pub fn sample() -> () {
    logger::log!(
        logger::Level::INFO,
        payment_id = 8565654,
        payment_attempt_id = 596456465,
        merchant_id = 954865,
        tag = ?logger::Tag::ApiIncomingRequest,
        category = ?logger::Category::Api,
        flow = "some_flow",
        session_id = "some_session",
    );
}
```


// File: crates/router_env/tests/logger.rs

mod test_module;

use ::config::ConfigError;
use router_env::TelemetryGuard;

use self::test_module::fn_with_colon;

fn logger() -> error_stack::Result<&'static TelemetryGuard, ConfigError> {
    use std::sync::OnceLock;

    static INSTANCE: OnceLock<TelemetryGuard> = OnceLock::new();

    #[allow(clippy::unwrap_used)]
    Ok(INSTANCE.get_or_init(|| {
        let config = router_env::Config::new().unwrap();

        router_env::setup(&config.log, "router_env_test", []).unwrap()
    }))
}

#[tokio::test]
async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    logger()?;

    fn_with_colon(13).await;

    Ok(())
}


// File: crates/router_env/tests/test_module.rs

use router_env as logger;

#[tracing::instrument(skip_all)]
pub async fn fn_with_colon(val: i32) {
    let a = 13;
    let b = 31;

    logger::log!(
        logger::Level::WARN,
        ?a,
        ?b,
        tag = ?logger::Tag::ApiIncomingRequest,
        category = ?logger::Category::Api,
        flow = "some_flow",
        session_id = "some_session",
        answer = 13,
        message2 = "yyy",
        message = "Experiment",
        val,
    );

    fn_without_colon(131).await;
}

#[tracing::instrument(fields(val3 = "abc"), skip_all)]
pub async fn fn_without_colon(val: i32) {
    let a = 13;
    let b = 31;

    // trace_macros!(true);
    logger::log!(
        logger::Level::INFO,
        ?a,
        ?b,
        tag = ?logger::Tag::ApiIncomingRequest,
        category = ?logger::Category::Api,
        flow = "some_flow",
        session_id = "some_session",
        answer = 13,
        message2 = "yyy",
        message = "Experiment",
        val,
    );
    // trace_macros!(false);
}


// File: crates/router_env/tests/env.rs

#[cfg(feature = "vergen")]
use router_env as env;

#[cfg(feature = "vergen")]
#[tokio::test]
async fn basic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    println!("CARGO_PKG_VERSION : {:?}", env!("CARGO_PKG_VERSION"));
    println!("CARGO_PROFILE : {:?}", env!("CARGO_PROFILE"));

    println!(
        "GIT_COMMIT_TIMESTAMP : {:?}",
        env!("VERGEN_GIT_COMMIT_TIMESTAMP")
    );
    println!("GIT_SHA : {:?}", env!("VERGEN_GIT_SHA"));
    println!("RUSTC_SEMVER : {:?}", env!("VERGEN_RUSTC_SEMVER"));
    println!(
        "CARGO_TARGET_TRIPLE : {:?}",
        env!("VERGEN_CARGO_TARGET_TRIPLE")
    );

    Ok(())
}

#[cfg(feature = "vergen")]
#[tokio::test]
async fn env_macro() {
    println!("version : {:?}", env::version!());
    println!("build : {:?}", env::build!());
    println!("commit : {:?}", env::commit!());
    // println!("platform : {:?}", env::platform!());

    assert!(!env::version!().is_empty());
    assert!(!env::build!().is_empty());
    assert!(!env::commit!().is_empty());
    // assert!(env::platform!().len() > 0);
}


// File: crates/router_env/src/vergen.rs

/// Configures the [`vergen`][::vergen] crate to generate the `cargo` build instructions.
///
/// This function should be typically called within build scripts to generate `cargo` build
/// instructions for the corresponding crate.
///
/// # Panics
///
/// Panics if `vergen` fails to generate `cargo` build instructions.
#[cfg(feature = "vergen")]
#[allow(clippy::expect_used)]
pub fn generate_cargo_instructions() {
    use std::io::Write;

    use vergen::EmitBuilder;

    EmitBuilder::builder()
        .cargo_debug()
        .cargo_opt_level()
        .cargo_target_triple()
        .git_commit_timestamp()
        .git_describe(true, true, None)
        .git_sha(true)
        .rustc_semver()
        .rustc_commit_hash()
        .emit()
        .expect("Failed to generate `cargo` build instructions");

    writeln!(
        &mut std::io::stdout(),
        "cargo:rustc-env=CARGO_PROFILE={}",
        std::env::var("PROFILE").expect("Failed to obtain `cargo` profile")
    )
    .expect("Failed to set `CARGO_PROFILE` environment variable");
}

#[cfg(not(feature = "vergen"))]
pub fn generate_cargo_instructions() {}


// File: crates/router_env/src/logger.rs

//! Logger of the system.

pub use tracing::{debug, error, event as log, info, warn};
pub use tracing_attributes::instrument;

pub mod config;
mod defaults;
pub use crate::config::Config;

// mod macros;
pub mod types;
pub use types::{Category, Flow, Level, Tag};

mod setup;
pub use setup::{setup, TelemetryGuard};

pub mod formatter;
pub use formatter::FormattingLayer;

pub mod storage;
pub use storage::{Storage, StorageSubscription};


// File: crates/router_env/src/env.rs

//! Information about the current environment.

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Environment variables accessed by the application. This module aims to be the source of truth
/// containing all environment variable that the application accesses.
pub mod vars {
    /// Parent directory where `Cargo.toml` is stored.
    pub const CARGO_MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR";

    /// Environment variable that sets development/sandbox/production environment.
    pub const RUN_ENV: &str = "RUN_ENV";

    /// Directory of config TOML files. Default is `config`.
    pub const CONFIG_DIR: &str = "CONFIG_DIR";
}

/// Current environment.
#[derive(
    Debug,
    Default,
    Deserialize,
    Serialize,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    strum::Display,
    strum::EnumString,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum Env {
    /// Development environment.
    #[default]
    Development,
    /// Integration environment.
    Integ,
    /// Sandbox environment.
    Sandbox,
    /// Production environment.
    Production,
}

/// Name of current environment. Either "development", "sandbox" or "production".
pub fn which() -> Env {
    #[cfg(debug_assertions)]
    let default_env = Env::Development;
    #[cfg(not(debug_assertions))]
    let default_env = Env::Production;

    std::env::var(vars::RUN_ENV).map_or_else(|_| default_env, |v| v.parse().unwrap_or(default_env))
}

/// Three letter (lowercase) prefix corresponding to the current environment.
/// Either `dev`, `integ`, `snd` or `prd`.
pub fn prefix_for_env() -> &'static str {
    match which() {
        Env::Development => "dev",
        Env::Integ => "integ",
        Env::Sandbox => "snd",
        Env::Production => "prd",
    }
}

/// Path to the root directory of the cargo workspace.
/// It is recommended that this be used by the application as the base path to build other paths
/// such as configuration and logs directories.
pub fn workspace_path() -> PathBuf {
    if let Ok(manifest_dir) = std::env::var(vars::CARGO_MANIFEST_DIR) {
        let mut path = PathBuf::from(manifest_dir);
        path.pop();
        path.pop();
        path
    } else {
        PathBuf::from(".")
    }
}

/// Version of the crate containing the following information:
///
/// - The latest git tag. If tags are not present in the repository, the short commit hash is used
///   instead.
/// - Short hash of the latest git commit.
/// - Timestamp of the latest git commit.
///
/// Example: `0.1.0-abcd012-2038-01-19T03:14:08Z`.
#[cfg(feature = "vergen")]
#[macro_export]
macro_rules! version {
    () => {
        concat!(
            env!("VERGEN_GIT_DESCRIBE"),
            "-",
            env!("VERGEN_GIT_SHA"),
            "-",
            env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
        )
    };
}

/// A string uniquely identifying the application build.
///
/// Consists of a combination of:
/// - Version defined in the crate file
/// - Timestamp of commit
/// - Hash of the commit
/// - Version of rust compiler
/// - Target triple
///
/// Example: `0.1.0-f5f383e-2022-09-04T11:39:37Z-1.63.0-x86_64-unknown-linux-gnu`
#[cfg(feature = "vergen")]
#[macro_export]
macro_rules! build {
    () => {
        concat!(
            env!("CARGO_PKG_VERSION"),
            "-",
            env!("VERGEN_GIT_SHA"),
            "-",
            env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
            "-",
            env!("VERGEN_RUSTC_SEMVER"),
            "-",
            $crate::profile!(),
            "-",
            env!("VERGEN_CARGO_TARGET_TRIPLE"),
        )
    };
}

/// Short hash of the current commit.
///
/// Example: `f5f383e`.
#[cfg(feature = "vergen")]
#[macro_export]
macro_rules! commit {
    () => {
        env!("VERGEN_GIT_SHA")
    };
}

// /// Information about the platform on which service was built, including:
// /// - Information about OS
// /// - Information about CPU
// ///
// /// Example: ``.
// #[macro_export]
// macro_rules! platform {
//     (
//     ) => {
//         concat!(
//             env!("VERGEN_SYSINFO_OS_VERSION"),
//             " - ",
//             env!("VERGEN_SYSINFO_CPU_BRAND"),
//         )
//     };
// }

/// Service name deduced from name of the binary.
/// This macro must be called within binaries only.
///
/// Example: `router`.
#[macro_export]
macro_rules! service_name {
    () => {
        env!("CARGO_BIN_NAME")
    };
}

/// Build profile, either debug or release.
///
/// Example: `release`.
#[macro_export]
macro_rules! profile {
    () => {
        env!("CARGO_PROFILE")
    };
}

/// The latest git tag. If tags are not present in the repository, the short commit hash is used
/// instead. Refer to the [`git describe`](https://git-scm.com/docs/git-describe) documentation for
/// more details.
#[macro_export]
macro_rules! git_tag {
    () => {
        env!("VERGEN_GIT_DESCRIBE")
    };
}


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

#![warn(missing_debug_implementations)]

//! Environment of payment router: logger, basic config, its environment awareness.

#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR" ), "/", "README.md"))]

/// Utilities to identify members of the current cargo workspace.
pub mod cargo_workspace;
pub mod env;
pub mod logger;
pub mod metrics;
#[cfg(feature = "actix_web")]
pub mod request_id;
#[cfg(feature = "actix_web")]
pub mod root_span;
/// `cargo` build instructions generation for obtaining information about the application
/// environment.
#[cfg(feature = "vergen")]
pub mod vergen;

// pub use literally;
#[doc(inline)]
pub use logger::*;
pub use opentelemetry;
// Re-export our internal request_id module for easier migration
#[cfg(feature = "actix_web")]
pub use request_id::{IdReuse, RequestId, RequestIdentifier};
#[cfg(feature = "actix_web")]
pub use root_span::CustomRootSpanBuilder;
pub use tracing;
#[cfg(feature = "actix_web")]
pub use tracing_actix_web;
pub use tracing_appender;

#[doc(inline)]
pub use self::env::*;


// File: crates/router_env/src/metrics.rs

//! Utilities to easily create opentelemetry contexts, meters and metrics.

/// Create a global [`Meter`][Meter] with the specified name and an optional description.
///
/// [Meter]: opentelemetry::metrics::Meter
#[macro_export]
macro_rules! global_meter {
    ($name:ident) => {
        static $name: ::std::sync::LazyLock<$crate::opentelemetry::metrics::Meter> =
            ::std::sync::LazyLock::new(|| $crate::opentelemetry::global::meter(stringify!($name)));
    };
    ($meter:ident, $name:literal) => {
        static $meter: ::std::sync::LazyLock<$crate::opentelemetry::metrics::Meter> =
            ::std::sync::LazyLock::new(|| $crate::opentelemetry::global::meter(stringify!($name)));
    };
}

/// Create a [`Counter`][Counter] metric with the specified name and an optional description,
/// associated with the specified meter. Note that the meter must be a valid [`Meter`][Meter].
///
/// [Counter]: opentelemetry::metrics::Counter
/// [Meter]: opentelemetry::metrics::Meter
#[macro_export]
macro_rules! counter_metric {
    ($name:ident, $meter:ident) => {
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Counter<u64>,
        > = ::std::sync::LazyLock::new(|| $meter.u64_counter(stringify!($name)).build());
    };
    ($name:ident, $meter:ident, description:literal) => {
        #[doc = $description]
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Counter<u64>,
        > = ::std::sync::LazyLock::new(|| {
            $meter
                .u64_counter(stringify!($name))
                .with_description($description)
                .build()
        });
    };
}

/// Create a [`Histogram`][Histogram] f64 metric with the specified name and an optional description,
/// associated with the specified meter. Note that the meter must be a valid [`Meter`][Meter].
///
/// [Histogram]: opentelemetry::metrics::Histogram
/// [Meter]: opentelemetry::metrics::Meter
#[macro_export]
macro_rules! histogram_metric_f64 {
    ($name:ident, $meter:ident) => {
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Histogram<f64>,
        > = ::std::sync::LazyLock::new(|| {
            $meter
                .f64_histogram(stringify!($name))
                .with_boundaries($crate::metrics::f64_histogram_buckets())
                .build()
        });
    };
    ($name:ident, $meter:ident, $description:literal) => {
        #[doc = $description]
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Histogram<f64>,
        > = ::std::sync::LazyLock::new(|| {
            $meter
                .f64_histogram(stringify!($name))
                .with_description($description)
                .with_boundaries($crate::metrics::f64_histogram_buckets())
                .build()
        });
    };
}

/// Create a [`Histogram`][Histogram] u64 metric with the specified name and an optional description,
/// associated with the specified meter. Note that the meter must be a valid [`Meter`][Meter].
///
/// [Histogram]: opentelemetry::metrics::Histogram
/// [Meter]: opentelemetry::metrics::Meter
#[macro_export]
macro_rules! histogram_metric_u64 {
    ($name:ident, $meter:ident) => {
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Histogram<u64>,
        > = ::std::sync::LazyLock::new(|| {
            $meter
                .u64_histogram(stringify!($name))
                .with_boundaries($crate::metrics::f64_histogram_buckets())
                .build()
        });
    };
    ($name:ident, $meter:ident, $description:literal) => {
        #[doc = $description]
        pub(crate) static $name: ::std::sync::LazyLock<
            $crate::opentelemetry::metrics::Histogram<u64>,
        > = ::std::sync::LazyLock::new(|| {
            $meter
                .u64_histogram(stringify!($name))
                .with_description($description)
                .with_boundaries($crate::metrics::f64_histogram_buckets())
                .build()
        });
    };
}

/// Create a [`Gauge`][Gauge] metric with the specified name and an optional description,
/// associated with the specified meter. Note that the meter must be a valid [`Meter`][Meter].
///
/// [Gauge]: opentelemetry::metrics::Gauge
/// [Meter]: opentelemetry::metrics::Meter
#[macro_export]
macro_rules! gauge_metric {
    ($name:ident, $meter:ident) => {
        pub(crate) static $name: ::std::sync::LazyLock<$crate::opentelemetry::metrics::Gauge<u64>> =
            ::std::sync::LazyLock::new(|| $meter.u64_gauge(stringify!($name)).build());
    };
    ($name:ident, $meter:ident, description:literal) => {
        #[doc = $description]
        pub(crate) static $name: ::std::sync::LazyLock<$crate::opentelemetry::metrics::Gauge<u64>> =
            ::std::sync::LazyLock::new(|| {
                $meter
                    .u64_gauge(stringify!($name))
                    .with_description($description)
                    .build()
            });
    };
}

/// Create attributes to associate with a metric from key-value pairs.
#[macro_export]
macro_rules! metric_attributes {
    ($(($key:expr, $value:expr $(,)?)),+ $(,)?) => {
        &[$($crate::opentelemetry::KeyValue::new($key, $value)),+]
    };
}

pub use helpers::f64_histogram_buckets;

mod helpers {
    /// Returns the buckets to be used for a f64 histogram
    #[inline(always)]
    pub fn f64_histogram_buckets() -> Vec<f64> {
        let mut init = 0.01;
        let mut buckets: [f64; 15] = [0.0; 15];

        for bucket in &mut buckets {
            init *= 2.0;
            *bucket = init;
        }

        Vec::from(buckets)
    }
}


// File: crates/router_env/src/root_span.rs

//! Custom RootSpanBuilder tracing-actix-web

use actix_web::{
    body::MessageBody,
    dev::{ServiceRequest, ServiceResponse},
    http::StatusCode,
    Error, HttpMessage, ResponseError,
};
use tracing::Span;
use tracing_actix_web::{root_span, RootSpanBuilder};

use crate::request_id::RequestId;

/// Custom RootSpanBuilder that captures x-request-id header in spans
#[derive(Debug)]
pub struct CustomRootSpanBuilder;

impl RootSpanBuilder for CustomRootSpanBuilder {
    fn on_request_start(request: &ServiceRequest) -> Span {
        // Extract the RequestId from extensions (set by RequestIdentifier middleware)
        // We clone the string to avoid lifetime issues with the temporary Ref guard
        let request_id = request
            .extensions()
            .get::<RequestId>()
            .map(|id| id.to_string())
            .unwrap_or_default();

        root_span!(
            level = crate::Level::INFO,
            request,
            request_id = request_id.as_str()
        )
    }

    fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
        match &outcome {
            Ok(response) => {
                if let Some(error) = response.response().error() {
                    // use the status code already constructed for the outgoing HTTP response
                    handle_error(span, response.status(), error.as_response_error());
                } else {
                    let code: i32 = response.response().status().as_u16().into();
                    span.record("http.status_code", code);
                    span.record("otel.status_code", "OK");
                }
            }
            Err(error) => {
                let response_error = error.as_response_error();
                handle_error(span, response_error.status_code(), response_error);
            }
        };
    }
}

fn handle_error(span: Span, status_code: StatusCode, response_error: &dyn ResponseError) {
    // pre-formatting errors is a workaround for https://github.com/tokio-rs/tracing/issues/1565
    let display = format!("{response_error}");
    let debug = format!("{response_error:?}");
    span.record("exception.message", tracing::field::display(display));
    span.record("exception.details", tracing::field::display(debug));
    let code: i32 = status_code.as_u16().into();

    span.record("http.status_code", code);

    if status_code.is_client_error() {
        span.record("otel.status_code", "OK");
    } else {
        span.record("otel.status_code", "ERROR");
    }
}


// File: crates/router_env/src/cargo_workspace.rs

/// Sets the `CARGO_WORKSPACE_MEMBERS` environment variable to include a comma-separated list of
/// names of all crates in the current cargo workspace.
///
/// This function should be typically called within build scripts, so that the environment variable
/// is available to the corresponding crate at compile time.
///
/// # Panics
///
/// Panics if running the `cargo metadata` command fails.
#[allow(clippy::expect_used)]
pub fn set_cargo_workspace_members_env() {
    use std::io::Write;

    let metadata = cargo_metadata::MetadataCommand::new()
        .exec()
        .expect("Failed to obtain cargo metadata");

    let workspace_members = metadata
        .workspace_packages()
        .iter()
        .map(|package| package.name.as_str())
        .collect::<Vec<_>>()
        .join(",");

    writeln!(
        &mut std::io::stdout(),
        "cargo:rustc-env=CARGO_WORKSPACE_MEMBERS={workspace_members}"
    )
    .expect("Failed to set `CARGO_WORKSPACE_MEMBERS` environment variable");
}

/// Verify that the cargo metadata workspace packages format matches that expected by
/// [`set_cargo_workspace_members_env`] to set the `CARGO_WORKSPACE_MEMBERS` environment variable.
///
/// This function should be typically called within build scripts, before the
/// [`set_cargo_workspace_members_env`] function is called.
///
/// # Panics
///
/// Panics if running the `cargo metadata` command fails, or if the workspace member package names
/// cannot be determined.
pub fn verify_cargo_metadata_format() {
    #[allow(clippy::expect_used)]
    let metadata = cargo_metadata::MetadataCommand::new()
        .exec()
        .expect("Failed to obtain cargo metadata");

    assert!(
        metadata
            .workspace_packages()
            .iter()
            .any(|package| package.name == env!("CARGO_PKG_NAME")),
        "Unable to determine workspace member package names from `cargo metadata`"
    );
}

/// Obtain the crates in the current cargo workspace as a `HashSet`.
///
/// This macro requires that [`set_cargo_workspace_members_env()`] function be called in the
/// build script of the crate where this macro is being called.
///
/// # Errors
///
/// Causes a compilation error if the `CARGO_WORKSPACE_MEMBERS` environment variable is unset.
#[macro_export]
macro_rules! cargo_workspace_members {
    () => {
        std::env!("CARGO_WORKSPACE_MEMBERS")
            .split(',')
            .collect::<std::collections::HashSet<&'static str>>()
    };
}


// File: crates/router_env/src/request_id.rs

#![warn(unused, missing_docs)]
//! Request ID middleware for Actix Web applications.
//!
//! This module provides middleware to associate every HTTP request with a unique identifier.
//! The request ID can be used for distributed tracing, error tracking, and request correlation
//! across microservices.
//!
//! # Features
//!
//! - **Automatic UUID v7 generation**: Uses time-ordered UUIDs for better performance
//! - **Header reuse**: Respects incoming request ID headers for request correlation
//! - **Configurable headers**: Custom header names supported for any request ID header
//! - **Request extraction**: RequestId can be extracted in route handlers
//! - **Response headers**: Automatically adds request ID to response headers
//! - **Upstream logging**: Logs incoming request IDs for debugging
//!
//! # Examples
//!
//! ```rust
//! use router_env::{RequestIdentifier, IdReuse};
//! use actix_web::{web, App, HttpServer};
//!
//! let app = App::new()
//!     .wrap(
//!         RequestIdentifier::new("x-request-id")
//!             .use_incoming_id(IdReuse::UseIncoming)
//!     );
//! ```

use std::{
    fmt::{self, Display, Formatter},
    future::{ready, Future, Ready},
    pin::Pin,
    str::FromStr,
    sync::Arc,
    task::{Context, Poll},
};

use actix_web::{
    dev::{Payload, Service, ServiceRequest, ServiceResponse, Transform},
    error::ResponseError,
    http::header::{HeaderName, HeaderValue},
    Error as ActixError, FromRequest, HttpMessage, HttpRequest,
};
use error_stack::{report, ResultExt};
use uuid::Uuid;

/// Custom result type for request ID operations.
pub type RequestIdResult<T> = Result<T, error_stack::Report<RequestIdError>>;

/// Errors that can occur when working with request IDs.
#[derive(Debug, Clone)]
pub enum RequestIdError {
    /// No request ID is associated with the current request.
    NoAssociatedId,
    /// Failed to convert header value to request ID.
    InvalidHeaderValue {
        /// The invalid header value that caused the error.
        value: String,
    },
}

impl error_stack::Context for RequestIdError {}

impl ResponseError for RequestIdError {}

impl Display for RequestIdError {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::NoAssociatedId => write!(fmt, "No request ID associated with this request"),
            Self::InvalidHeaderValue { value } => write!(fmt, "Invalid header value: {}", value),
        }
    }
}

/// Configuration for handling incoming request ID headers.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IdReuse {
    /// Reuse the incoming request ID if present, otherwise generate a new one.
    UseIncoming,
    /// Always generate a new request ID, ignoring any incoming headers.
    #[default]
    IgnoreIncoming,
}

/// Generate a new UUID v7 request ID.
fn generate_uuid_v7() -> String {
    Uuid::now_v7().to_string()
}

/// Request ID middleware that takes a configurable header name
/// and determines how to handle incoming request IDs.
#[derive(Clone, Debug)]
pub struct RequestIdentifier {
    header_name: String,
    use_incoming_id: IdReuse,
}

/// Request ID value that can be extracted in route handlers.
///
/// Wraps an `Arc<str>` for optimal performance in web middleware.
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub struct RequestId(Arc<str>);

/// The middleware implementation that processes requests.
#[derive(Debug)]
pub struct RequestIdMiddleware<S> {
    service: S,
    header_name: HeaderName,
    use_incoming_id: IdReuse,
}

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

impl FromStr for RequestId {
    type Err = error_stack::Report<RequestIdError>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            Err(report!(RequestIdError::InvalidHeaderValue {
                value: s.to_string(),
            }))
        } else {
            Ok(Self(s.into()))
        }
    }
}

impl TryFrom<HeaderValue> for RequestId {
    type Error = error_stack::Report<RequestIdError>;

    fn try_from(value: HeaderValue) -> Result<Self, Self::Error> {
        let s = value
            .to_str()
            .change_context(RequestIdError::InvalidHeaderValue {
                value: format!("{:?}", value),
            })?;
        Self::from_str(s)
    }
}

impl From<RequestId> for String {
    fn from(request_id: RequestId) -> Self {
        request_id.0.to_string()
    }
}

impl TryFrom<String> for RequestId {
    type Error = error_stack::Report<RequestIdError>;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        Self::from_str(s.as_str())
    }
}

impl TryFrom<&str> for RequestId {
    type Error = error_stack::Report<RequestIdError>;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        Self::from_str(s)
    }
}

impl RequestId {
    /// Extract request ID from ServiceRequest header or generate UUID v7.
    ///
    /// This is the core logic: try to extract from the specified header,
    /// if not possible or not desired, generate a new UUID v7.
    pub fn extract_or_generate(
        request: &ServiceRequest,
        header_name: &HeaderName,
        use_incoming_id: IdReuse,
    ) -> Self {
        let request_id_string = match use_incoming_id {
            IdReuse::UseIncoming => {
                // Try to extract from incoming header
                if let Some(existing_header) = request.headers().get(header_name) {
                    Self::try_from(existing_header.clone())
                        .map(|id| id.0.to_string())
                        .unwrap_or_else(|e| {
                            tracing::warn!(
                                error = %e,
                                "Invalid request ID header, generating new UUID v7"
                            );
                            generate_uuid_v7()
                        })
                } else {
                    // No header found, generate new UUID v7
                    generate_uuid_v7()
                }
            }
            IdReuse::IgnoreIncoming => {
                // Always generate new UUID v7
                generate_uuid_v7()
            }
        };

        Self(request_id_string.into())
    }

    /// Convert this request ID to a `HeaderValue`.
    pub fn to_header_value(&self) -> RequestIdResult<HeaderValue> {
        HeaderValue::from_str(&self.0).change_context(RequestIdError::InvalidHeaderValue {
            value: self.0.to_string(),
        })
    }

    /// Get a string representation of this request ID.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl RequestIdentifier {
    /// Create a request ID middleware with a custom header name.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use router_env::RequestIdentifier;
    ///
    /// // Use any custom header name for request ID extraction
    /// let middleware = RequestIdentifier::new("x-request-id");
    /// ```
    #[must_use]
    pub fn new(header_name: &str) -> Self {
        Self {
            header_name: header_name.to_string(),
            use_incoming_id: IdReuse::default(),
        }
    }

    /// Configure how incoming request ID headers should be handled.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use router_env::{RequestIdentifier, IdReuse};
    ///
    /// let middleware = RequestIdentifier::new("x-request-id")
    ///     .use_incoming_id(IdReuse::IgnoreIncoming);
    /// ```
    #[must_use]
    pub fn use_incoming_id(self, use_incoming_id: IdReuse) -> Self {
        Self {
            use_incoming_id,
            ..self
        }
    }
}

impl<S, B> Transform<S, ServiceRequest> for RequestIdentifier
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
    S::Future: 'static,
    B: 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Transform = RequestIdMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        let header_name = HeaderName::from_str(&self.header_name).unwrap_or_else(|_| {
            tracing::error!(
                "Invalid header name '{}', using fallback 'x-request-id'",
                self.header_name
            );
            HeaderName::from_static("x-request-id")
        });

        ready(Ok(RequestIdMiddleware {
            service,
            header_name,
            use_incoming_id: self.use_incoming_id,
        }))
    }
}

#[allow(clippy::type_complexity)]
impl<S, B> Service<ServiceRequest> for RequestIdMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
    S::Future: 'static,
    B: 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

    fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.service.poll_ready(ctx)
    }

    fn call(&self, request: ServiceRequest) -> Self::Future {
        let header_name = self.header_name.clone();

        // Capture incoming request ID for logging
        let incoming_request_id = request.headers().get(&header_name).cloned();

        // Extract request ID from header or generate new UUID v7
        let request_id =
            RequestId::extract_or_generate(&request, &header_name, self.use_incoming_id);

        // Create header value for response
        let header_value = request_id.to_header_value().unwrap_or_else(|e| {
            tracing::error!(
                error = %e,
                request_id = %request_id,
                "Failed to convert request ID to header value"
            );
            HeaderValue::from_static("invalid-request-id")
        });

        // Store request ID for extraction in handlers
        request.extensions_mut().insert(request_id.clone());

        let fut = self.service.call(request);

        Box::pin(async move {
            // Log incoming request IDs for correlation
            if let Some(upstream_request_id) = incoming_request_id {
                tracing::debug!(
                    ?upstream_request_id,
                    generated_request_id = %request_id,
                    "Received upstream request ID for correlation"
                );
            }

            let mut response = fut.await?;

            // Add request ID to response headers
            response.headers_mut().insert(header_name, header_value);

            Ok(response)
        })
    }
}

impl FromRequest for RequestId {
    type Error = RequestIdError;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
        ready(
            req.extensions()
                .get::<Self>()
                .cloned()
                .ok_or(RequestIdError::NoAssociatedId),
        )
    }
}