use serde_json::Value;

use crate::canonical::CanonicalBillingDocument;
use crate::contracts::billing_provider_runtime_contract;
use crate::errors::BillingAdapterError;
use crate::{BillingConnectionMode, BillingProvider};

pub use athena_billing_core::provider_types::{
    BillingProviderAdapter, BillingProviderConnectionConfigDescriptor,
    BillingProviderConnectionFieldDescriptor, BillingProviderDescriptor,
    BillingProviderRefetchProjection, BillingProviderRefetchRequest, BillingProviderResourceKind,
    BillingWebhookEventSummary, BillingWebhookProjection, config_field,
};

pub mod mollie;
pub mod stripe;

pub const SUPPORTED_BILLING_WEBHOOK_PROVIDERS: &[&str] = &["mollie", "stripe"];

pub fn supports_billing_webhook_provider(provider: &str) -> bool {
    SUPPORTED_BILLING_WEBHOOK_PROVIDERS
        .iter()
        .any(|candidate| candidate.eq_ignore_ascii_case(provider.trim()))
}

fn adapter_for_provider(provider: BillingProvider) -> &'static dyn BillingProviderAdapter {
    match provider {
        BillingProvider::Mollie => &mollie::MOLLIE_PROVIDER_ADAPTER,
        BillingProvider::Stripe => &stripe::STRIPE_PROVIDER_ADAPTER,
    }
}

fn adapter_for_provider_name(
    provider: &str,
) -> Result<&'static dyn BillingProviderAdapter, BillingAdapterError> {
    let provider = BillingProvider::parse(provider).ok_or_else(|| {
        BillingAdapterError::UnsupportedProvider(provider.trim().to_ascii_lowercase())
    })?;
    Ok(adapter_for_provider(provider))
}

pub fn billing_provider_catalog() -> Vec<BillingProviderDescriptor> {
    SUPPORTED_BILLING_WEBHOOK_PROVIDERS
        .iter()
        .filter_map(|provider| BillingProvider::parse(provider))
        .map(|provider| {
            let mut descriptor = adapter_for_provider(provider).descriptor();
            descriptor.integration = Some(
                serde_json::to_value(billing_provider_runtime_contract(
                    provider,
                    "billing_document_upserted",
                ))
                .expect("billing provider runtime contract should serialize"),
            );
            descriptor
        })
        .collect()
}

pub fn billing_signature_header_name(provider: &str) -> Option<&'static str> {
    adapter_for_provider_name(provider)
        .ok()
        .and_then(BillingProviderAdapter::signature_header_name)
}

pub fn enrich_connection_config_for_provider(
    provider: &str,
    mode: BillingConnectionMode,
    credential_kind: &str,
    provider_profile_id: Option<&str>,
    config: Value,
) -> Result<Value, BillingAdapterError> {
    let Value::Object(mut config_map) = config else {
        return Err(BillingAdapterError::InvalidConfig(
            "config must be a JSON object".to_string(),
        ));
    };

    let adapter = adapter_for_provider_name(provider)?;
    adapter.normalize_connection_config(
        &mut config_map,
        mode,
        credential_kind,
        provider_profile_id,
    );

    let config = Value::Object(config_map);
    adapter.validate_connection_config(&config)?;
    Ok(config)
}

pub fn validate_connection_config_for_provider(
    provider: &str,
    config: &Value,
) -> Result<(), BillingAdapterError> {
    adapter_for_provider_name(provider)?.validate_connection_config(config)
}

pub async fn project_webhook_for_provider(
    provider: &str,
    config: &Value,
    body: &[u8],
    signature_headers: &[String],
) -> Result<BillingWebhookProjection, BillingAdapterError> {
    adapter_for_provider_name(provider)?
        .project_webhook(config, body, signature_headers)
        .await
}

pub async fn project_webhook_document_for_provider(
    provider: &str,
    config: &Value,
    body: &[u8],
    signature_headers: &[String],
) -> Result<Option<CanonicalBillingDocument>, BillingAdapterError> {
    project_webhook_for_provider(provider, config, body, signature_headers)
        .await
        .map(|projection| projection.document)
}

pub async fn project_refetch_for_provider(
    provider: &str,
    config: &Value,
    request: &BillingProviderRefetchRequest,
) -> Result<BillingProviderRefetchProjection, BillingAdapterError> {
    adapter_for_provider_name(provider)?
        .project_refetch(config, request)
        .await
}

#[cfg(test)]
mod tests {
    use crate::providers::{
        BillingProviderRefetchRequest, BillingProviderResourceKind, billing_provider_catalog,
        billing_signature_header_name, enrich_connection_config_for_provider,
        project_refetch_for_provider, project_webhook_document_for_provider,
        project_webhook_for_provider, supports_billing_webhook_provider,
        validate_connection_config_for_provider,
    };
    use crate::{BillingConnectionMode, BillingProvider};
    use hmac::{Hmac, Mac};
    use sha2::Sha256;

    #[test]
    fn supported_provider_catalog_is_case_insensitive() {
        assert!(supports_billing_webhook_provider("mollie"));
        assert!(supports_billing_webhook_provider("Stripe"));
        assert!(!supports_billing_webhook_provider("paypal"));
    }

    #[test]
    fn provider_catalog_tracks_runtime_implementation_state() {
        let catalog = billing_provider_catalog();
        assert_eq!(catalog.len(), 2);
        assert_eq!(catalog[0].provider, BillingProvider::Mollie);
        assert_eq!(catalog[0].implementation_status, "implemented");
        assert!(catalog[0].supports_webhook_ingestion);
        assert!(catalog[0].supports_reconcile);
        assert_eq!(
            catalog[0].supported_reconcile_resource_kinds,
            vec!["payment", "payment_link", "subscription", "invoice"]
        );
        assert_eq!(catalog[1].provider, BillingProvider::Stripe);
        assert_eq!(catalog[1].implementation_status, "implemented");
        assert!(catalog[1].supports_webhook_ingestion);
        assert!(catalog[1].supports_reconcile);
        assert_eq!(
            catalog[1].supported_reconcile_resource_kinds,
            vec!["payment", "payment_link", "subscription", "invoice"]
        );
        assert_eq!(
            catalog[0].connection_config.default_credential_kind,
            "api_key"
        );
        assert_eq!(
            catalog[1].connection_config.default_credential_kind,
            "secret_key"
        );
        assert_eq!(
            catalog[0]
                .integration
                .as_ref()
                .expect("mollie integration should be present")["verification"]["verificationModes"],
            serde_json::json!(["classic_unsigned", "next_gen_signature"])
        );
        assert_eq!(
            catalog[1]
                .integration
                .as_ref()
                .expect("stripe integration should be present")["athenaErrors"][0]["code"],
            "STRIPE_WEBHOOK_REJECTED"
        );
    }

    #[test]
    fn provider_catalog_surfaces_provider_connection_config_contracts() {
        let catalog = billing_provider_catalog();
        let mollie = &catalog[0];
        let stripe = &catalog[1];

        assert!(
            mollie
                .connection_config
                .fields
                .iter()
                .any(|field| field.key == "profileId"
                    && field.derived_from_connection_field.as_deref() == Some("providerProfileId"))
        );
        assert!(
            mollie
                .connection_config
                .fields
                .iter()
                .any(|field| field.key == "signingSecrets"
                    && field.default_value == Some(serde_json::json!([])))
        );
        assert!(
            stripe
                .connection_config
                .fields
                .iter()
                .any(|field| field.key == "apiKey" && field.secret)
        );
        assert!(
            stripe
                .connection_config
                .fields
                .iter()
                .any(|field| field.key == "accountId"
                    && field.derived_from_connection_field.as_deref() == Some("providerProfileId"))
        );
    }

    #[test]
    fn provider_metadata_exposes_signature_headers_and_config_defaults() {
        assert_eq!(
            billing_signature_header_name("mollie"),
            Some("x-mollie-signature")
        );
        assert_eq!(
            billing_signature_header_name("stripe"),
            Some("stripe-signature")
        );

        let mollie_config = enrich_connection_config_for_provider(
            "mollie",
            BillingConnectionMode::Test,
            "advanced_access_token",
            Some("pfl_123"),
            serde_json::json!({ "apiKey": "secret" }),
        )
        .expect("mollie config should enrich");
        assert_eq!(mollie_config["credentialKind"], "advanced_access_token");
        assert_eq!(mollie_config["profileId"], "pfl_123");

        let stripe_config = enrich_connection_config_for_provider(
            "stripe",
            BillingConnectionMode::Test,
            "secret_key",
            Some("acct_123"),
            serde_json::json!({ "apiKey": "sk_test_123" }),
        )
        .expect("stripe config should enrich");
        assert_eq!(stripe_config["accountId"], "acct_123");
        assert_eq!(
            stripe_config["webhookSigningSecrets"],
            serde_json::json!([])
        );
    }

    #[test]
    fn connection_config_validation_rejects_provider_specific_credential_mismatches() {
        let error = validate_connection_config_for_provider(
            "mollie",
            &serde_json::json!({
                "apiBaseUrl": "https://api.mollie.com",
                "credentialKind": "secret_key",
                "apiKey": "sk_test_123",
                "mode": "test",
                "signingSecrets": []
            }),
        )
        .unwrap_err();

        assert!(
            error.to_string().contains("unknown variant `secret_key`"),
            "unexpected error: {error}"
        );
    }

    #[tokio::test]
    async fn provider_projection_rejects_unsupported_provider() {
        let error =
            project_webhook_document_for_provider("paypal", &serde_json::json!({}), b"{}", &[])
                .await
                .unwrap_err();
        assert_eq!(
            error.to_string(),
            "billing provider 'paypal' is not supported yet"
        );
    }

    #[tokio::test]
    async fn provider_refetch_rejects_unsupported_provider() {
        let error = project_refetch_for_provider(
            "paypal",
            &serde_json::json!({}),
            &BillingProviderRefetchRequest {
                resource_kind: BillingProviderResourceKind::Payment,
                provider_ref: "pay_123".to_string(),
                provider_customer_ref: None,
            },
        )
        .await
        .unwrap_err();
        assert_eq!(
            error.to_string(),
            "billing provider 'paypal' is not supported yet"
        );
    }

    #[tokio::test]
    async fn mollie_next_gen_projection_surfaces_verified_event_summary() {
        let body = br#"{"resource":"event","id":"event_123","type":"customer.created","entityId":"cst_123","createdAt":"2024-12-16T15:57:04Z"}"#;
        let secret = "super-secret";
        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(body);
        let signature = format!("sha256={:x}", mac.finalize().into_bytes());

        let projection = project_webhook_for_provider(
            "mollie",
            &serde_json::json!({
                "apiBaseUrl": "https://api.mollie.com",
                "credentialKind": "api_key",
                "apiKey": "live_dummy",
                "mode": "live",
                "profileId": null,
                "signingSecrets": [secret]
            }),
            body,
            &[signature],
        )
        .await
        .expect("projection should parse");

        assert!(projection.document.is_none());
        assert_eq!(projection.event.provider, BillingProvider::Mollie);
        assert_eq!(projection.event.envelope_kind, "next_gen");
        assert_eq!(projection.event.verification_mode, "next_gen_signature");
        assert_eq!(projection.event.event_id.as_deref(), Some("event_123"));
        assert_eq!(
            projection.event.event_type.as_deref(),
            Some("customer.created")
        );
        assert_eq!(projection.event.entity_id.as_deref(), Some("cst_123"));
        assert_eq!(projection.payload["entityId"], "cst_123");
    }

    #[tokio::test]
    async fn stripe_payment_intent_projection_surfaces_verified_event_summary() {
        let body = br#"{"id":"evt_123","type":"payment_intent.succeeded","created":1734364624,"account":"acct_123","data":{"object":{"id":"pi_123","object":"payment_intent","status":"succeeded","amount":2495,"amount_received":2495,"currency":"eur","description":"Bike tires","metadata":{"campaign":"spring"},"created":1734364624}}}"#;
        let timestamp = chrono::Utc::now().timestamp();
        let mut mac = Hmac::<Sha256>::new_from_slice(b"whsec_test").unwrap();
        mac.update(format!("{timestamp}.{}", String::from_utf8_lossy(body)).as_bytes());
        let signature = format!("t={timestamp},v1={:x}", mac.finalize().into_bytes());

        let projection = project_webhook_for_provider(
            "stripe",
            &serde_json::json!({
                "apiBaseUrl": "https://api.stripe.com",
                "credentialKind": "secret_key",
                "apiKey": "sk_test_dummy",
                "mode": "test",
                "accountId": "acct_123",
                "webhookSigningSecrets": ["whsec_test"]
            }),
            body,
            &[signature],
        )
        .await
        .expect("projection should parse");

        assert_eq!(projection.event.provider, BillingProvider::Stripe);
        assert_eq!(projection.event.verification_mode, "stripe_signature");
        assert_eq!(projection.event.event_id.as_deref(), Some("evt_123"));
        assert_eq!(projection.event.entity_id.as_deref(), Some("pi_123"));
        assert!(matches!(
            projection.document,
            Some(crate::canonical::CanonicalBillingDocument::Payment(_))
        ));
    }
}
