use async_trait::async_trait;
use axum::{extract::FromRequestParts, http::request::Parts};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use std::sync::Arc;

use crate::{models::Claims, AppState};

/// Authenticated user information extracted from the bearer token.
pub struct AuthUser {
    pub user_id: String,
    pub email: String,
    pub display_name: String,
}

#[async_trait]
impl FromRequestParts<Arc<AppState>> for AuthUser {
    type Rejection = axum::http::StatusCode;

    /// Read the `Authorization: Bearer <token>` header and decode the JWT.
    ///
    /// Axum runs this automatically for any handler parameter of type
    /// `AuthUser`, which keeps individual handlers free from repeated token
    /// parsing logic.
    async fn from_request_parts(
        parts: &mut Parts,
        state: &Arc<AppState>,
    ) -> Result<Self, Self::Rejection> {
        let auth_header = parts
            .headers
            .get("Authorization")
            .and_then(|v| v.to_str().ok())
            .ok_or(axum::http::StatusCode::UNAUTHORIZED)?;

        let token = auth_header
            .strip_prefix("Bearer ")
            .ok_or(axum::http::StatusCode::UNAUTHORIZED)?;

        let claims = decode_token(token, &state.jwt_secret)
            .map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?;

        Ok(AuthUser {
            user_id: claims.sub,
            email: claims.email,
            display_name: claims.display_name,
        })
    }
}

/// Create a signed JWT for a logged-in user.
///
/// The token expires after seven days and carries the small amount of identity
/// data the server wants available on every request.
pub fn create_token(
    user_id: &str,
    email: &str,
    display_name: &str,
    secret: &str,
) -> Result<String, jsonwebtoken::errors::Error> {
    let expiration = chrono::Utc::now()
        .checked_add_signed(chrono::Duration::days(7))
        .unwrap()
        .timestamp() as usize;

    let claims = Claims {
        sub: user_id.to_string(),
        email: email.to_string(),
        display_name: display_name.to_string(),
        exp: expiration,
    };

    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_bytes()),
    )
}

/// Decode and validate a previously issued JWT.
pub fn decode_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::default(),
    )?;
    Ok(token_data.claims)
}
