use crate::{
  AppState,
  auth::{
    SessionAuthorization,
    bearer_token::BearerTokenClaims,
    middleware::{compute_fingerprint, extract_ip},
    session::Session,
  },
  error::{Error, Result},
};
use argon2::{
  Argon2,
  PasswordHash,
  PasswordHasher,
  PasswordVerifier,
  password_hash::{SaltString, rand_core::OsRng},
};
use axum::{
  Json,
  Router,
  extract::Path,
  http::{HeaderMap, StatusCode},
  response::IntoResponse,
  routing::{delete, get, post, put},
};
use axum_extra::extract::CookieJar;
use cookie::{Cookie, SameSite};
use serde::{Deserialize, Serialize};
use siren_core::data;
use std::{env, sync::Arc};
use uuid::Uuid;

pub fn get_routes() -> Router<Arc<AppState>> {
  Router::new()
    .route("/register", post(register))
    .route("/login", post(login))
    .route("/logout", post(logout))
    .route("/me", get(me))
    .route("/profile", put(update_profile))
    .route("/change-password", post(change_password))
    .route("/connections/{provider}", delete(disconnect_provider))
}

#[derive(Deserialize)]
struct RegisterPayload {
  username: String,
  password: String,
}

#[derive(Deserialize)]
struct LoginPayload {
  username: String,
  password: String,
}

#[derive(Deserialize)]
struct UpdateProfilePayload {
  first_name: Option<String>,
  last_name: Option<String>,
}

#[derive(Deserialize)]
struct ChangePasswordPayload {
  /// Required when the user already has a password set. Omit (null) when
  /// setting a password for the first time (OAuth-only account).
  current_password: Option<String>,
  new_password: String,
}

#[derive(Serialize)]
pub struct ConnectionInfo {
  pub provider: String,
  pub provider_username: Option<String>,
  pub provider_avatar: Option<String>,
}

#[derive(Serialize)]
pub struct UserInfo {
  pub id: String,
  pub username: String,
  pub first_name: Option<String>,
  pub last_name: Option<String>,
  pub email: Option<String>,
  /// True when the account has a local password set (i.e. can log in without
  /// OAuth and can safely disconnect OAuth providers).
  pub has_password: bool,
  pub connections: Vec<ConnectionInfo>,
  /// Site-level role: `"admin"` or `"user"`.
  pub role: String,
  /// Account status: `"active"` or `"banned"`.
  pub status: String,
}

#[derive(sqlx::FromRow)]
struct DbUser {
  id: Uuid,
  username: String,
  first_name: Option<String>,
  last_name: Option<String>,
  email: Option<String>,
  password_hash: Option<String>,
  role: String,
  status: String,
}

#[derive(sqlx::FromRow)]
struct DbConnection {
  provider: String,
  provider_username: Option<String>,
  provider_avatar: Option<String>,
}

/// Hash and salt a plaintext password with Argon2.
pub fn hash_password(password: &str) -> Result<String> {
  let salt = SaltString::generate(&mut OsRng);
  Argon2::default()
    .hash_password(password.as_bytes(), &salt)
    .map(|h| h.to_string())
    .map_err(|e| Error::internal_server_error(format!("Password hashing error: {e}")))
}

/// Return `true` if `password` matches the stored Argon2id `hash`.
pub fn verify_password(password: &str, hash: &str) -> bool {
  let Ok(parsed) = PasswordHash::new(hash) else {
    return false;
  };
  Argon2::default()
    .verify_password(password.as_bytes(), &parsed)
    .is_ok()
}

/// Build the `siren_session` HttpOnly Secure cookie.
pub fn build_session_cookie(token: String) -> Cookie<'static> {
  Cookie::build(("siren_session", token))
    .http_only(true)
    .secure(true)
    .same_site(SameSite::Lax)
    .path("/")
    .build()
}

/// Issue a signed JWT for `session`.
pub fn issue_jwt(session: &Session) -> Result<String> {
  let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
  let encoding_key = jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes());
  let claims = BearerTokenClaims {
    sub: session.user_id.to_string(),
    name: session.user_name.clone(),
    iat: chrono::Utc::now().timestamp(),
    exp: session.expires_at.timestamp(),
    jti: session.session_id.clone(),
  };
  jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &encoding_key)
    .map_err(|e| Error::internal_server_error(format!("JWT error: {e}")))
}

/// Create a session + JWT + Set-Cookie for `user_id` / `user_name`.
#[allow(dead_code)]
pub async fn create_session_and_cookie(
  user_id: Uuid,
  user_name: String,
  headers: &HeaderMap,
) -> Result<(CookieJar, ())> {
  let ip = extract_ip(headers);
  let user_agent = headers
    .get("user-agent")
    .and_then(|h| h.to_str().ok())
    .unwrap_or("unknown")
    .to_string();
  let fingerprint = compute_fingerprint(&ip, &user_agent);

  let session = Session::new(user_id, user_name, fingerprint);
  session.insert().await?;

  let token = issue_jwt(&session)?;
  let cookie = build_session_cookie(token);
  let jar = CookieJar::new().add(cookie);
  Ok((jar, ()))
}

async fn load_user_info(user_id: Uuid) -> Result<UserInfo> {
  let pool = data::pool();

  let user: DbUser = sqlx::query_as(
    "SELECT id, username, first_name, last_name, email, password_hash, role, status \
     FROM users WHERE id = $1",
  )
  .bind(user_id)
  .fetch_one(pool)
  .await?;

  let connections: Vec<DbConnection> = sqlx::query_as(
    "SELECT provider, provider_username, provider_avatar \
     FROM user_connections WHERE user_id = $1",
  )
  .bind(user_id)
  .fetch_all(pool)
  .await?;

  Ok(UserInfo {
    id: user.id.to_string(),
    username: user.username,
    first_name: user.first_name,
    last_name: user.last_name,
    email: user.email,
    has_password: user.password_hash.is_some(),
    role: user.role,
    status: user.status,
    connections: connections
      .into_iter()
      .map(|c| ConnectionInfo {
        provider: c.provider,
        provider_username: c.provider_username,
        provider_avatar: c.provider_avatar,
      })
      .collect(),
  })
}

async fn register(
  headers: HeaderMap,
  jar: CookieJar,
  Json(payload): Json<RegisterPayload>,
) -> Result<impl IntoResponse> {
  let username = payload.username.trim().to_string();
  if username.is_empty() || username.len() > 32 {
    return Err(Error::new(422, "Username must be 1–32 characters".into()));
  }

  let pool = data::pool();

  let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)")
    .bind(&username)
    .fetch_one(pool)
    .await?;
  if exists {
    return Err(Error::new(409, "Username already taken".into()));
  }

  let password_hash = hash_password(&payload.password)?;
  let user_id: Uuid =
    sqlx::query_scalar("INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id")
      .bind(&username)
      .bind(&password_hash)
      .fetch_one(pool)
      .await?;

  let ip = extract_ip(&headers);
  let user_agent = headers
    .get("user-agent")
    .and_then(|h| h.to_str().ok())
    .unwrap_or("unknown")
    .to_string();
  let fingerprint = compute_fingerprint(&ip, &user_agent);

  let session = Session::new(user_id, username, fingerprint);
  session.insert().await?;

  let token = issue_jwt(&session)?;
  let cookie = build_session_cookie(token);

  Ok((jar.add(cookie), StatusCode::CREATED))
}

async fn login(
  headers: HeaderMap,
  jar: CookieJar,
  Json(payload): Json<LoginPayload>,
) -> Result<impl IntoResponse> {
  let pool = data::pool();

  let row: Option<(Uuid, String, Option<String>, String)> =
    sqlx::query_as("SELECT id, username, password_hash, status FROM users WHERE username = $1")
      .bind(&payload.username)
      .fetch_optional(pool)
      .await?;

  let (user_id, username, password_hash, status) =
    row.ok_or_else(|| Error::new(401, "Invalid username or password".into()))?;

  let hash =
    password_hash.ok_or_else(|| Error::new(401, "This account uses external login only".into()))?;

  if !verify_password(&payload.password, &hash) {
    return Err(Error::new(401, "Invalid username or password".into()));
  }

  if status == "banned" {
    return Err(Error::new(403, "This account has been banned".into()));
  }

  let ip = extract_ip(&headers);
  let user_agent = headers
    .get("user-agent")
    .and_then(|h| h.to_str().ok())
    .unwrap_or("unknown")
    .to_string();
  let fingerprint = compute_fingerprint(&ip, &user_agent);

  let session = Session::new(user_id, username, fingerprint);
  session.insert().await?;

  let token = issue_jwt(&session)?;
  let cookie = build_session_cookie(token);

  Ok((jar.add(cookie), StatusCode::OK))
}

async fn logout(
  jar: CookieJar,
  SessionAuthorization(session): SessionAuthorization,
) -> impl IntoResponse {
  if let Some(s) = session {
    let _ = Session::delete(&s.session_id).await;
  }
  let removal = Cookie::build(("siren_session", ""))
    .http_only(true)
    .secure(true)
    .same_site(SameSite::Lax)
    .path("/")
    .max_age(cookie::time::Duration::seconds(0))
    .build();
  (jar.add(removal), StatusCode::NO_CONTENT)
}

async fn me(SessionAuthorization(session): SessionAuthorization) -> Result<Json<UserInfo>> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
  Ok(Json(load_user_info(session.user_id).await?))
}

async fn update_profile(
  SessionAuthorization(session): SessionAuthorization,
  Json(payload): Json<UpdateProfilePayload>,
) -> Result<Json<UserInfo>> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
  let pool = data::pool();

  // Validate lengths if provided
  if let Some(ref f) = payload.first_name {
    if f.len() > 64 {
      return Err(Error::new(422, "First name must be ≤ 64 characters".into()));
    }
  }
  if let Some(ref l) = payload.last_name {
    if l.len() > 64 {
      return Err(Error::new(422, "Last name must be ≤ 64 characters".into()));
    }
  }

  // COALESCE: only update fields that were sent (Some vs None)
  // We allow explicitly setting a field to an empty string to clear it,
  // so we map Some("") → SQL NULL.
  let first = payload
    .first_name
    .map(|s| if s.trim().is_empty() { None } else { Some(s) });
  let last = payload
    .last_name
    .map(|s| if s.trim().is_empty() { None } else { Some(s) });

  sqlx::query(
    "UPDATE users
     SET first_name  = CASE WHEN $2 THEN $3 ELSE first_name END,
         last_name   = CASE WHEN $4 THEN $5 ELSE last_name  END,
         updated_at  = NOW()
     WHERE id = $1",
  )
  .bind(session.user_id)
  .bind(first.is_some())
  .bind(first.flatten())
  .bind(last.is_some())
  .bind(last.flatten())
  .execute(pool)
  .await?;

  Ok(Json(load_user_info(session.user_id).await?))
}

async fn change_password(
  SessionAuthorization(session): SessionAuthorization,
  Json(payload): Json<ChangePasswordPayload>,
) -> Result<StatusCode> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;

  let pool = data::pool();

  let existing_hash: Option<String> =
    sqlx::query_scalar("SELECT password_hash FROM users WHERE id = $1")
      .bind(session.user_id)
      .fetch_optional(pool)
      .await?
      .flatten();

  match existing_hash {
    Some(hash) => {
      // User already has a password — require current password
      let current = payload
        .current_password
        .ok_or_else(|| Error::new(422, "Current password is required".into()))?;
      if !verify_password(&current, &hash) {
        return Err(Error::new(401, "Current password is incorrect".into()));
      }
    }
    None => {
      // OAuth-only account — allow setting a password without current_password
    }
  }

  let new_hash = hash_password(&payload.new_password)?;
  sqlx::query("UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2")
    .bind(&new_hash)
    .bind(session.user_id)
    .execute(pool)
    .await?;

  Ok(StatusCode::NO_CONTENT)
}

async fn disconnect_provider(
  SessionAuthorization(session): SessionAuthorization,
  Path(provider): Path<String>,
) -> Result<StatusCode> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
  let pool = data::pool();

  // Safety check: ensure the user has a password before disconnecting OAuth.
  let has_password: bool =
    sqlx::query_scalar("SELECT password_hash IS NOT NULL FROM users WHERE id = $1")
      .bind(session.user_id)
      .fetch_one(pool)
      .await?;

  if !has_password {
    return Err(Error::new(
      422,
      "Set a password before disconnecting your OAuth provider".into(),
    ));
  }

  sqlx::query("DELETE FROM user_connections WHERE user_id = $1 AND provider = $2")
    .bind(session.user_id)
    .bind(&provider)
    .execute(pool)
    .await?;

  Ok(StatusCode::NO_CONTENT)
}
