use auth::{
    crypto::{hash_password, verify_password},
    jwt::generate_tokens,
};
use axum::{
    extract::State,
    http::{header::SET_COOKIE, StatusCode},
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use db::models::employee::EmployeeRepository;
use serde::{Deserialize, Serialize};
use contracts::auth_middleware::AuthUser;
use crate::AppState;

// Retrigger: force an isolated single-service rebuild of `employees` — the
// prior concurrent 20-service push (8a29b81) landed a digest in gitops that
// the registry doesn't actually have (same failure mode as the earlier
// documented incident in 512eb72/ea622a4).
pub fn router() -> Router<AppState> {
    // Includes forgot-password / reset-password against EmployeeRepository.
    Router::new()
        .route("/login",           post(login))
        .route("/logout",          post(logout))
        .route("/refresh",         post(refresh))
        .route("/session",         get(session))
        .route("/forgot-password", post(forgot_password))
        .route("/reset-password",  post(reset_password))
}

#[derive(Deserialize)]
pub struct LoginPayload {
    pub email:    String,
    pub password: String,
    pub captcha_id:     String,
    pub captcha_answer: String,
}

#[derive(Serialize)]
#[allow(dead_code)]
pub struct SessionEmployee {
    pub id:             String,
    pub email:          String,
    pub full_name:      String,
    pub role_code:      String,
}

#[derive(Serialize)]
pub struct ErrorResponse {
    pub error:       String,
    pub code:        String,
}

fn err(status: StatusCode, msg: &str, code: &str) -> (StatusCode, Json<ErrorResponse>) {
    (status, Json(ErrorResponse {
        error:       msg.to_string(),
        code:        code.to_string(),
    }))
}

async fn login(
    State(state): State<AppState>,
    Json(payload): Json<LoginPayload>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let email = payload.email.to_lowercase();
    let mut redis = state.redis.clone();

    // Captcha must be verified (and is consumed, single-use) before anything else.
    if !cache::captcha::verify(&mut redis, &payload.captcha_id, &payload.captcha_answer).await {
        return Err(err(StatusCode::BAD_REQUEST, "Invalid or expired captcha", "CAPTCHA_FAILED"));
    }

    if !cache::rate_limit::check_admin_login(&mut redis, &email).await.unwrap_or(true) {
        return Err(err(StatusCode::TOO_MANY_REQUESTS, "Too many login attempts. Try again in 15 minutes.", "RATE_LIMITED"));
    }

    let employee = EmployeeRepository::get_by_email(&state.pool, &email)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, email = %email, "employee login: get_by_email failed");
            err(StatusCode::INTERNAL_SERVER_ERROR, "DB error", "DB_ERROR")
        })?
        .ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Invalid credentials", "INVALID_CREDENTIALS"))?;

    if employee.status != "ACTIVE" {
        return Err(err(StatusCode::FORBIDDEN, "Account not active", "ACCOUNT_INACTIVE"));
    }

    let is_valid = verify_password(&payload.password, &employee.password_hash)
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "INTERNAL_ERROR"))?;
    
    if !is_valid {
        return Err(err(StatusCode::UNAUTHORIZED, "Invalid credentials", "INVALID_CREDENTIALS"));
    }

    let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
    
    // Internal Staff Roles
    let roles = vec![employee.role_code.clone()];
    
    let tokens = generate_tokens(
        employee.id.to_string(),
        employee.email.clone(),
        roles.clone(),
        roles.first().cloned(),
        &jwt_secret,
    )
    .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "TOKEN_ERROR"))?;

    // Store session
    EmployeeRepository::store_session(&state.pool, employee.id, &tokens.refresh_token, chrono::Utc::now() + chrono::Duration::days(30))
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;

    let cookie = format!(
        "nxtgauge_admin_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000",
        tokens.refresh_token
    );

    Ok((StatusCode::OK, [(SET_COOKIE, cookie)], Json(serde_json::json!({
        "access_token": tokens.access_token,
        "token_type":   "Bearer",
        "user": {
            "id":             employee.id.to_string(),
            "email":          employee.email,
            "full_name":      format!("{} {}", employee.first_name, employee.last_name),
            "role_code":      employee.role_code,
        }
    }))))
}

async fn logout(
    _req: axum::http::Request<axum::body::Body>,
) -> impl IntoResponse {
    let clear = "nxtgauge_admin_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0";
    (StatusCode::OK, [(SET_COOKIE, clear)], Json(serde_json::json!({ "message": "Logged out" })))
}

/// POST /api/admin/auth/refresh
///
/// Exchanges the HttpOnly refresh-token cookie for a new 15-minute access
/// token, rotating the underlying session row (revoke old, store new).
/// Called by the admin frontend on user activity to implement a sliding
/// session window: active admins never see a 15-minute timeout, idle ones do.
async fn refresh(
    State(state): State<AppState>,
    req: axum::http::Request<axum::body::Body>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let cookie_header = req
        .headers()
        .get(axum::http::header::COOKIE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");

    let token = cookie_header
        .split(';')
        .map(str::trim)
        .find_map(|p| p.strip_prefix("nxtgauge_admin_token="))
        .ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Refresh token missing", "REFRESH_TOKEN_INVALID"))?;

    let session = EmployeeRepository::get_valid_session_by_token(&state.pool, token)
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?
        .ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Refresh token expired", "REFRESH_TOKEN_INVALID"))?;

    let employee = EmployeeRepository::get_by_id(&state.pool, session.employee_id)
        .await
        .map_err(|_| err(StatusCode::UNAUTHORIZED, "Employee not found", "INVALID_CREDENTIALS"))?
        .ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Employee not found", "INVALID_CREDENTIALS"))?;

    if employee.status != "ACTIVE" {
        return Err(err(StatusCode::FORBIDDEN, "Account not active", "ACCOUNT_INACTIVE"));
    }

    // Rotate: revoke old session, issue a new one.
    EmployeeRepository::revoke_session(&state.pool, session.id)
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;

    let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
    let roles = vec![employee.role_code.clone()];

    let tokens = generate_tokens(
        employee.id.to_string(),
        employee.email.clone(),
        roles.clone(),
        roles.first().cloned(),
        &jwt_secret,
    )
    .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "TOKEN_ERROR"))?;

    EmployeeRepository::store_session(&state.pool, employee.id, &tokens.refresh_token, chrono::Utc::now() + chrono::Duration::days(30))
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;

    let new_cookie = format!(
        "nxtgauge_admin_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000",
        tokens.refresh_token
    );

    Ok((StatusCode::OK, [(SET_COOKIE, new_cookie)], Json(serde_json::json!({
        "access_token": tokens.access_token,
        "expires_in":   900
    }))))
}

async fn session(
    auth: AuthUser,
    _state: State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    // For now, allow simple return from AuthUser if it matches internal structure
    // In future, fetch from employee repository for fresh data
    Ok(Json(serde_json::json!({
        "id":        auth.user_id.to_string(),
        "email":     auth.email,
        "role_code": auth.claims.active_role,
    })))
}

#[derive(Deserialize)]
pub struct ForgotPasswordPayload {
    pub email: String,
}

#[derive(Deserialize)]
pub struct ResetPasswordPayload {
    pub code: String,
    pub new_password: String,
}

/// POST /api/admin/auth/forgot-password
/// Always responds 200 regardless of whether the email exists, to avoid
/// leaking which addresses have employee accounts.
async fn forgot_password(
    State(state): State<AppState>,
    Json(payload): Json<ForgotPasswordPayload>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let silent_ok = (StatusCode::OK, Json(serde_json::json!({ "message": "Reset code sent if email exists" })));
    let email = payload.email.to_lowercase();

    let mut redis = state.redis.clone();
    if !cache::rate_limit::check(&mut redis, "employee_forgot_password", &email, 3, 900).await.unwrap_or(true) {
        return Ok(silent_ok);
    }

    let employee = match EmployeeRepository::get_by_email(&state.pool, &email).await {
        Ok(Some(e)) => e,
        _ => return Ok(silent_ok),
    };

    let code = format!("{:06}", rand::random::<u32>() % 1_000_000);
    tracing::info!(email = %employee.email, "OTP generated for employee password reset");

    cache::token::store_employee_reset(&mut redis, &code, &employee.id.to_string())
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;

    let name = format!("{} {}", employee.first_name, employee.last_name);
    let _ = state.mail.send_password_reset_email(&employee.email, &name, &code).await;

    Ok(silent_ok)
}

/// POST /api/admin/auth/reset-password
async fn reset_password(
    State(state): State<AppState>,
    Json(payload): Json<ResetPasswordPayload>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let mut redis = state.redis.clone();

    let employee_id_str = cache::token::consume_employee_reset(&mut redis, &payload.code)
        .await
        .map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "Cache error", "CACHE_ERROR"))?
        .ok_or_else(|| err(StatusCode::UNAUTHORIZED, "Invalid or expired reset code", "INVALID_CODE"))?;

    let employee_id = employee_id_str
        .parse::<uuid::Uuid>()
        .map_err(|_| err(StatusCode::UNAUTHORIZED, "Invalid reset code", "INVALID_CODE"))?;

    if payload.new_password.len() < 8 {
        return Err(err(StatusCode::UNPROCESSABLE_ENTITY, "Password must be at least 8 characters", "VALIDATION_ERROR"));
    }

    let password_hash = hash_password(&payload.new_password)
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "INTERNAL_ERROR"))?;

    EmployeeRepository::change_password(&state.pool, employee_id, &password_hash)
        .await
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;

    Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Password reset successfully" }))))
}
