//! Tenant context propagated through every authenticated request.
//!
//! M7.1 single source of truth for "who is this request for". Claims come
//! from a Keycloak-issued JWT and land here via [`crate::auth::require_jwt_auth`]
//! (enabled with the `axum` feature). Handlers reach into the request
//! extensions with the [`crate::tenant_ctx::TenantCtx`] extractor.
//!
//! The shape mirrors the JWT claim names the breakpilot-platform realm
//! emits (see `platform/orca-platform/dev/keycloak/realm-export.json`).
//! Stable contract — adding fields is fine; renaming is a breaking
//! change for every downstream product.

use serde::{Deserialize, Serialize};

/// Tenant lifecycle status from `PLATFORM_ARCHITECTURE.md §5c`.
///
/// Drives the `tenant_status` middleware:
/// * `Demo` / `Trial` / `Active` — full access.
/// * `Frozen` — read-only after cancel / non-payment. Mutating endpoints
///   return 402.
/// * `Archived` — data-retention window closed. Every endpoint returns 410.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TenantStatus {
    Demo,
    Trial,
    Active,
    Frozen,
    Archived,
}

impl TenantStatus {
    /// True for statuses that block write paths.
    pub fn is_frozen(&self) -> bool {
        matches!(self, TenantStatus::Frozen)
    }
    /// True for statuses that block every request.
    pub fn is_archived(&self) -> bool {
        matches!(self, TenantStatus::Archived)
    }
    /// True for the shared demo tenant — metering, billing, and audit
    /// export are skipped.
    pub fn is_demo(&self) -> bool {
        matches!(self, TenantStatus::Demo)
    }
}

impl std::fmt::Display for TenantStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Demo => write!(f, "demo"),
            Self::Trial => write!(f, "trial"),
            Self::Active => write!(f, "active"),
            Self::Frozen => write!(f, "frozen"),
            Self::Archived => write!(f, "archived"),
        }
    }
}

/// Org-level role baked into the JWT by the realm's protocol mapper.
/// `PLATFORM_ARCHITECTURE.md §6` is the canonical list.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum OrgRole {
    ItAdmin,
    Cxo,
    Finance,
    Legal,
    User,
    /// Anything we haven't enumerated yet — forwards-compatible.
    #[serde(other)]
    Unknown,
}

impl OrgRole {
    /// Parses a single role string (Keycloak emits these as `IT_ADMIN`,
    /// `CXO`, etc.). Round-trips with the JSON layer.
    pub fn parse(s: &str) -> Self {
        match s {
            "IT_ADMIN" => OrgRole::ItAdmin,
            "CXO" => OrgRole::Cxo,
            "FINANCE" => OrgRole::Finance,
            "LEGAL" => OrgRole::Legal,
            "USER" => OrgRole::User,
            _ => OrgRole::Unknown,
        }
    }
}

/// Everything we know about the requesting tenant at the moment a request
/// lands. Cheap to clone (every field is owned + small).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantContext {
    /// `tenants.id` from the platform's tenant-registry (UUID).
    pub tenant_id: String,
    /// Lowercase URL-safe slug. Useful for log lines + audit emit.
    pub tenant_slug: String,
    /// Org-level roles the authenticated user holds inside this tenant.
    /// Drives the per-handler RBAC in `M7.1-followup` PRs.
    pub org_roles: Vec<OrgRole>,
    /// Products this tenant is currently entitled to. Used to short-circuit
    /// MCP / API calls for unsubscribed products.
    pub products: Vec<String>,
    /// Customer plan (`starter` / `professional` / `enterprise`) — gates
    /// per-plan feature flags (e.g., MCP server is enterprise-only).
    pub plan: String,
    /// Lifecycle status — read by `require_tenant_status` middleware.
    pub status: TenantStatus,
    /// Keycloak user id of the requester (`sub` claim). Required for audit
    /// emit so we know WHO did the thing, not just WHICH tenant.
    pub user_id: String,
    /// Optional user-facing name from the `name` / `preferred_username`
    /// claim. Only used in audit + log lines.
    pub user_name: Option<String>,
}

impl TenantContext {
    /// True if the caller holds at least one of the listed roles.
    pub fn has_any_role(&self, roles: &[OrgRole]) -> bool {
        self.org_roles.iter().any(|r| roles.contains(r))
    }
}

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

    #[test]
    fn org_role_parses_known_values() {
        assert_eq!(OrgRole::parse("IT_ADMIN"), OrgRole::ItAdmin);
        assert_eq!(OrgRole::parse("CXO"), OrgRole::Cxo);
        assert_eq!(OrgRole::parse("USER"), OrgRole::User);
    }

    #[test]
    fn org_role_unknown_is_forward_compat() {
        assert_eq!(OrgRole::parse("FUTURE_ROLE"), OrgRole::Unknown);
    }

    #[test]
    fn tenant_status_predicates() {
        assert!(TenantStatus::Frozen.is_frozen());
        assert!(!TenantStatus::Active.is_frozen());
        assert!(TenantStatus::Archived.is_archived());
        assert!(TenantStatus::Demo.is_demo());
        assert!(!TenantStatus::Active.is_demo());
    }

    #[test]
    fn has_any_role_matches() {
        let ctx = TenantContext {
            tenant_id: "t1".into(),
            tenant_slug: "acme".into(),
            org_roles: vec![OrgRole::ItAdmin],
            products: vec![],
            plan: "professional".into(),
            status: TenantStatus::Active,
            user_id: "u".into(),
            user_name: None,
        };
        assert!(ctx.has_any_role(&[OrgRole::ItAdmin]));
        assert!(ctx.has_any_role(&[OrgRole::Cxo, OrgRole::ItAdmin]));
        assert!(!ctx.has_any_role(&[OrgRole::User, OrgRole::Cxo]));
    }
}
