// AUTO-GENERATED — do not edit by hand.
// Source: services.proto ∩ bindings/uniffi.rs  |  Regenerate: make generate

use std::collections::HashMap;
use std::sync::{Arc, RwLock};

use crate::error::SdkError;
use crate::http_client::{
    generate_proxy_cache_key, merge_http_options, HttpClient, HttpOptions as NativeHttpOptions,
    HttpRequest as ClientHttpRequest, NetworkError,
};
use grpc_api_types::payments::NetworkErrorCode;
use connector_service_ffi::types::{FfiMetadataPayload, FfiRequestData};
use connector_service_ffi::utils::ffi_headers_to_masked_metadata;
use domain_types::router_data::ConnectorSpecificConfig;
use domain_types::router_response_types::Response;
use domain_types::utils::ForeignTryFrom;
{%- set modules = {} -%}
{%- for f in flows -%}
  {%- set mod = f.module -%}
  {%- if mod not in modules -%}
    {%- set _ = modules.update({mod: []}) -%}
  {%- endif -%}
  {%- set _ = modules[mod].append(f.request) -%}
  {%- set _ = modules[mod].append(f.response) -%}
{%- endfor -%}
{%- for f in single_flows if f.service == "EventService" -%}
  {%- set mod = f.module -%}
  {%- if mod not in modules -%}
    {%- set _ = modules.update({mod: []}) -%}
  {%- endif -%}
  {%- set _ = modules[mod].append(f.request) -%}
  {%- set _ = modules[mod].append(f.response) -%}
{%- endfor -%}
use grpc_api_types::payments::{
    ConnectorConfig, FfiOptions, RequestConfig,
};
{% for mod, types in modules.items() | sort %}
use grpc_api_types::{{ mod }}::{
{% for type_name in types | unique | sort %}
    {{ type_name }},
{% endfor %}
};
{% endfor %}

/// ConnectorClient — high-level Rust wrapper for the Connector Service.
///
/// Handles the full round-trip for any payment flow:
///   1. Build connector HTTP request via Rust core handlers
///   2. Execute the HTTP request via our standardized HttpClient (reqwest)
///   3. Parse the connector response via Rust core handlers
///
/// This client maintains a cache of HTTP clients keyed by proxy configuration,
/// so repeated calls with the same proxy settings reuse the same connection pool.
pub struct ConnectorClient {
    base_http_config: NativeHttpOptions,
    client_cache: Arc<RwLock<HashMap<String, HttpClient>>>,
    config: ConnectorConfig,
}

// ── Internal macro: generate a ConnectorClient method for a payment flow ──────
//
// Each generated method follows the same round-trip pattern:
//   1. Build FfiRequestData from caller inputs
//   2. Call the flow-specific req_handler to build the connector HTTP request
//   3. Execute HTTP via the shared HttpClient
//   4. Call the flow-specific res_handler to parse the response
//
// Usage: impl_flow_method!(method_name, ReqType, ResType, req_handler_fn, res_handler_fn);
macro_rules! impl_flow_method {
    ($method:ident, $req_type:ty, $res_type:ty, $req_handler:ident, $res_handler:ident) => {
        pub async fn $method(
            &self,
            request: $req_type,
            metadata: &HashMap<String, String>,
            options: Option<RequestConfig>,
        ) -> Result<$res_type, SdkError> {
            use connector_service_ffi::handlers::payments::{$req_handler, $res_handler};

            let ffi_options = self.resolve_ffi_options(&options);
            let effective_http_config = self.resolve_http_options(options.as_ref());

            let ffi_request =
                build_ffi_request(request.clone(), metadata, &ffi_options).map_err(|e| {
                    SdkError::IntegrationError {
                        error_code: "SDK_INTERNAL_ERROR".to_string(),
                        error_message: format!("{:?}", e),
                        suggested_action: None,
                        doc_url: None,
                    }
                })?;
            let environment = Some(
                grpc_api_types::payments::Environment::try_from(ffi_options.environment).map_err(
                    |e| SdkError::IntegrationError {
                        error_code: "INVALID_ENVIRONMENT".to_string(),
                        error_message: format!("{:?}", e),
                        suggested_action: None,
                        doc_url: None,
                    },
                )?,
            );

            let connector_request = $req_handler(ffi_request, environment)
                .map_err(SdkError::from)?
                .ok_or_else(|| SdkError::IntegrationError {
                    error_code: "NO_REQUEST_GENERATED".to_string(),
                    error_message: "No connector request was generated".to_string(),
                    suggested_action: None,
                    doc_url: None,
                })?;

            let (body, boundary) = connector_request
                .body
                .as_ref()
                .map(|b| b.get_body_bytes())
                .transpose()
                .map_err(|e| SdkError::IntegrationError {
                    error_code: "BODY_EXTRACTION_FAILED".to_string(),
                    error_message: format!("{e}"),
                    suggested_action: None,
                    doc_url: None,
                })?
                .unwrap_or((None, None));
            let mut headers = connector_request.get_headers_map();
            if let Some(boundary) = boundary {
                headers.insert(
                    "content-type".to_string(),
                    format!("multipart/form-data; boundary={}", boundary),
                );
            }
            let http_req = ClientHttpRequest {
                url: connector_request.url.clone(),
                method: connector_request.method,
                headers,
                body,
            };
            let http_client = self
                .get_or_create_client(&effective_http_config)
                .map_err(SdkError::from)?;
            let http_response = http_client
                .execute(http_req, Some(effective_http_config))
                .await
                .map_err(SdkError::from)?;

            let mut header_map = http::HeaderMap::new();
            for (key, value) in &http_response.headers {
                if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) {
                    if let Ok(val) = http::header::HeaderValue::from_bytes(value.as_bytes()) {
                        header_map.insert(name, val);
                    }
                }
            }
            let response = Response {
                headers: Some(header_map),
                response: bytes::Bytes::from(http_response.body),
                status_code: http_response.status_code,
            };

            let ffi_request_for_res =
                build_ffi_request(request, metadata, &ffi_options).map_err(|e| {
                    SdkError::IntegrationError {
                        error_code: "SDK_INTERNAL_ERROR".to_string(),
                        error_message: format!("{:?}", e),
                        suggested_action: None,
                        doc_url: None,
                    }
                })?;
            $res_handler(ffi_request_for_res, response, environment).map_err(SdkError::from)
        }
    };
}

impl ConnectorClient {
    /// Initialize a new ConnectorClient.
    ///
    /// # Arguments
    /// * `config` - The ConnectorConfig (connector_config with typed auth, options with environment).
    /// * `options` - Optional RequestConfig for default http/vault settings.
    pub fn new(
        config: ConnectorConfig,
        options: Option<RequestConfig>,
    ) -> Result<Self, NetworkError> {
        let defaults = options.unwrap_or_default();

        // Map the Protobuf options to native transport options and store as base config
        let base_http_config = match defaults.http.as_ref() {
            Some(http_proto) => NativeHttpOptions::from(http_proto),
            None => NativeHttpOptions::default(),
        };

        Ok(Self {
            base_http_config,
            client_cache: Arc::new(RwLock::new(HashMap::new())),
            config,
        })
    }

    /// Builds FfiOptions from config. Environment comes from SdkOptions (immutable).
    fn resolve_ffi_options(&self, _options: &Option<RequestConfig>) -> FfiOptions {
        let environment = self
            .config
            .options
            .as_ref()
            .map(|o| o.environment)
            .unwrap_or(0);
        FfiOptions {
            environment,
            connector_config: self.config.connector_config.clone(),
        }
    }

    /// Merges client defaults with per-request HTTP overrides. Per-request wins per field.
    fn resolve_http_options(&self, options: Option<&RequestConfig>) -> NativeHttpOptions {
        let override_opts = options
            .and_then(|o| o.http.as_ref())
            .map(NativeHttpOptions::from)
            .unwrap_or_default();
        merge_http_options(&self.base_http_config, &override_opts)
    }

    /// Get or create a cached HTTP client based on the effective proxy configuration.
    fn get_or_create_client(
        &self,
        effective_config: &NativeHttpOptions,
    ) -> Result<HttpClient, NetworkError> {
        let cache_key = generate_proxy_cache_key(&effective_config.proxy);

        // Fast read path - check if client exists
        {
            let cache = self.client_cache.read().map_err(|e| NetworkError {
                code: NetworkErrorCode::ClientInitializationFailure,
                message: format!("HTTP client cache lock poisoned (read): {e}"),
                status_code: None,
            })?;
            if let Some(client) = cache.get(&cache_key) {
                return Ok(client.clone());
            }
        }

        // Slow write path - create new client
        let mut cache = self.client_cache.write().map_err(|e| NetworkError {
            code: NetworkErrorCode::ClientInitializationFailure,
            message: format!("HTTP client cache lock poisoned (write): {e}"),
            status_code: None,
        })?;

        // Double-check in case another thread created it
        if let Some(client) = cache.get(&cache_key) {
            return Ok(client.clone());
        }

        let new_client = HttpClient::new(effective_config.clone())?;
        cache.insert(cache_key, new_client.clone());
        Ok(new_client)
    }

{% for service in all_services %}
    // ── {{ service }} flows ───────────────────────────────────────────────────
{% for f in groups.get(service, []) %}
    impl_flow_method!(
        {{ get_flow_method_name(f.service, f.rpc) }},
        {{ f.request }},
        {{ f.response }},
        {{ f.name }}_req_handler,
        {{ f.name }}_res_handler
    );
{% endfor %}
{% for f in single_groups.get(service, []) %}
{% if f.service == "EventService" %}
    // ── {{ f.name }} — inbound only, no outgoing HTTP call ───────────────────
    // Connector identity is extracted from the config variant; auth fields are not required.
    pub fn {{ f.name }}(
        &self,
        request: {{ f.request }},
    ) -> Result<{{ f.response }}, SdkError> {
        use connector_service_ffi::bindings::utils::parse_webhook_metadata;
        use connector_service_ffi::handlers::payments::{{ f.name }}_handler;
        use connector_service_ffi::types::FfiRequestData;
        let ffi_options = self.resolve_ffi_options(&None);
        let environment = Some(
            grpc_api_types::payments::Environment::try_from(ffi_options.environment).map_err(
                |e| SdkError::IntegrationError {
                    error_code: "INVALID_ENVIRONMENT".to_string(),
                    error_message: format!("{:?}", e),
                    suggested_action: None,
                    doc_url: None,
                },
            )?,
        );
        let ffi_metadata = parse_webhook_metadata(&ffi_options).map_err(SdkError::from)?;
        let ffi_request = FfiRequestData {
            payload: request,
            extracted_metadata: ffi_metadata,
            masked_metadata: None,
        };
        {{ f.name }}_handler(ffi_request, environment).map_err(SdkError::from)
    }
{% else %}
    // TODO: Single-step flow {{ f.name }} needs different macro/implementation
{% endif %}
{% endfor %}
{% endfor %}
}

/// Internal helper to build the context-heavy FfiRequestData from raw inputs.
pub fn build_ffi_request<T>(
    payload: T,
    metadata: &HashMap<String, String>,
    options: &FfiOptions,
) -> Result<FfiRequestData<T>, SdkError> {
    let proto_config =
        options
            .connector_config
            .as_ref()
            .ok_or_else(|| SdkError::IntegrationError {
                error_code: "MISSING_CONNECTOR_CONFIG".to_string(),
                error_message: "Missing connector_config in FfiOptions".to_string(),
                suggested_action: Some(
                    "Provide connector_config when constructing ConnectorClient".to_string(),
                ),
                doc_url: None,
            })?;

    let config_variant =
        proto_config
            .config
            .as_ref()
            .ok_or_else(|| SdkError::IntegrationError {
                error_code: "MISSING_CONFIG_VARIANT".to_string(),
                error_message: "Missing config variant in ConnectorSpecificConfig".to_string(),
                suggested_action: Some(
                    "Set the connector-specific config (e.g. stripe.api_key)".to_string(),
                ),
                doc_url: None,
            })?;

    let connector =
        domain_types::connector_types::ConnectorVariant::foreign_try_from(config_variant.clone())
            .map_err(|e| SdkError::IntegrationError {
                error_code: "CONNECTOR_MAPPING_FAILED".to_string(),
                error_message: format!("Connector mapping failed: {e}"),
                suggested_action: None,
                doc_url: None,
            })?;

    let connector_config = ConnectorSpecificConfig::foreign_try_from(proto_config.clone())
        .map_err(|e| SdkError::IntegrationError {
            error_code: "CONNECTOR_CONFIG_MAPPING_FAILED".to_string(),
            error_message: format!("Connector config mapping failed: {e}"),
            suggested_action: None,
            doc_url: None,
        })?;

    let masked_metadata =
        ffi_headers_to_masked_metadata(metadata).map_err(|e| SdkError::IntegrationError {
            error_code: "METADATA_MAPPING_FAILED".to_string(),
            error_message: format!("Metadata mapping failed: {:?}", e),
            suggested_action: None,
            doc_url: None,
        })?;

    Ok(FfiRequestData {
        payload,
        extracted_metadata: FfiMetadataPayload {
            connector,
            connector_config: Some(connector_config),
        },
        masked_metadata: Some(masked_metadata),
    })
}
