//! Authentication handlers: register, login, refresh, delete account.

use axum::{extract::State, Json};
use bcrypt::{hash, verify};
use chrono::Utc;
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{
    error::AppError,
    middleware::{validate_refresh_token, AuthenticatedUser, Claims},
    AppState,
};

// ---------------------------------------------------------------------------
// Request / response shapes
// ---------------------------------------------------------------------------

/// Body for `POST /api/auth/register` and `POST /api/auth/login`.
#[derive(Debug, Deserialize)]
pub struct AuthRequest {
    pub username: String,
    pub password: String,
}

/// Body for `POST /api/auth/refresh`.
#[derive(Debug, Deserialize)]
pub struct RefreshRequest {
    pub refresh_token: String,
}

/// Successful auth response — contains both tokens.
#[derive(Debug, Serialize)]
pub struct AuthResponse {
    pub access_token: String,
    pub refresh_token: String,
}

/// Successful refresh response — contains only the new access token.
#[derive(Debug, Serialize)]
pub struct RefreshResponse {
    pub access_token: String,
}

// ---------------------------------------------------------------------------
// Internal database row type
// ---------------------------------------------------------------------------

/// User row fetched from the database during login.
/// Fields are `Option<String>` because sqlx treats all SQLite TEXT columns
/// as nullable regardless of the NOT NULL constraint in the schema.
struct UserRow {
    id: Option<String>,
    password_hash: Option<String>,
}

// ---------------------------------------------------------------------------
// bcrypt cost used for password hashing
// ---------------------------------------------------------------------------

/// bcrypt work factor. Cost 12 ≈ 300 ms on modern hardware — balances security against registration latency.
const BCRYPT_COST: u32 = 12;

// ---------------------------------------------------------------------------
// Token generation helpers
// ---------------------------------------------------------------------------

/// Encode a JWT access token (24-hour expiry) for `user_id`.
pub fn make_access_token(user_id: &str, secret: &str) -> Result<String, AppError> {
    let exp = (Utc::now() + chrono::Duration::hours(24)).timestamp() as usize;
    let claims = Claims {
        sub: user_id.to_string(),
        exp,
        kind: "access".to_string(),
    };
    encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
        .map_err(|e| AppError::Internal(e.to_string()))
}

/// Encode a JWT refresh token (30-day expiry) for `user_id`.
pub fn make_refresh_token(user_id: &str, secret: &str) -> Result<String, AppError> {
    let exp = (Utc::now() + chrono::Duration::days(30)).timestamp() as usize;
    let claims = Claims {
        sub: user_id.to_string(),
        exp,
        kind: "refresh".to_string(),
    };
    encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
        .map_err(|e| AppError::Internal(e.to_string()))
}

// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------

/// `POST /api/auth/register` — create a new account and return tokens.
/// Minimum and maximum allowed username lengths.
const USERNAME_MIN: usize = 3;
const USERNAME_MAX: usize = 32;
/// Minimum password length.
const PASSWORD_MIN: usize = 8;

/// Returns `true` if every character in `s` is ASCII alphanumeric or `_`.
fn username_chars_ok(s: &str) -> bool {
    s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}

pub async fn register(
    State(state): State<AppState>,
    Json(body): Json<AuthRequest>,
) -> Result<Json<AuthResponse>, AppError> {
    // Validate username: 3–32 characters, alphanumeric + underscores only.
    let trimmed = body.username.trim();
    if trimmed.len() < USERNAME_MIN || trimmed.len() > USERNAME_MAX {
        return Err(AppError::BadRequest(format!(
            "username must be {USERNAME_MIN}–{USERNAME_MAX} characters"
        )));
    }
    if !username_chars_ok(trimmed) {
        return Err(AppError::BadRequest(
            "username may only contain letters, digits, and underscores".into(),
        ));
    }
    // Validate password: minimum 8 characters.
    if body.password.len() < PASSWORD_MIN {
        return Err(AppError::BadRequest(format!(
            "password must be at least {PASSWORD_MIN} characters"
        )));
    }

    let username = trimmed.to_string();

    // Check for duplicate username. SQLite returns TEXT as nullable so we
    // flatten the Option<Option<String>> produced by fetch_optional.
    let existing: Option<String> = sqlx::query_scalar!(
        "SELECT id FROM users WHERE username = ?",
        username
    )
    .fetch_optional(&state.pool)
    .await?
    .flatten();

    if existing.is_some() {
        tracing::warn!(username = %username, "register: username already taken");
        return Err(AppError::UsernameTaken);
    }

    let user_id = Uuid::new_v4().to_string();
    let password_hash = hash(&body.password, BCRYPT_COST)?;
    let now = Utc::now().to_rfc3339();

    sqlx::query!(
        "INSERT INTO users (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)",
        user_id,
        username,
        password_hash,
        now
    )
    .execute(&state.pool)
    .await?;

    Ok(Json(AuthResponse {
        access_token: make_access_token(&user_id, &state.jwt_secret)?,
        refresh_token: make_refresh_token(&user_id, &state.jwt_secret)?,
    }))
}

/// `POST /api/auth/login` — verify credentials and return tokens.
pub async fn login(
    State(state): State<AppState>,
    Json(body): Json<AuthRequest>,
) -> Result<Json<AuthResponse>, AppError> {
    let username = body.username.trim().to_string();
    let row = sqlx::query_as!(
        UserRow,
        "SELECT id, password_hash FROM users WHERE username = ?",
        username
    )
    .fetch_optional(&state.pool)
    .await?;

    let row = row.ok_or(AppError::InvalidCredentials)?;
    let row_id = row.id.ok_or_else(|| AppError::Internal("user id missing".into()))?;
    let row_hash = row.password_hash.ok_or_else(|| AppError::Internal("password hash missing".into()))?;

    let valid = verify(&body.password, &row_hash)?;
    if !valid {
        tracing::warn!(username = %username, "login: invalid password");
        return Err(AppError::InvalidCredentials);
    }

    Ok(Json(AuthResponse {
        access_token: make_access_token(&row_id, &state.jwt_secret)?,
        refresh_token: make_refresh_token(&row_id, &state.jwt_secret)?,
    }))
}

/// `POST /api/auth/refresh` — exchange a refresh token for a new access token.
pub async fn refresh(
    State(state): State<AppState>,
    Json(body): Json<RefreshRequest>,
) -> Result<Json<RefreshResponse>, AppError> {
    let claims = validate_refresh_token(&body.refresh_token, &state.jwt_secret)?;

    Ok(Json(RefreshResponse {
        access_token: make_access_token(&claims.sub, &state.jwt_secret)?,
    }))
}

/// `DELETE /api/account` — permanently delete the authenticated user's account.
///
/// All related rows are removed via `ON DELETE CASCADE` in the schema.
pub async fn delete_account(
    State(state): State<AppState>,
    user: AuthenticatedUser,
) -> Result<Json<serde_json::Value>, AppError> {
    sqlx::query!("DELETE FROM users WHERE id = ?", user.user_id)
        .execute(&state.pool)
        .await?;

    Ok(Json(serde_json::json!({ "ok": true })))
}

#[cfg(test)]
mod tests {
    use super::*;
    use jsonwebtoken::{decode, DecodingKey, Validation};

    const TEST_SECRET: &str = "test_secret_for_unit_tests_only";

    fn decode_token(token: &str) -> Claims {
        let mut validation = Validation::default();
        validation.leeway = 60;
        decode::<Claims>(
            token,
            &DecodingKey::from_secret(TEST_SECRET.as_bytes()),
            &validation,
        )
        .unwrap()
        .claims
    }

    #[test]
    fn make_access_token_decodes_with_correct_claims() {
        let token = make_access_token("user-123", TEST_SECRET).unwrap();
        let claims = decode_token(&token);
        assert_eq!(claims.sub, "user-123");
        assert_eq!(claims.kind, "access");
        let now = Utc::now().timestamp() as usize;
        // expiry should be roughly 24 hours in the future (allow ±60s for test execution)
        assert!(claims.exp > now + 86_400 - 60);
        assert!(claims.exp < now + 86_400 + 60);
    }

    #[test]
    fn make_refresh_token_decodes_with_correct_claims() {
        let token = make_refresh_token("user-456", TEST_SECRET).unwrap();
        let claims = decode_token(&token);
        assert_eq!(claims.sub, "user-456");
        assert_eq!(claims.kind, "refresh");
        let now = Utc::now().timestamp() as usize;
        // expiry should be roughly 30 days in the future (allow ±60s for test execution)
        assert!(claims.exp > now + 30 * 86_400 - 60);
        assert!(claims.exp < now + 30 * 86_400 + 60);
    }

    #[test]
    fn make_access_token_wrong_secret_fails_decode() {
        let token = make_access_token("user-789", TEST_SECRET).unwrap();
        let result = decode::<Claims>(
            &token,
            &DecodingKey::from_secret(b"wrong_secret"),
            &Validation::default(),
        );
        assert!(result.is_err(), "decoding with wrong secret must fail");
    }

    #[test]
    fn access_and_refresh_tokens_have_different_kinds() {
        let access = make_access_token("u", TEST_SECRET).unwrap();
        let refresh = make_refresh_token("u", TEST_SECRET).unwrap();
        let a_claims = decode_token(&access);
        let r_claims = decode_token(&refresh);
        assert_ne!(a_claims.kind, r_claims.kind);
    }

    #[test]
    fn username_chars_ok_accepts_alphanumeric_and_underscore() {
        assert!(username_chars_ok("alice"));
        assert!(username_chars_ok("Alice_123"));
        assert!(username_chars_ok("UPPER_case_99"));
    }

    #[test]
    fn username_chars_ok_rejects_special_chars() {
        assert!(!username_chars_ok("ali ce"));   // space
        assert!(!username_chars_ok("ali-ce"));   // hyphen
        assert!(!username_chars_ok("ali.ce"));   // dot
        assert!(!username_chars_ok("ali@ce"));   // at
        assert!(!username_chars_ok("ali!ce"));   // exclamation
    }

    #[test]
    fn username_chars_ok_accepts_empty_string() {
        // The length check in `register` guards against empty usernames;
        // this function only validates characters, so empty is technically ok.
        assert!(username_chars_ok(""));
    }

    #[test]
    fn username_chars_ok_rejects_unicode_letters() {
        // Non-ASCII characters must be rejected even if they look like letters.
        assert!(!username_chars_ok("héro"));
        assert!(!username_chars_ok("用户"));
    }
}
