#[cfg(test)]
use argon2::{Algorithm, Params, Version};
use argon2::{
    Argon2,
    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use axum::{
    extract::{OriginalUri, Request, State},
    middleware::Next,
    response::Response,
};
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crank_core::{User, UserSessionId, WorkspaceId};
use crank_registry::WorkspaceMembershipRecord;
use rand::RngCore;
use serde::Serialize;
use sha2::{Digest, Sha256};
use time::{Duration, OffsetDateTime};

use crate::{error::ApiError, state::AppState};

pub const SESSION_COOKIE_NAME: &str = "crank_session";

#[derive(Clone)]
pub struct BootstrapAdminConfig {
    pub email: String,
    pub password: String,
    pub display_name: String,
}

#[derive(Clone)]
pub struct AuthSettings {
    pub session_secret: String,
    pub password_pepper: String,
    pub session_ttl_hours: i64,
    pub cookie_secure: bool,
    pub bootstrap_admin: BootstrapAdminConfig,
}

#[derive(Clone, Debug, Serialize)]
pub struct AuthenticatedSession {
    pub session_id: UserSessionId,
    pub user: User,
    pub memberships: Vec<WorkspaceMembershipRecord>,
    pub current_workspace_id: Option<WorkspaceId>,
}

#[derive(Clone)]
pub struct SessionCookie {
    pub session_id: UserSessionId,
    pub value: String,
    pub expires_at: OffsetDateTime,
}

pub fn hash_password(password: &str, pepper: &str) -> Result<String, ApiError> {
    let salt = SaltString::generate(&mut OsRng);
    let password = format!("{password}{pepper}");
    password_hasher()?
        .hash_password(password.as_bytes(), &salt)
        .map(|hash| hash.to_string())
        .map_err(|error| ApiError::internal(format!("failed to hash password: {error}")))
}

pub fn verify_password(password: &str, pepper: &str, password_hash: &str) -> bool {
    let Ok(parsed) = PasswordHash::new(password_hash) else {
        return false;
    };
    let password = format!("{password}{pepper}");
    match password_hasher() {
        Ok(argon2) => argon2.verify_password(password.as_bytes(), &parsed).is_ok(),
        Err(_) => false,
    }
}

fn password_hasher() -> Result<Argon2<'static>, ApiError> {
    #[cfg(test)]
    {
        let params = Params::new(8 * 1024, 1, 1, None).map_err(|error| {
            ApiError::internal(format!("failed to initialize test argon2 params: {error}"))
        })?;
        Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
    }

    #[cfg(not(test))]
    {
        Ok(Argon2::default())
    }
}

pub fn create_session_cookie(settings: &AuthSettings) -> Result<SessionCookie, ApiError> {
    let session_id = UserSessionId::new(format!("sess_{}", uuid::Uuid::now_v7().simple()));
    let mut secret_bytes = [0_u8; 32];
    rand::thread_rng().fill_bytes(&mut secret_bytes);
    let secret = URL_SAFE_NO_PAD.encode(secret_bytes);
    let expires_at = OffsetDateTime::now_utc()
        .checked_add(Duration::hours(settings.session_ttl_hours))
        .ok_or_else(|| ApiError::internal("failed to compute session expiration"))?;

    Ok(SessionCookie {
        session_id,
        value: format!("{secret}.{}", expires_at.unix_timestamp_nanos()),
        expires_at,
    })
}

pub fn hash_session_secret(
    session_id: &UserSessionId,
    session_value: &str,
    session_secret: &str,
) -> String {
    let mut digest = Sha256::new();
    digest.update(session_id.as_str().as_bytes());
    digest.update(b":");
    digest.update(session_value.as_bytes());
    digest.update(b":");
    digest.update(session_secret.as_bytes());
    URL_SAFE_NO_PAD.encode(digest.finalize())
}

pub fn session_cookie(settings: &AuthSettings, token: &str) -> Cookie<'static> {
    Cookie::build((SESSION_COOKIE_NAME, token.to_owned()))
        .http_only(true)
        .same_site(SameSite::Lax)
        .secure(settings.cookie_secure)
        .path("/")
        .max_age(Duration::hours(settings.session_ttl_hours))
        .build()
}

pub fn cleared_session_cookie(settings: &AuthSettings) -> Cookie<'static> {
    Cookie::build((SESSION_COOKIE_NAME, String::new()))
        .http_only(true)
        .same_site(SameSite::Lax)
        .secure(settings.cookie_secure)
        .path("/")
        .max_age(Duration::seconds(0))
        .build()
}

pub fn extract_session_token(jar: &CookieJar) -> Option<(UserSessionId, String)> {
    let cookie = jar.get(SESSION_COOKIE_NAME)?;
    let value = cookie.value();
    let (session_id, session_value) = value.split_once('.')?;

    Some((UserSessionId::new(session_id), session_value.to_owned()))
}

pub async fn require_session(
    State(state): State<AppState>,
    jar: CookieJar,
    mut request: Request,
    next: Next,
) -> Result<Response, ApiError> {
    let session = resolve_authenticated_session(state.clone(), &jar).await?;
    request.extensions_mut().insert(session);
    Ok(next.run(request).await)
}

pub async fn require_workspace_session(
    State(state): State<AppState>,
    jar: CookieJar,
    original_uri: OriginalUri,
    mut request: Request,
    next: Next,
) -> Result<Response, ApiError> {
    let session = resolve_authenticated_session(state.clone(), &jar).await?;
    let workspace_id = workspace_id_from_path(original_uri.path())
        .ok_or_else(|| ApiError::internal("workspace middleware was applied to an invalid path"))?;

    let has_access = state
        .service
        .user_has_workspace_access(&session.user.id, &workspace_id)
        .await?;
    if !has_access {
        return Err(ApiError::forbidden("workspace access denied"));
    }

    request.extensions_mut().insert(session);
    Ok(next.run(request).await)
}

async fn resolve_authenticated_session(
    state: AppState,
    jar: &CookieJar,
) -> Result<AuthenticatedSession, ApiError> {
    let (session_id, session_value) = extract_session_token(jar)
        .ok_or_else(|| ApiError::unauthorized("authentication required"))?;
    let session = state
        .service
        .get_session(&session_id, &session_value)
        .await?
        .ok_or_else(|| ApiError::unauthorized("session is invalid or expired"))?;

    state.service.touch_session(&session_id).await?;

    Ok(session)
}

fn workspace_id_from_path(path: &str) -> Option<WorkspaceId> {
    let marker = "/api/admin/workspaces/";
    let (_, suffix) = path.split_once(marker)?;
    let workspace_id = suffix.split('/').next()?;
    if workspace_id.is_empty() {
        return None;
    }

    Some(WorkspaceId::new(workspace_id))
}
