// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

//! Audit logging for key server token lifecycle events.
//!
//! Every event is emitted via `tracing::info!` with structured fields, making
//! the audit trail queryable by any log aggregator (Loki, `CloudWatch`, Datadog).
//!
//! # Events
//!
//! | Event | When |
//! |-------|------|
//! | `token.issued` | A new temporary token is successfully issued |
//! | `token.used` | A temporary token is validated for a request |
//! | `token.expired` | A token is rejected because its `exp` has passed |
//! | `token.revoked` | A token is explicitly revoked via `DELETE /auth/token/{jti}` |
//! | `token.denied` | OIDC verification or policy matching failed |
//! | `token.invalid` | The token string is structurally invalid |

use std::net::IpAddr;

use serde::Serialize;

use super::{oidc::VerifiedIdentity, store::TemporaryToken};

/// Structured audit event emitted for every token lifecycle transition.
#[derive(Debug, Serialize)]
pub struct AuditEvent {
    /// Event type string (e.g., `"token.issued"`).
    pub event: &'static str,
    /// Identity associated with the event (present for most events).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identity: Option<VerifiedIdentity>,
    /// JTI of the affected token.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_jti: Option<String>,
    /// Allowed backends (for `token.issued`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backends: Option<Vec<String>>,
    /// Allowed tools (for `token.issued`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tools: Option<Vec<String>>,
    /// Rate limit in requests/minute (for `token.issued`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rate_limit: Option<u32>,
    /// Client IP address (when available).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_ip: Option<IpAddr>,
    /// Human-readable reason for denial or error events.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl AuditEvent {
    /// Construct a `token.issued` event.
    #[must_use]
    pub fn issued(token: &TemporaryToken) -> Self {
        Self {
            event: "token.issued",
            identity: Some(token.identity.clone()),
            token_jti: Some(token.jti.clone()),
            backends: Some(token.scopes.backends.clone()),
            tools: Some(token.scopes.tools.clone()),
            rate_limit: Some(token.scopes.rate_limit),
            client_ip: token.client_ip,
            reason: None,
        }
    }

    /// Construct a `token.used` event.
    #[must_use]
    pub fn used(token: &TemporaryToken, client_ip: Option<IpAddr>) -> Self {
        Self {
            event: "token.used",
            identity: Some(token.identity.clone()),
            token_jti: Some(token.jti.clone()),
            backends: None,
            tools: None,
            rate_limit: None,
            client_ip,
            reason: None,
        }
    }

    /// Construct a `token.expired` event.
    #[must_use]
    pub fn expired(jti: &str, identity: VerifiedIdentity) -> Self {
        Self {
            event: "token.expired",
            identity: Some(identity),
            token_jti: Some(jti.to_string()),
            backends: None,
            tools: None,
            rate_limit: None,
            client_ip: None,
            reason: None,
        }
    }

    /// Construct a `token.revoked` event.
    #[must_use]
    pub fn revoked(jti: &str, identity: Option<VerifiedIdentity>) -> Self {
        Self {
            event: "token.revoked",
            identity,
            token_jti: Some(jti.to_string()),
            backends: None,
            tools: None,
            rate_limit: None,
            client_ip: None,
            reason: None,
        }
    }

    /// Construct a `token.denied` event (OIDC verification or policy failure).
    #[must_use]
    pub fn denied(reason: impl Into<String>, client_ip: Option<IpAddr>) -> Self {
        Self {
            event: "token.denied",
            identity: None,
            token_jti: None,
            backends: None,
            tools: None,
            rate_limit: None,
            client_ip,
            reason: Some(reason.into()),
        }
    }

    /// Construct a `token.invalid` event.
    #[must_use]
    pub fn invalid(reason: impl Into<String>, client_ip: Option<IpAddr>) -> Self {
        Self {
            event: "token.invalid",
            identity: None,
            token_jti: None,
            backends: None,
            tools: None,
            rate_limit: None,
            client_ip,
            reason: Some(reason.into()),
        }
    }
}

/// Emit an audit event via `tracing::info!` with structured fields.
///
/// The event is serialized as a JSON blob in the `audit` field, making it
/// easy to extract in log aggregators:
///
/// ```text
/// INFO key_server::audit audit={"event":"token.issued","identity":...}
/// ```
pub fn emit(event: &AuditEvent) {
    match serde_json::to_string(event) {
        Ok(ref json) => tracing::info!(audit = %json, "key_server audit"),
        Err(ref e) => tracing::warn!(error = %e, "Failed to serialize audit event"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::key_server::{
        oidc::VerifiedIdentity,
        store::{InMemoryTokenStore, TemporaryToken, TokenScopes},
    };
    use std::time::{Duration, SystemTime, UNIX_EPOCH};

    fn make_identity() -> VerifiedIdentity {
        VerifiedIdentity {
            subject: "sub123".to_string(),
            email: "alice@company.com".to_string(),
            name: Some("Alice".to_string()),
            groups: Vec::new(),
            issuer: "https://accounts.google.com".to_string(),
        }
    }

    fn make_token() -> TemporaryToken {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or(Duration::ZERO)
            .as_secs();
        TemporaryToken {
            jti: InMemoryTokenStore::generate_jti(),
            token: InMemoryTokenStore::generate_bearer(),
            identity: make_identity(),
            scopes: TokenScopes {
                backends: vec!["tavily".to_string()],
                tools: Vec::new(),
                rate_limit: 100,
            },
            iat: now,
            exp: now + 3600,
            client_ip: None,
        }
    }

    #[test]
    fn issued_event_has_correct_type() {
        // GIVEN: a token
        let token = make_token();

        // WHEN: build issued event
        let event = AuditEvent::issued(&token);

        // THEN: event type is "token.issued" and fields are populated
        assert_eq!(event.event, "token.issued");
        assert!(event.identity.is_some());
        assert!(event.token_jti.is_some());
        assert!(event.backends.is_some());
    }

    #[test]
    fn used_event_has_correct_type() {
        // GIVEN: a token
        let token = make_token();

        // WHEN: build used event
        let event = AuditEvent::used(&token, None);

        // THEN: event type is "token.used", no scope fields
        assert_eq!(event.event, "token.used");
        assert!(event.backends.is_none());
    }

    #[test]
    fn denied_event_contains_reason() {
        // GIVEN/WHEN: build denied event with a reason
        let event = AuditEvent::denied("Policy not matched", None);

        // THEN: event type and reason are set
        assert_eq!(event.event, "token.denied");
        assert_eq!(event.reason.as_deref(), Some("Policy not matched"));
        assert!(event.identity.is_none());
    }

    #[test]
    fn revoked_event_has_jti() {
        // GIVEN/WHEN: build revoked event
        let event = AuditEvent::revoked("some-jti", Some(make_identity()));

        // THEN: event type and JTI set
        assert_eq!(event.event, "token.revoked");
        assert_eq!(event.token_jti.as_deref(), Some("some-jti"));
    }

    #[test]
    fn events_serialize_to_json() {
        // GIVEN: various event types
        let token = make_token();
        let events = vec![
            AuditEvent::issued(&token),
            AuditEvent::used(&token, None),
            AuditEvent::denied("test", None),
            AuditEvent::revoked("jti", None),
            AuditEvent::invalid("bad token", None),
        ];

        // WHEN/THEN: all serialize without error
        for event in events {
            let result = serde_json::to_string(&event);
            assert!(result.is_ok(), "Serialization failed: {result:?}");
        }
    }

    #[test]
    fn emit_does_not_panic() {
        // GIVEN: any audit event
        let token = make_token();
        let event = AuditEvent::issued(&token);

        // WHEN: emit (just check it doesn't panic)
        emit(&event);
    }
}
