// File: crates/router/src/core/files/helpers.rs

use actix_multipart::Field;
use common_utils::errors::CustomResult;
use error_stack::ResultExt;
use futures::TryStreamExt;
use hyperswitch_domain_models::router_response_types::disputes::FileInfo;

use crate::{
    core::{
        errors::{self, utils::ConnectorErrorExt, StorageErrorExt},
        payments, utils,
    },
    routes::SessionState,
    services,
    types::{self, api, domain, transformers::ForeignTryFrom},
};

pub async fn read_string(field: &mut Field) -> Option<String> {
    let bytes = field.try_next().await;
    if let Ok(Some(bytes)) = bytes {
        String::from_utf8(bytes.to_vec()).ok()
    } else {
        None
    }
}

pub async fn get_file_purpose(field: &mut Field) -> Option<api::FilePurpose> {
    let purpose = read_string(field).await;
    match purpose.as_deref() {
        Some("dispute_evidence") => Some(api::FilePurpose::DisputeEvidence),
        _ => None,
    }
}

pub async fn validate_file_upload(
    state: &SessionState,
    platform: domain::Platform,
    create_file_request: api::CreateFileRequest,
) -> CustomResult<(), errors::ApiErrorResponse> {
    //File Validation based on the purpose of file upload
    match create_file_request.purpose {
        api::FilePurpose::DisputeEvidence => {
            let dispute_id = &create_file_request
                .dispute_id
                .ok_or(errors::ApiErrorResponse::MissingDisputeId)?;
            let dispute = state
                .store
                .find_dispute_by_merchant_id_dispute_id(
                    platform.get_processor().get_account().get_id(),
                    dispute_id,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
                    dispute_id: dispute_id.to_string(),
                })?;
            // Connector is not called for validating the file, connector_id can be passed as None safely
            let connector_data = api::ConnectorData::get_connector_by_name(
                &state.conf.connectors,
                &dispute.connector,
                api::GetToken::Connector,
                None,
            )?;

            let validation = connector_data.connector.validate_file_upload(
                create_file_request.purpose,
                create_file_request.file_size,
                create_file_request.file_type.clone(),
            );
            match validation {
                Ok(()) => Ok(()),
                Err(err) => match err.current_context() {
                    errors::ConnectorError::FileValidationFailed { reason } => {
                        Err(errors::ApiErrorResponse::FileValidationFailed {
                            reason: reason.to_string(),
                        }
                        .into())
                    }
                    //We are using parent error and ignoring this
                    _error => Err(err
                        .change_context(errors::ApiErrorResponse::InternalServerError)
                        .attach_printable("File validation failed"))?,
                },
            }
        }
    }
}

pub async fn delete_file_using_file_id(
    state: &SessionState,
    file_key: String,
    platform: &domain::Platform,
) -> CustomResult<(), errors::ApiErrorResponse> {
    let file_metadata_object = state
        .store
        .find_file_metadata_by_merchant_id_file_id(
            platform.get_processor().get_account().get_id(),
            &file_key,
        )
        .await
        .change_context(errors::ApiErrorResponse::FileNotFound)?;
    let (provider, provider_file_id) = match (
        file_metadata_object.file_upload_provider,
        file_metadata_object.provider_file_id,
        file_metadata_object.available,
    ) {
        (Some(provider), Some(provider_file_id), true) => (provider, provider_file_id),
        _ => Err(errors::ApiErrorResponse::FileNotAvailable)
            .attach_printable("File not available")?,
    };
    match provider {
        diesel_models::enums::FileUploadProvider::Router => state
            .file_storage_client
            .delete_file(&provider_file_id)
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError),
        _ => Err(errors::ApiErrorResponse::FileProviderNotSupported {
            message: "Not Supported because provider is not Router".to_string(),
        }
        .into()),
    }
}

pub async fn retrieve_file_from_connector(
    state: &SessionState,
    file_metadata: diesel_models::file::FileMetadata,
    dispute_id: Option<String>,
    platform: &domain::Platform,
) -> CustomResult<Vec<u8>, errors::ApiErrorResponse> {
    let connector = &types::Connector::foreign_try_from(
        file_metadata
            .file_upload_provider
            .ok_or(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Missing file upload provider")?,
    )?
    .to_string();
    let connector_data = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        connector,
        api::GetToken::Connector,
        file_metadata.merchant_connector_id.clone(),
    )?;

    let dispute = match dispute_id {
        Some(dispute) => Some(
            state
                .store
                .find_dispute_by_merchant_id_dispute_id(
                    platform.get_processor().get_account().get_id(),
                    &dispute,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
                    dispute_id: dispute,
                })?,
        ),
        None => None,
    };

    let connector_integration: services::BoxedFilesConnectorIntegrationInterface<
        api::Retrieve,
        types::RetrieveFileRequestData,
        types::RetrieveFileResponse,
    > = connector_data.connector.get_connector_integration();
    let router_data = utils::construct_retrieve_file_router_data(
        state,
        platform,
        &file_metadata,
        dispute,
        connector,
    )
    .await
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed constructing the retrieve file router data")?;
    let response = services::execute_connector_processing_step(
        state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_files_failed_response()
    .attach_printable("Failed while calling retrieve file connector api")?;
    let retrieve_file_response =
        response
            .response
            .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: connector.to_string(),
                status_code: err.status_code,
                reason: err.reason,
            })?;
    Ok(retrieve_file_response.file_data)
}

pub async fn retrieve_file_and_provider_file_id_from_file_id(
    state: &SessionState,
    file_id: Option<String>,
    dispute_id: Option<String>,
    platform: &domain::Platform,
    is_connector_file_data_required: api::FileDataRequired,
) -> CustomResult<FileInfo, errors::ApiErrorResponse> {
    match file_id {
        None => Ok(FileInfo {
            file_data: None,
            provider_file_id: None,
            file_type: None,
        }),
        Some(file_key) => {
            let file_metadata_object = state
                .store
                .find_file_metadata_by_merchant_id_file_id(
                    platform.get_processor().get_account().get_id(),
                    &file_key,
                )
                .await
                .change_context(errors::ApiErrorResponse::FileNotFound)?;
            let (provider, provider_file_id) = match (
                file_metadata_object.file_upload_provider,
                file_metadata_object.provider_file_id.clone(),
                file_metadata_object.available,
            ) {
                (Some(provider), Some(provider_file_id), true) => (provider, provider_file_id),
                _ => Err(errors::ApiErrorResponse::FileNotAvailable)
                    .attach_printable("File not available")?,
            };
            match provider {
                diesel_models::enums::FileUploadProvider::Router => Ok(FileInfo {
                    file_data: Some(
                        state
                            .file_storage_client
                            .retrieve_file(&provider_file_id)
                            .await
                            .change_context(errors::ApiErrorResponse::InternalServerError)?,
                    ),
                    provider_file_id: Some(provider_file_id),
                    file_type: Some(file_metadata_object.file_type),
                }),
                _ => {
                    let connector_file_data = match is_connector_file_data_required {
                        api::FileDataRequired::Required => Some(
                            retrieve_file_from_connector(
                                state,
                                file_metadata_object.clone(),
                                dispute_id,
                                platform,
                            )
                            .await?,
                        ),
                        api::FileDataRequired::NotRequired => None,
                    };
                    Ok(FileInfo {
                        file_data: connector_file_data,
                        provider_file_id: Some(provider_file_id),
                        file_type: Some(file_metadata_object.file_type),
                    })
                }
            }
        }
    }
}

#[cfg(feature = "v2")]
//Upload file to connector if it supports / store it in S3 and return file_upload_provider, provider_file_id accordingly
pub async fn upload_and_get_provider_provider_file_id_profile_id(
    state: &SessionState,
    platform: &domain::Platform,
    create_file_request: &api::CreateFileRequest,
    file_key: String,
) -> CustomResult<
    (
        String,
        api_models::enums::FileUploadProvider,
        Option<common_utils::id_type::ProfileId>,
        Option<common_utils::id_type::MerchantConnectorAccountId>,
    ),
    errors::ApiErrorResponse,
> {
    todo!()
}

#[cfg(feature = "v1")]
//Upload file to connector if it supports / store it in S3 and return file_upload_provider, provider_file_id accordingly
pub async fn upload_and_get_provider_provider_file_id_profile_id(
    state: &SessionState,
    platform: &domain::Platform,
    create_file_request: &api::CreateFileRequest,
    file_key: String,
) -> CustomResult<
    (
        String,
        api_models::enums::FileUploadProvider,
        Option<common_utils::id_type::ProfileId>,
        Option<common_utils::id_type::MerchantConnectorAccountId>,
    ),
    errors::ApiErrorResponse,
> {
    match create_file_request.purpose {
        api::FilePurpose::DisputeEvidence => {
            let dispute_id = create_file_request
                .dispute_id
                .clone()
                .ok_or(errors::ApiErrorResponse::MissingDisputeId)?;
            let dispute = state
                .store
                .find_dispute_by_merchant_id_dispute_id(
                    platform.get_processor().get_account().get_id(),
                    &dispute_id,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id })?;
            let connector_data = api::ConnectorData::get_connector_by_name(
                &state.conf.connectors,
                &dispute.connector,
                api::GetToken::Connector,
                dispute.merchant_connector_id.clone(),
            )?;
            if connector_data.connector_name.supports_file_storage_module() {
                let payment_intent = state
                    .store
                    .find_payment_intent_by_payment_id_merchant_id(
                        &dispute.payment_id,
                        platform.get_processor().get_account().get_id(),
                        platform.get_processor().get_key_store(),
                        platform.get_processor().get_account().storage_scheme,
                    )
                    .await
                    .change_context(errors::ApiErrorResponse::PaymentNotFound)?;

                let payment_attempt = state
                    .store
                    .find_payment_attempt_by_attempt_id_merchant_id(
                        &dispute.attempt_id,
                        platform.get_processor().get_account().get_id(),
                        platform.get_processor().get_account().storage_scheme,
                    )
                    .await
                    .change_context(errors::ApiErrorResponse::PaymentNotFound)?;
                let connector_integration: services::BoxedFilesConnectorIntegrationInterface<
                    api::Upload,
                    types::UploadFileRequestData,
                    types::UploadFileResponse,
                > = connector_data.connector.get_connector_integration();
                let router_data = utils::construct_upload_file_router_data(
                    state,
                    &payment_intent,
                    &payment_attempt,
                    platform,
                    create_file_request,
                    dispute,
                    &connector_data.connector_name.to_string(),
                    file_key,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed constructing the upload file router data")?;
                let response = services::execute_connector_processing_step(
                    state,
                    connector_integration,
                    &router_data,
                    payments::CallConnectorAction::Trigger,
                    None,
                    None,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed while calling upload file connector api")?;

                let upload_file_response = response.response.map_err(|err| {
                    errors::ApiErrorResponse::ExternalConnectorError {
                        code: err.code,
                        message: err.message,
                        connector: connector_data.connector_name.to_string(),
                        status_code: err.status_code,
                        reason: err.reason,
                    }
                })?;
                Ok((
                    upload_file_response.provider_file_id,
                    api_models::enums::FileUploadProvider::foreign_try_from(
                        &connector_data.connector_name,
                    )?,
                    payment_intent.profile_id,
                    payment_attempt.merchant_connector_id,
                ))
            } else {
                state
                    .file_storage_client
                    .upload_file(&file_key, create_file_request.file.clone())
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)?;
                Ok((
                    file_key,
                    api_models::enums::FileUploadProvider::Router,
                    None,
                    None,
                ))
            }
        }
    }
}


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

use actix_http::header;
use api_models::admin::PaymentLinkConfig;
use common_utils::validation::validate_domain_against_allowed_domains;
use error_stack::{report, ResultExt};
use url::Url;

use crate::{
    core::errors::{self, RouterResult},
    types::storage::PaymentLink,
};

pub fn validate_secure_payment_link_render_request(
    request_headers: &header::HeaderMap,
    payment_link: &PaymentLink,
    payment_link_config: &PaymentLinkConfig,
) -> RouterResult<()> {
    let link_id = payment_link.payment_link_id.clone();
    let allowed_domains = payment_link_config
        .allowed_domains
        .clone()
        .ok_or(report!(errors::ApiErrorResponse::InvalidRequestUrl))
        .attach_printable_lazy(|| {
            format!("Secure payment link was not generated for {link_id}\nmissing allowed_domains")
        })?;

    // Validate secure_link was generated
    if payment_link.secure_link.clone().is_none() {
        return Err(report!(errors::ApiErrorResponse::InvalidRequestUrl)).attach_printable_lazy(
            || format!("Secure payment link was not generated for {link_id}\nmissing secure_link"),
        );
    }

    // Fetch destination is "iframe"
    match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) {
        Some("iframe") => Ok(()),
        Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden {
            resource: "payment_link".to_string(),
        }))
        .attach_printable_lazy(|| {
            format!(
                "Access to payment_link [{link_id}] is forbidden when requested through {requestor}",

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

            )
        }),
    }?;

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

                )
            })?;

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

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

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


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

use api_models::routing::{
    DynamicRoutingAlgorithm, MerchantRoutingAlgorithm, RoutingAlgorithmKind,
    RoutingAlgorithmWrapper, RoutingDictionaryRecord,
};
#[cfg(feature = "v1")]
use api_models::{
    open_router::{OpenRouterDecideGatewayRequest, PaymentInfo, RankingAlgorithm},
    routing::RoutableConnectorChoice,
};
use common_utils::ext_traits::ValueExt;
use diesel_models::{
    enums as storage_enums,
    routing_algorithm::{RoutingAlgorithm, RoutingProfileMetadata},
};
#[cfg(feature = "v1")]
use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt;
use masking::{PeekInterface, Secret};

use crate::{
    core::{errors, routing},
    types::transformers::{ForeignFrom, ForeignInto, ForeignTryFrom},
};

impl ForeignFrom<RoutingProfileMetadata> for RoutingDictionaryRecord {
    fn foreign_from(value: RoutingProfileMetadata) -> Self {
        Self {
            id: value.algorithm_id,

            profile_id: value.profile_id,
            name: value.name,
            kind: value.kind.foreign_into(),
            description: value.description.unwrap_or_default(),
            created_at: value.created_at.assume_utc().unix_timestamp(),
            modified_at: value.modified_at.assume_utc().unix_timestamp(),
            algorithm_for: Some(value.algorithm_for),
            decision_engine_routing_id: None,
        }
    }
}

impl ForeignFrom<RoutingAlgorithm> for RoutingDictionaryRecord {
    fn foreign_from(value: RoutingAlgorithm) -> Self {
        Self {
            id: value.algorithm_id,

            profile_id: value.profile_id,
            name: value.name,
            kind: value.kind.foreign_into(),
            description: value.description.unwrap_or_default(),
            created_at: value.created_at.assume_utc().unix_timestamp(),
            modified_at: value.modified_at.assume_utc().unix_timestamp(),
            algorithm_for: Some(value.algorithm_for),
            decision_engine_routing_id: value.decision_engine_routing_id,
        }
    }
}

impl ForeignTryFrom<RoutingAlgorithm> for MerchantRoutingAlgorithm {
    type Error = error_stack::Report<errors::ParsingError>;

    fn foreign_try_from(value: RoutingAlgorithm) -> Result<Self, Self::Error> {
        let algorithm: RoutingAlgorithmWrapper = match value.kind {
            diesel_models::enums::RoutingAlgorithmKind::Dynamic => value
                .algorithm_data
                .parse_value::<DynamicRoutingAlgorithm>("RoutingAlgorithmDynamic")
                .map(RoutingAlgorithmWrapper::Dynamic)?,
            _ => value
                .algorithm_data
                .parse_value::<api_models::routing::StaticRoutingAlgorithm>("RoutingAlgorithm")
                .map(RoutingAlgorithmWrapper::Static)?,
        };

        Ok(Self {
            id: value.algorithm_id,
            name: value.name,
            profile_id: value.profile_id,
            description: value.description.unwrap_or_default(),
            algorithm,
            created_at: value.created_at.assume_utc().unix_timestamp(),
            modified_at: value.modified_at.assume_utc().unix_timestamp(),
            algorithm_for: value.algorithm_for,
        })
    }
}

impl ForeignFrom<storage_enums::RoutingAlgorithmKind> for RoutingAlgorithmKind {
    fn foreign_from(value: storage_enums::RoutingAlgorithmKind) -> Self {
        match value {
            storage_enums::RoutingAlgorithmKind::Single => Self::Single,
            storage_enums::RoutingAlgorithmKind::Priority => Self::Priority,
            storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit,
            storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced,
            storage_enums::RoutingAlgorithmKind::Dynamic => Self::Dynamic,
            storage_enums::RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule,
        }
    }
}

impl ForeignFrom<RoutingAlgorithmKind> for storage_enums::RoutingAlgorithmKind {
    fn foreign_from(value: RoutingAlgorithmKind) -> Self {
        match value {
            RoutingAlgorithmKind::Single => Self::Single,
            RoutingAlgorithmKind::Priority => Self::Priority,
            RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit,
            RoutingAlgorithmKind::Advanced => Self::Advanced,
            RoutingAlgorithmKind::Dynamic => Self::Dynamic,
            RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule,
        }
    }
}

impl From<&routing::TransactionData<'_>> for storage_enums::TransactionType {
    fn from(value: &routing::TransactionData<'_>) -> Self {
        match value {
            routing::TransactionData::Payment(_) => Self::Payment,
            #[cfg(feature = "payouts")]
            routing::TransactionData::Payout(_) => Self::Payout,
        }
    }
}

#[cfg(feature = "v1")]
pub trait OpenRouterDecideGatewayRequestExt {
    fn construct_sr_request(
        attempt: &PaymentAttempt,
        eligible_gateway_list: Vec<RoutableConnectorChoice>,
        ranking_algorithm: Option<RankingAlgorithm>,
        is_elimination_enabled: bool,
    ) -> Self
    where
        Self: Sized;

    fn construct_debit_request(
        attempt: &PaymentAttempt,
        metadata: Option<String>,
        card_isin: Option<Secret<String>>,
        ranking_algorithm: Option<RankingAlgorithm>,
    ) -> Self
    where
        Self: Sized;
}

#[cfg(feature = "v1")]
impl OpenRouterDecideGatewayRequestExt for OpenRouterDecideGatewayRequest {
    fn construct_sr_request(
        attempt: &PaymentAttempt,
        eligible_gateway_list: Vec<RoutableConnectorChoice>,
        ranking_algorithm: Option<RankingAlgorithm>,
        is_elimination_enabled: bool,
    ) -> Self {
        Self {
            payment_info: PaymentInfo {
                payment_id: attempt.payment_id.clone(),
                amount: attempt.net_amount.get_order_amount(),
                currency: attempt.currency.unwrap_or(storage_enums::Currency::USD),
                payment_type: "ORDER_PAYMENT".to_string(),
                payment_method_type: "UPI".into(), // TODO: once open-router makes this field string, we can send from attempt
                payment_method: attempt.payment_method.unwrap_or_default(),
                metadata: None,
                card_isin: None,
            },
            merchant_id: attempt.profile_id.clone(),
            eligible_gateway_list: Some(
                eligible_gateway_list
                    .into_iter()
                    .map(|connector| connector.to_string())
                    .collect(),
            ),
            ranking_algorithm,
            elimination_enabled: Some(is_elimination_enabled),
        }
    }

    fn construct_debit_request(
        attempt: &PaymentAttempt,
        metadata: Option<String>,
        card_isin: Option<Secret<String>>,
        ranking_algorithm: Option<RankingAlgorithm>,
    ) -> Self {
        Self {
            payment_info: PaymentInfo {
                payment_id: attempt.payment_id.clone(),
                amount: attempt.net_amount.get_total_amount(),
                currency: attempt.currency.unwrap_or(storage_enums::Currency::USD),
                payment_type: "ORDER_PAYMENT".to_string(),
                card_isin: card_isin.map(|value| value.peek().clone()),
                metadata,
                payment_method_type: "UPI".into(), // TODO: once open-router makes this field string, we can send from attempt
                payment_method: attempt.payment_method.unwrap_or_default(),
            },
            merchant_id: attempt.profile_id.clone(),
            // eligible gateway list is not used in debit routing
            eligible_gateway_list: None,
            ranking_algorithm,
            elimination_enabled: None,
        }
    }
}