//! Webhook delivery enqueue - shared between API and Worker crates.
//!
//! When an identity event occurs (entity.created, entity.merged, etc.),
//! callers use `enqueue_webhook_deliveries` to fan out delivery rows
//! for every active webhook that subscribes to that event type.

use sqlx::PgPool;
use uuid::Uuid;
use tracing::info;

/// Fan out a single event to all matching active webhooks for a tenant.
///
/// Returns the number of delivery rows inserted.
pub async fn enqueue_webhook_deliveries(
    pool: &PgPool,
    tenant_id: Uuid,
    event_id: Uuid,
    event_type: &str,
    payload: &serde_json::Value,
) -> crate::Result<u64> {
    // Set tenant context so RLS allows us to read webhooks for this tenant.
    let tid = tenant_id.to_string();
    sqlx::query!("SELECT set_config('request.tenant_id', $1::text, false)", tid)
        .fetch_one(pool)
        .await?;

    // Find all active webhooks whose event_types array contains this event_type.
    let rows = sqlx::query_scalar!(
        r#"SELECT id as "id!" FROM webhooks
           WHERE tenant_id = $1
             AND active = TRUE
             AND $2 = ANY(event_types)"#,
        tenant_id,
        event_type,
    )
    .fetch_all(pool)
    .await?;

    if rows.is_empty() {
        return Ok(0);
    }

    let mut inserted: u64 = 0;
    for webhook_id in &rows {
        sqlx::query!(
            r#"INSERT INTO webhook_deliveries
               (tenant_id, webhook_id, event_id, event_type, payload, status, next_retry_at)
               VALUES ($1, $2, $3, $4, $5, 'pending', NOW())"#,
            tenant_id,
            webhook_id,
            event_id,
            event_type,
            payload,
        )
        .execute(pool)
        .await?;
        inserted += 1;
    }

    if inserted > 0 {
        info!(
            tenant_id = %tenant_id,
            event_type = %event_type,
            event_id = %event_id,
            webhook_count = inserted,
            "Enqueued webhook deliveries"
        );
    }

    Ok(inserted)
}

/// Valid event types that webhooks can subscribe to.
pub const VALID_EVENT_TYPES: &[&str] = &[
    "entity.created",
    "entity.updated",
    "entity.merged",
    "entity.split",
    "entity.deleted",
    "reconciliation.completed",
    "reconciliation.failed",
    "source.ingested",
    "proposal.created",
    "proposal.reviewed",
];

/// Check whether all provided event types are valid.
pub fn validate_event_types(event_types: &[String]) -> std::result::Result<(), String> {
    for et in event_types {
        if !VALID_EVENT_TYPES.contains(&et.as_str()) {
            return Err(format!(
                "Invalid event type '{}'. Valid types: {}",
                et,
                VALID_EVENT_TYPES.join(", ")
            ));
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_event_types_all_valid() {
        let types: Vec<String> = vec![
            "entity.created".into(),
            "entity.merged".into(),
            "reconciliation.completed".into(),
        ];
        assert!(validate_event_types(&types).is_ok());
    }

    #[test]
    fn test_validate_event_types_empty_list() {
        let types: Vec<String> = vec![];
        assert!(validate_event_types(&types).is_ok());
    }

    #[test]
    fn test_validate_event_types_invalid_type() {
        let types = vec!["entity.created".into(), "entity.exploded".into()];
        let result = validate_event_types(&types);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("entity.exploded"), "Error should name the invalid type: {}", err);
        assert!(err.contains("Valid types:"), "Error should list valid types: {}", err);
    }

    #[test]
    fn test_validate_event_types_all_invalid() {
        let types = vec!["nope".into()];
        assert!(validate_event_types(&types).is_err());
    }

    #[test]
    fn test_validate_event_types_first_invalid_reported() {
        // Should fail on the first invalid type
        let types = vec!["bad.one".into(), "bad.two".into()];
        let err = validate_event_types(&types).unwrap_err();
        assert!(err.contains("bad.one"));
    }

    #[test]
    fn test_validate_event_types_all_known_types_accepted() {
        // Verify every constant in VALID_EVENT_TYPES passes validation
        let types: Vec<String> = VALID_EVENT_TYPES.iter().map(|s| s.to_string()).collect();
        assert!(validate_event_types(&types).is_ok());
    }

    #[test]
    fn test_valid_event_types_contains_expected_events() {
        assert!(VALID_EVENT_TYPES.contains(&"entity.created"));
        assert!(VALID_EVENT_TYPES.contains(&"entity.updated"));
        assert!(VALID_EVENT_TYPES.contains(&"entity.merged"));
        assert!(VALID_EVENT_TYPES.contains(&"entity.split"));
        assert!(VALID_EVENT_TYPES.contains(&"entity.deleted"));
        assert!(VALID_EVENT_TYPES.contains(&"reconciliation.completed"));
        assert!(VALID_EVENT_TYPES.contains(&"reconciliation.failed"));
        assert!(VALID_EVENT_TYPES.contains(&"source.ingested"));
        assert_eq!(VALID_EVENT_TYPES.len(), 10);
    }

    #[test]
    fn test_validate_event_types_case_sensitive() {
        // Event types are case-sensitive - "Entity.Created" is NOT valid
        let types = vec!["Entity.Created".into()];
        assert!(validate_event_types(&types).is_err());
    }
}
