use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
use serde::{Deserialize, Serialize};

/// JWT claims — must match `contracts::auth_middleware::Claims` field-for-field
/// so that tokens generated here can be decoded there.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub sub: String,          // user_id (UUID string)
    pub email: String,
    pub roles: Vec<String>,
    pub active_role: String,
    pub exp: usize,
    pub iat: usize,
}

pub struct JwtTokens {
    pub access_token: String,
    pub refresh_token: String,
}

/// Generate an access token + a random refresh token string.
///
/// The refresh token is NOT a JWT — it is a random opaque string stored
/// (hashed) in the `refresh_tokens` table.
pub fn generate_tokens(
    user_id: String,
    email: String,
    roles: Vec<String>,
    active_role: Option<String>,
    jwt_secret: &str,
) -> anyhow::Result<JwtTokens> {
    let now = Utc::now();
    let access_exp = now + Duration::minutes(15);

    let active_role = active_role
        .or_else(|| roles.first().cloned())
        .unwrap_or_default();

    let claims = Claims {
        sub: user_id,
        email,
        roles,
        active_role,
        iat: now.timestamp() as usize,
        exp: access_exp.timestamp() as usize,
    };

    let access_token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(jwt_secret.as_bytes()),
    )?;

    // Refresh token is an opaque random string stored in DB, not a JWT.
    let refresh_token = uuid::Uuid::new_v4().to_string() + &uuid::Uuid::new_v4().to_string();

    Ok(JwtTokens {
        access_token,
        refresh_token: refresh_token.replace('-', ""),
    })
}

pub fn verify_access_token(token: &str, jwt_secret: &str) -> anyhow::Result<Claims> {
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(jwt_secret.as_bytes()),
        &Validation::new(Algorithm::HS256),
    )?;
    Ok(token_data.claims)
}
