use std::sync::Arc;
use std::time::{Duration, Instant};

use askama::Template;
use axum::Form;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Redirect, Response};
use axum_extra::extract::cookie::CookieJar;
use serde::Deserialize;
use tracing::{info, warn};

use crate::config::Config;
use crate::csrf::{CsrfForm, CsrfOnlyForm};
use crate::error::AppError;
use crate::web_session::{
    SESSION_COOKIE, SessionStore, WebSession, build_session_cookie, generate_token,
};

#[derive(Template)]
#[template(path = "login.html")]
struct LoginTemplate {
    error: Option<&'static str>,
}

#[derive(Deserialize)]
pub struct LoginForm {
    slug: String,
    password: String,
}

pub async fn handle_login_page() -> Result<Html<String>, AppError> {
    render_login(None)
}

pub async fn handle_login_submit(
    State(config): State<Arc<Config>>,
    State(store): State<SessionStore>,
    jar: CookieJar,
    Form(form): Form<LoginForm>,
) -> Result<Response, AppError> {
    // Shape-check the slug before touching the user list. We fold a
    // bad-shape slug into the same "Login fehlgeschlagen" path as a
    // wrong password to avoid leaking whether the slug *could* be valid.
    // The bcrypt-on-unknown-user dummy verify below already keeps
    // timing comparable; this just denies path-traversal-shaped slugs
    // before they reach disk-touching code anywhere.
    let slug_ok = crate::validate::validate_slug(&form.slug).is_ok();

    let user = if slug_ok {
        config.users.iter().find(|u| u.slug == form.slug)
    } else {
        None
    };
    let valid = user
        .map(|u| bcrypt::verify(&form.password, &u.web_password).unwrap_or(false))
        .unwrap_or(false);

    // Always evaluate bcrypt even on unknown slug to reduce timing signal.
    if !valid {
        if user.is_none() {
            // Dummy verify so timing is similar regardless of slug existence.
            let _ = bcrypt::verify(
                &form.password,
                "$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalidii",
            );
        }
        warn!(slug = %form.slug, "login failed");
        return render_login(Some("Login fehlgeschlagen")).map(IntoResponse::into_response);
    }

    let user = user.unwrap();
    let token = generate_token();
    let csrf_token = generate_token();

    let ttl = Duration::from_secs(u64::from(config.session_timeout_hours) * 3600);
    {
        let mut w = store.write().await;
        w.insert(
            token.clone(),
            WebSession {
                slug: user.slug.clone(),
                role: user.role.clone(),
                expires_at: Instant::now() + ttl,
                csrf_token,
            },
        );
    }

    info!(slug = %user.slug, "login ok");

    let cookie = build_session_cookie(token, ttl, config.cookie_secure);
    let jar = jar.add(cookie);
    Ok((jar, Redirect::to("/web/cases")).into_response())
}

pub async fn handle_logout(
    State(config): State<Arc<Config>>,
    State(store): State<SessionStore>,
    jar: CookieJar,
    _csrf: CsrfForm<CsrfOnlyForm>,
) -> Result<Response, AppError> {
    if let Some(c) = jar.get(SESSION_COOKIE) {
        let token = c.value().to_owned();
        let mut w = store.write().await;
        let removed = w.remove(&token);
        if let Some(s) = removed {
            info!(slug = %s.slug, "logout");
        }
    }

    // Expire the cookie by setting an empty value with max_age=0.
    let mut cookie =
        build_session_cookie(String::new(), Duration::from_secs(0), config.cookie_secure);
    cookie.make_removal();
    let jar = jar.add(cookie);
    Ok((jar, Redirect::to("/web/login")).into_response())
}

fn render_login(error: Option<&'static str>) -> Result<Html<String>, AppError> {
    LoginTemplate { error }
        .render()
        .map(Html)
        .map_err(|e| AppError::Internal(format!("Template render failed: {e}")))
}
