//! Authentication policy trait and types.
//!
//! Engine implements `AuthPolicy` to declare auth decisions.
//! `bind_http!` generates `auth_middleware()` that calls these methods.

use std::sync::Arc;

/// File system access for pre-authentication checks.
///
/// Provides sync file IO with transaction context (trace_id + tx_name).
/// Type-erased — does not require knowing the transaction type `T`.
///
/// Used by `AuthPolicy::pre_auth_check()` to read configuration files
/// (e.g. checking if setup is completed) without requiring atomic flags.
pub struct AuthScopedFs {
    trace_id: [u8; 4],
    tx_name: &'static str,
}

impl AuthScopedFs {
    /// Create from raw trace_id and tx_name.
    #[doc(hidden)]
    pub fn new(trace_id: [u8; 4], tx_name: &'static str) -> Self {
        Self { trace_id, tx_name }
    }

    /// Read a file to string.
    pub fn read_to_string(&self, path: impl AsRef<std::path::Path>) -> crate::Critical<String> {
        match std::fs::read_to_string(path.as_ref()) {
            Ok(content) => {
                crate::Critical::ok(content).with_tx_context(self.trace_id, self.tx_name)
            }
            Err(e) => crate::Critical::error(
                "read_to_string",
                format!("{}: {}", path.as_ref().display(), e),
            )
            .with_tx_context(self.trace_id, self.tx_name),
        }
    }

    /// Check if a path exists.
    pub fn exists(&self, path: impl AsRef<std::path::Path>) -> bool {
        path.as_ref().exists()
    }
}

/// Pre-authentication hook injected by auth_middleware.
///
/// Contains the auth decision logic as a closure. Inserted into request
/// extensions by `auth_middleware`, extracted and called by `tx_guard`.
///
/// Returns `Some(Response)` to reject (e.g. 401/redirect), `None` to allow.
#[derive(Clone)]
#[doc(hidden)]
pub struct PreAuthHook(
    Arc<
        dyn Fn(&AuthScopedFs, &axum::extract::Request) -> Option<axum::response::Response>
            + Send
            + Sync,
    >,
);

impl PreAuthHook {
    /// Create a new hook from a closure.
    pub fn new(
        f: impl Fn(&AuthScopedFs, &axum::extract::Request) -> Option<axum::response::Response>
            + Send
            + Sync
            + 'static,
    ) -> Self {
        Self(Arc::new(f))
    }

    /// Execute the hook. Returns `Some(Response)` to reject, `None` to allow.
    pub fn check(
        &self,
        fs: &AuthScopedFs,
        req: &axum::extract::Request,
    ) -> Option<axum::response::Response> {
        (self.0)(fs, req)
    }
}

/// Result of pre-authentication business logic check.
pub enum PreAuthResult {
    /// Continue normal auth flow (check session cookie).
    Continue,
    /// Allow request without further checks.
    Allow,
    /// Redirect to the given URL.
    Redirect(String),
}

/// Authentication policy trait.
///
/// Implement this for your application state to declare all auth decisions.
/// The generated `auth_middleware()` calls these methods in order:
///
/// 1. `skip_auth()` — global bypass (e.g. dev mode)
/// 2. path whitelist check (auto-generated from bind_http! declarations)
/// 3. `pre_auth_check()` — request-level business logic (setup redirect, localhost skip, etc.)
/// 4. session cookie validation via `session_cookie_name()` + `validate_session()`
/// 5. `unauthorized_response()` — when all checks fail
pub trait AuthPolicy: Send + Sync + 'static {
    /// Whether to skip all authentication (e.g. `skip_auth` env var).
    fn skip_auth(&self) -> bool;

    /// The session cookie name (runtime value, e.g. "{prefix}_{port}").
    fn session_cookie_name(&self) -> &str;

    /// Validate a session token extracted from the cookie.
    fn validate_session(&self, token: &str) -> bool;

    /// Pre-authentication business logic check.
    ///
    /// Called after path whitelist check, before cookie validation.
    /// Use this for request-level decisions like:
    /// - Setup not completed → redirect to setup page
    /// - Default password → allow without auth
    /// - Localhost request → allow without auth
    ///
    /// `auth_fs` provides type-erased file IO for reading configuration
    /// (e.g. checking if setup is completed) without requiring cached state.
    fn pre_auth_check(
        &self,
        path: &str,
        req: &axum::extract::Request,
        auth_fs: &AuthScopedFs,
    ) -> PreAuthResult;

    /// Response when authentication fails (e.g. redirect to login page).
    fn unauthorized_response(&self) -> axum::response::Response;

    /// The current session token value (used by grant_auth to set the cookie).
    fn session_token(&self) -> &str;
}

/// Grant authentication: set session cookie and redirect.
///
/// Cookie details (path="/", httponly=true, max_age=7 days) are hat domain knowledge.
pub fn grant_auth(policy: &impl AuthPolicy, redirect_to: &str) -> axum::response::Response {
    use axum::response::IntoResponse;

    let cookie = crate::http::build_cookie(
        policy.session_cookie_name(),
        policy.session_token(),
        "/",
        true,
        Some(7 * 24 * 3600),
    );
    let mut headers = axum::http::HeaderMap::new();
    headers.insert(axum::http::header::SET_COOKIE, cookie.parse().unwrap());
    (headers, axum::response::Redirect::to(redirect_to)).into_response()
}

/// Revoke authentication: clear session cookie and redirect.
pub fn revoke_auth(policy: &impl AuthPolicy, redirect_to: &str) -> axum::response::Response {
    use axum::response::IntoResponse;

    let cookie = crate::http::build_clear_cookie(policy.session_cookie_name(), "/");
    let mut headers = axum::http::HeaderMap::new();
    headers.insert(axum::http::header::SET_COOKIE, cookie.parse().unwrap());
    (headers, axum::response::Redirect::to(redirect_to)).into_response()
}