//! Multi-tenancy middleware for extracting and enforcing `org_id`.
//!
//! Reads the tenant identifier from JWT claims or request headers and injects
//! it into the request extensions as [`TenantContext`] so downstream handlers
//! can scope database queries to the correct organisation.

use axum::{body::Body, http::Request, middleware::Next, response::Response};
use tracing::debug;

use crate::middleware::oidc_auth::AuthUser;

/// Extract `org_id` from request and add to context
///
/// # Security
///
/// Only accepts X-Org-ID header from **authenticated requests**. Unauthenticated
/// requests cannot set tenant context, preventing tenant isolation bypass attacks.
///
/// # Tenant Routing Priority
/// 1. **Authenticated + X-Org-ID header**: Use header value (validated by auth)
/// 2. **Authenticated, no header**: No tenant scope (user-level access)
/// 3. **Unauthenticated**: No tenant scope, X-Org-ID header is ignored
pub async fn tenant_middleware(mut request: Request<Body>, next: Next) -> Response {
    let mut org_id: Option<String> = None;

    // SECURITY: Only accept X-Org-ID from authenticated requests.
    // Unauthenticated requests MUST NOT be able to set tenant context.
    if let Some(auth_user) = request.extensions().get::<AuthUser>() {
        if let Some(header_value) = request.headers().get("X-Org-ID") {
            if let Ok(org_id_str) = header_value.to_str() {
                org_id = Some(org_id_str.to_string());
                debug!(
                    user_id = %auth_user.0.user_id,
                    org_id = %org_id_str,
                    source = "header",
                    "Extracted org_id from X-Org-ID header for authenticated user"
                );
            }
        }
    } else if request.headers().contains_key("X-Org-ID") {
        tracing::warn!("Rejected X-Org-ID header from unauthenticated request");
    }

    // Store org_id in request extensions for downstream handlers
    request.extensions_mut().insert(TenantContext { org_id });

    next.run(request).await
}

/// Tenant context extracted from request
#[derive(Debug, Clone)]
pub struct TenantContext {
    /// Organization/Tenant ID for multi-tenancy enforcement
    pub org_id: Option<String>,
}

impl TenantContext {
    /// Check if tenant is explicitly set
    #[must_use]
    pub const fn is_tenant_scoped(&self) -> bool {
        self.org_id.is_some()
    }

    /// Get tenant ID if available
    #[must_use]
    pub fn get_org_id(&self) -> Option<&str> {
        self.org_id.as_deref()
    }

    /// Require tenant ID (for operations that must be tenant-scoped)
    ///
    /// # Errors
    ///
    /// Returns a `String` error message if no `org_id` is present on this context.
    pub fn require_org_id(&self) -> Result<&str, String> {
        self.org_id
            .as_deref()
            .ok_or_else(|| "Request must be tenant-scoped (missing org_id)".to_string())
    }
}
