/**
 * Authenticate on BJColle.
 */
use crate::{configuration::{get_account_by_class, Settings}};
use redis::{Commands, Connection, RedisError, RedisResult};
use reqwest::{self, header::HeaderValue, redirect::Policy};

pub async fn request_session() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://bjcolle.fr/acces.php";

    let response = reqwest::get(url).await?;
    // Get response headers
    let session_id = response.headers().get("set-cookie");
    if !session_id.is_some() {
        return Err("Failed to get session ID".into());
    }
    Ok(session_id.unwrap().to_str()?.to_string())
}

pub async fn login(
    username: &str,
    password: &str,
    session_id: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://bjcolle.fr/acces.php";

    let client = reqwest::Client::builder()
        .redirect(Policy::none())
        .build()?;
    let response = client
        .post(url)
        .header("Cookie", session_id)
        .form(&[
            ("USERNAME_ACCES", username),
            ("PASSWORD_ACCES", password),
            ("SOUVENIR", "on"),
            ("valider_ident", "Valider"),
        ])
        .send()
        .await?;

    // Get session ID from cookies
    let session_id: Vec<String> = response
        .headers()
        .get_all("set-cookie")
        .iter()
        .filter_map(|value: &HeaderValue| value.to_str().ok())
        .filter_map(|s| {
            // Get the part before the first semicolon
            let first_part = s.split(';').next()?.trim();

            // Only keep "bjid" or "bjp"
            if first_part.starts_with("bjid=") || first_part.starts_with("bjp=") {
                Some(first_part.to_string())
            } else {
                None
            }
        })
        .collect();

    if session_id.is_empty() {
        return Err("Invalid credentials".into());
    }

    Ok(session_id.join("; "))
}

pub async fn refresh_session(cookie: &str) -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://bjcolle.fr/acces_cookies.php";

    let client = reqwest::Client::builder()
        .redirect(Policy::none())
        .build()?;
    let response = client.get(url).header("Cookie", cookie).send().await?;

    // Get session ID from cookies
    let new_session_id = response.headers().get("set-cookie");
    if !new_session_id.is_some() {
        return Err("Failed to refresh session ID".into());
    }

    Ok(new_session_id.unwrap().to_str()?.to_string())
}

pub async fn authenticate(
    class_name: &str,
    con: &mut Connection,
    config: &Settings,
) -> RedisResult<String> {
    // Try reusing an existing session if available
    let session_key = format!("session_{}", class_name);
    let session_duration = 900; // 15 minutes
    let session_id: RedisResult<Option<String>> = con.get(&session_key);
    match session_id? {
        Some(id) => {
            println!("Reusing existing session: {}", id);
            return Ok(id);
        }
        None => {
            println!("No existing session found for class: {}", class_name);
        }
    }

    // Request a session ID (valid for 15 minutes)
    let session = request_session().await;
    if let Err(err) = session {
        eprintln!("Failed to request session: {}", err);
        return Err(redis::RedisError::from((
            redis::ErrorKind::AuthenticationFailed,
            "Session request failed",
        )));
    }
    let session_id = session.unwrap();
    println!("Session requested: {}", session_id);

    // Try to get cached session ID
    let cookie_key = format!("auth_cookie_{}", class_name);
    let cached_session: RedisResult<Option<String>> = con.get(&cookie_key);

    match cached_session? {
        Some(cookie) => {
            // Attempt to refresh session
            let refreshed = refresh_session(&cookie).await;
            if let Err(err) = refreshed {
                eprintln!("Failed to refresh session: {}", err);
                // Remove the cached cookie if refresh fails and retry authentication
                let _: Result<(), RedisError> = con.del(&cookie_key);
                return Box::pin(authenticate(class_name, con, config)).await;
            }

            let new_session_id = refreshed.unwrap();
            println!("Session refreshed: {}", new_session_id);

            // Set the new session id (for 15 minutes)
            let _: Result<(), RedisError> = con.set_ex(&session_key, &new_session_id, session_duration);
            Ok(new_session_id)
        }

        None => {
            // No cookie found, authenticate with credentials
            let account = get_account_by_class(&config, class_name);
            if account.is_none() {
                eprintln!("No account found for class: {}", class_name);
                return Err(redis::RedisError::from((
                    redis::ErrorKind::AuthenticationFailed,
                    "No account found",
                )));
            }
            let account = account.unwrap();
            println!("Using account: {}", account.username);

            let login_result = login(&account.username, &account.password, &session_id).await;
            if let Err(err) = login_result {
                eprintln!("Failed to login: {}", err);
                return Err(redis::RedisError::from((
                    redis::ErrorKind::AuthenticationFailed,
                    "Login failed",
                )));
            }
            let cookie = login_result.unwrap();
            println!("Login successful: {}", cookie);

            let _: Result<(), RedisError> = con.set(&cookie_key, cookie.clone());
            // Set the new session id (for 15 minutes)
            let _: Result<(), RedisError> = con.set_ex(&session_key, &session_id, session_duration);
            Ok(session_id)
        }
    }
}
