use std::str::FromStr;

use async_trait::async_trait;
use common_enums::{CallConnectorAction, ConnectorType, ExecutionPath};
use common_utils::{errors::CustomResult, id_type, request::Request, ucs_types};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
    router_data::RouterData, router_flow_types as domain,
    router_request_types::AccessTokenRequestData,
};
use hyperswitch_interfaces::{
    api::gateway as payment_gateway,
    connector_integration_interface::{BoxedConnectorIntegrationInterface, RouterDataConversion},
    errors::ConnectorError,
    unified_connector_service::transformers::UnifiedConnectorServiceError,
};
use unified_connector_service_client::payments as payments_grpc;

use crate::{
    core::{payments::gateway::context::RouterGatewayContext, unified_connector_service},
    routes::SessionState,
    services::logger,
    types::transformers::ForeignTryFrom,
};

// =============================================================================
// PaymentGateway Implementation for domain::AccessTokenAuth
// =============================================================================

/// Implementation of PaymentGateway for AccessTokenAuth flow
#[async_trait]
impl<RCD>
    payment_gateway::PaymentGateway<
        SessionState,
        RCD,
        Self,
        AccessTokenRequestData,
        hyperswitch_domain_models::router_data::AccessToken,
        RouterGatewayContext,
    > for domain::access_token_auth::AccessTokenAuth
where
    RCD: Clone
        + Send
        + Sync
        + 'static
        + RouterDataConversion<
            Self,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
        >,
{
    async fn execute(
        self: Box<Self>,
        state: &SessionState,
        _connector_integration: BoxedConnectorIntegrationInterface<
            Self,
            RCD,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
        >,
        router_data: &RouterData<
            Self,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
        >,
        call_connector_action: CallConnectorAction,
        _connector_request: Option<Request>,
        _return_raw_connector_response: Option<bool>,
        context: RouterGatewayContext,
    ) -> CustomResult<
        RouterData<
            Self,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
        >,
        ConnectorError,
    > {
        let merchant_connector_account = context.merchant_connector_account;
        let processor = &context.processor;
        let lineage_ids = context.lineage_ids;
        let header_payload = context.header_payload;
        let unified_connector_service_execution_mode = context.execution_mode;

        let client = state
            .grpc_client
            .unified_connector_service_client
            .clone()
            .ok_or(ConnectorError::RequestEncodingFailed)
            .attach_printable("Failed to fetch Unified Connector Service client")?;

        let create_access_token_request =
            payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest::foreign_try_from(
                (router_data, call_connector_action),
            )
            .change_context(ConnectorError::RequestEncodingFailed)
            .attach_printable("Failed to construct Create Access Token Request")?;

        let connector_auth_metadata =
            unified_connector_service::build_unified_connector_service_auth_metadata(
                merchant_connector_account.clone(),
                processor.get_account().get_id(),
                router_data.connector.clone(),
            )
            .change_context(ConnectorError::RequestEncodingFailed)
            .attach_printable("Failed to construct request metadata")?;
        // A merchant-authentication (access-token) call can originate from either a
        // payment or a payout. The connector type selects the UCS connector header
        // namespace, while the ids below carry the payment/payout reference context.
        let connector_type = if router_data.payout_id.is_some() {
            ConnectorType::PayoutProcessor
        } else {
            ConnectorType::PaymentProcessor
        };

        let (merchant_reference_id, resource_id) = if let Some(payout_id) =
            router_data.payout_id.as_deref()
        {
            let merchant_reference_id =
                unified_connector_service::parse_merchant_payout_reference_id(
                    header_payload
                        .x_reference_id
                        .as_deref()
                        .unwrap_or(payout_id),
                )
                .map(ucs_types::UcsReferenceId::Payout);

            let resource_id = id_type::PayoutResourceId::from_str(router_data.attempt_id.as_str())
                .inspect_err(
                    |err| logger::warn!(error=?err, "Invalid Payout AttemptId for UCS resource id"),
                )
                .ok()
                .map(ucs_types::UcsResourceId::PayoutAttempt);

            (merchant_reference_id, resource_id)
        } else {
            let merchant_reference_id = unified_connector_service::parse_merchant_reference_id(
                header_payload
                    .x_reference_id
                    .as_deref()
                    .unwrap_or(router_data.payment_id.as_str()),
            )
            .map(ucs_types::UcsReferenceId::Payment);

            let resource_id =
                    id_type::PaymentResourceId::from_str(router_data.attempt_id.as_str())
                        .inspect_err(|err| {
                            logger::warn!(error=?err, "Invalid Payment AttemptId for UCS resource id")
                        })
                        .ok()
                        .map(ucs_types::UcsResourceId::PaymentAttempt);

            (merchant_reference_id, resource_id)
        };

        let header_payload = state
            .get_grpc_headers_ucs(unified_connector_service_execution_mode)
            .external_vault_proxy_metadata(None)
            .merchant_reference_id(merchant_reference_id)
            .resource_id(resource_id)
            .lineage_ids(lineage_ids);

        Box::pin(unified_connector_service::ucs_logging_wrapper_granular(
            router_data.clone(),
            state,
            create_access_token_request,
            header_payload,
            unified_connector_service_execution_mode,
            |mut router_data, create_access_token_request, grpc_headers| async move {
                let response = match client
                    .create_access_token(
                        create_access_token_request,
                        connector_auth_metadata,
                        grpc_headers,
                        connector_type,
                    )
                    .await
                {
                    Ok(response) => response,
                    Err(report) => {
                        if let UnifiedConnectorServiceError::ConnectorError(inner) =
                            report.current_context()
                        {
                            logger::debug!(
                                "Connector error via UCS for access token (connector {}, status {}): {} - {}",
                                inner.connector,
                                inner.status_code,
                                inner.code,
                                inner.message
                            );
                            router_data.response = Err(inner.as_ref().into());
                            router_data.connector_http_status_code = Some(inner.status_code);
                            return Ok((
                                router_data,
                                (),
                                payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse::default(),
                            ));
                        }
                        return Err(report.attach_printable("Failed to create access token"));
                    }
                };

                let create_access_token_response = response.into_inner();

                let (access_token_result, status_code) =
                    unified_connector_service::handle_unified_connector_service_response_for_create_access_token(
                        create_access_token_response.clone(),
                    )
                    .attach_printable("Failed to deserialize UCS response")?;

                let access_token_result = match access_token_result {
                    Ok(response) => Ok(response),
                    Err(err) => {
                        logger::debug!("Error in UCS router data response");
                        if let Some(attempt_status) = err.attempt_status {
                            router_data.status = attempt_status;
                        }
                        Err(err)
                    }
                };

                router_data.response = access_token_result;
                router_data.connector_http_status_code = Some(status_code);

                Ok((router_data,(), create_access_token_response))
            },
        ))
        .await
        .map(|(router_data, _)| router_data)
        .map_err(super::convert_ucs_error_to_connector_error)
    }
}

/// Implementation of FlowGateway for AccessTokenAuth
///
/// This allows the flow to provide its specific gateway based on execution path
impl<RCD>
    payment_gateway::FlowGateway<
        SessionState,
        RCD,
        AccessTokenRequestData,
        hyperswitch_domain_models::router_data::AccessToken,
        RouterGatewayContext,
    > for domain::access_token_auth::AccessTokenAuth
where
    RCD: Clone
        + Send
        + Sync
        + 'static
        + RouterDataConversion<
            Self,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
        >,
{
    fn get_gateway(
        execution_path: ExecutionPath,
    ) -> Box<
        dyn payment_gateway::PaymentGateway<
            SessionState,
            RCD,
            Self,
            AccessTokenRequestData,
            hyperswitch_domain_models::router_data::AccessToken,
            RouterGatewayContext,
        >,
    > {
        match execution_path {
            ExecutionPath::Direct => Box::new(payment_gateway::DirectGateway),
            ExecutionPath::UnifiedConnectorService
            | ExecutionPath::ShadowUnifiedConnectorService => Box::new(Self),
        }
    }
}
