Skip to content
use actix_web::{HttpResponse, Responder, post, web};
use log::info;
use serde::Deserialize;

use crate::{
    auth::{
        HodisContext, create_exercise, create_goal, create_user, generate_short_code,
        verify_password,
    },
    config::Server,
    view::auth::{render_login_success, render_signup_success},
    view::{render_error, render_success},
};

#[derive(Debug, Deserialize)]
pub struct SignupForm {
    pub username: String,
    pub email: String,
    pub password: String,
    pub invite_code: String,
}

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

#[derive(Debug, Deserialize)]
pub struct DeleteInviteForm {
    pub code: String,
}

#[derive(Debug, Deserialize)]
pub struct CreateExerciseForm {
    pub name: String,
    pub description: String,
    pub unit: String,
    pub points_per_unit: String,
}

#[derive(Debug, Deserialize)]
pub struct DeleteExerciseForm {
    pub id: String,
}

#[derive(Debug, Deserialize)]
pub struct CreateGoalForm {
    pub name: String,
    pub target_points: String,
}

#[derive(Debug, Deserialize)]
pub struct DeleteGoalForm {
    pub id: String,
}

#[derive(Debug, Deserialize)]
pub struct EditGoalForm {
    pub id: String,
    pub target_points: String,
}

#[derive(Debug, Deserialize)]
pub struct ToggleAdminForm {
    pub user_id: String,
    pub is_admin: String,
}

#[derive(Debug, Deserialize)]
pub struct LogExerciseForm {
    pub exercise_id: String,
    pub amount: String,
}

#[derive(Debug, Deserialize)]
pub struct ToggleFavoriteForm {
    pub exercise_id: String,
}

#[post("/auth/signup")]
pub async fn signup_handler(
    auth_state: web::Data<HodisContext>,
    form: web::Form<SignupForm>,
) -> impl Responder {
    let db = auth_state.db();

    if form.invite_code.is_empty() {
        return HttpResponse::BadRequest()
            .body(render_error("You need an invite link to create an account").into_string());
    }

    match db.validate_invite_link(&form.invite_code).await {
        Ok(true) => {}
        Ok(false) => {
            return HttpResponse::BadRequest()
                .body(render_error("Invalid invite link").into_string());
        }
        Err(e) => {
            log::error!("Database error checking invite: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    }

    if form.username.len() < 3 {
        return HttpResponse::BadRequest()
            .body(render_error("Username must be at least 3 characters").into_string());
    }
    if form.email.is_empty() || !form.email.contains('@') {
        return HttpResponse::BadRequest()
            .body(render_error("Please enter a valid email address").into_string());
    }
    if form.password.len() < 8 {
        return HttpResponse::BadRequest()
            .body(render_error("Password must be at least 8 characters").into_string());
    }

    match db.get_user_by_username(&form.username).await {
        Ok(Some(_)) => {
            return HttpResponse::Conflict()
                .body(render_error("Username already exists").into_string());
        }
        Ok(None) => {}
        Err(e) => {
            log::error!("Database error: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    }

    let user = match create_user(
        form.username.clone(),
        form.email.clone(),
        form.password.clone(),
    ) {
        Ok(user) => user,
        Err(e) => {
            log::error!("Failed to create user: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Failed to create user").into_string());
        }
    };

    if let Err(e) = db.create_user(&user).await {
        log::error!("Failed to save user: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to save user").into_string());
    }

    info!("Created user: {}", user.username);

    HttpResponse::Ok()
        .content_type("text/html")
        .body(render_signup_success(&user.username).into_string())
}

#[post("/auth/login")]
pub async fn login_handler(
    auth_state: web::Data<HodisContext>,
    form: web::Form<LoginForm>,
) -> impl Responder {
    let db = auth_state.db();

    let user = match db.get_user_by_username(&form.username).await {
        Ok(Some(user)) => user,
        Ok(None) => {
            return HttpResponse::Unauthorized()
                .body(render_error("Invalid credentials").into_string());
        }
        Err(e) => {
            log::error!("Database error: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    match verify_password(&form.password, &user.password_hash) {
        Ok(true) => {}
        Ok(false) => {
            return HttpResponse::Unauthorized()
                .body(render_error("Invalid credentials").into_string());
        }
        Err(e) => {
            log::error!("Password verification error: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Authentication error").into_string());
        }
    }

    let token = match auth_state.create_session(user.id.clone()).await {
        Ok(token) => token,
        Err(e) => {
            log::error!("Failed to create session: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Failed to create session").into_string());
        }
    };

    info!("User logged in: {}", form.username);

    HttpResponse::Ok()
        .insert_header(("HX-Redirect", "/"))
        .cookie(
            actix_web::cookie::Cookie::build("session", token)
                .path("/")
                .http_only(true)
                .same_site(actix_web::cookie::SameSite::Strict)
                .max_age(actix_web::cookie::time::Duration::days(365))
                .finish(),
        )
        .body(render_login_success(&user.username).into_string())
}

#[post("/auth/logout")]
pub async fn logout_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
) -> impl Responder {
    let token = match req.cookie("session") {
        Some(cookie) => cookie.value().to_string(),
        None => {
            return HttpResponse::Unauthorized().body("Not logged in");
        }
    };

    if let Err(e) = auth_state.invalidate_token(&token).await {
        log::error!("Failed to invalidate token: {e}");
        return HttpResponse::InternalServerError().body("Failed to logout");
    }

    HttpResponse::Found()
        .insert_header(("Location", "/"))
        .cookie(
            actix_web::cookie::Cookie::build("session", "")
                .path("/")
                .http_only(true)
                .same_site(actix_web::cookie::SameSite::Strict)
                .max_age(actix_web::cookie::time::Duration::ZERO)
                .finish(),
        )
        .finish()
}

#[post("/admin/invites/create")]
pub async fn admin_create_invite_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    let code = generate_short_code();
    if let Err(e) = auth_state.db().create_invite_link(&code).await {
        log::error!("Failed to create invite link: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to create invite link").into_string());
    }

    info!("Created invite link: {code}");

    let invite_links = match auth_state.db().get_all_invite_links().await {
        Ok(links) => links,
        Err(e) => {
            log::error!("Failed to fetch invite links: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    HttpResponse::Ok()
        .content_type("text/html")
        .body(crate::view::admin::render_invites_list(&invite_links).into_string())
}

#[post("/admin/invites/delete")]
pub async fn admin_delete_invite_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<DeleteInviteForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    if let Err(e) = auth_state.db().delete_invite_link(&form.code).await {
        log::error!("Failed to delete invite link: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to delete invite link").into_string());
    }

    let invite_links = match auth_state.db().get_all_invite_links().await {
        Ok(links) => links,
        Err(e) => {
            log::error!("Failed to fetch invite links: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    HttpResponse::Ok()
        .content_type("text/html")
        .body(crate::view::admin::render_invites_list(&invite_links).into_string())
}

#[post("/admin/exercises/create")]
pub async fn admin_create_exercise_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<CreateExerciseForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    let points: f64 = match form.points_per_unit.parse() {
        Ok(p) if p > 0.0 => p,
        _ => {
            return HttpResponse::BadRequest()
                .body(render_error("Points per unit must be a positive number").into_string());
        }
    };

    let unit = if form.unit.is_empty() {
        "reps".to_string()
    } else {
        form.unit.clone()
    };

    let exercise = create_exercise(form.name.clone(), form.description.clone(), unit, points);

    if let Err(e) = auth_state.db().create_exercise(&exercise).await {
        log::error!("Failed to create exercise: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to create exercise").into_string());
    }

    info!("Created exercise: {}", exercise.name);

    let exercises = match auth_state.db().get_all_exercises().await {
        Ok(exs) => exs,
        Err(e) => {
            log::error!("Failed to fetch exercises: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    HttpResponse::Ok()
        .content_type("text/html")
        .body(crate::view::admin::render_exercises_list(&exercises).into_string())
}

#[post("/admin/exercises/delete")]
pub async fn admin_delete_exercise_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<DeleteExerciseForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    if let Err(e) = auth_state.db().delete_exercise(&form.id).await {
        log::error!("Failed to delete exercise: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to delete exercise").into_string());
    }

    let exercises = match auth_state.db().get_all_exercises().await {
        Ok(exs) => exs,
        Err(e) => {
            log::error!("Failed to fetch exercises: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    HttpResponse::Ok()
        .content_type("text/html")
        .body(crate::view::admin::render_exercises_list(&exercises).into_string())
}

#[post("/admin/goals/create")]
pub async fn admin_create_goal_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<CreateGoalForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    let target_points: f64 = match form.target_points.parse() {
        Ok(p) if p > 0.0 => p,
        _ => {
            return HttpResponse::BadRequest()
                .body(render_error("Target points must be a positive number").into_string());
        }
    };

    let goal = create_goal(form.name.clone(), target_points);

    if let Err(e) = auth_state.db().create_goal(&goal).await {
        log::error!("Failed to create goal: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to create goal").into_string());
    }

    info!("Created goal: {}", goal.name);

    HttpResponse::Ok()
        .content_type("text/html")
        .insert_header(("HX-Redirect", "/admin"))
        .body(render_success("Goal created!").into_string())
}

#[post("/admin/goals/delete")]
pub async fn admin_delete_goal_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<DeleteGoalForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    if let Err(e) = auth_state.db().delete_goal(&form.id).await {
        log::error!("Failed to delete goal: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to delete goal").into_string());
    }

    info!("Deleted goal: {}", form.id);

    HttpResponse::Ok()
        .content_type("text/html")
        .insert_header(("HX-Redirect", "/admin"))
        .body(render_success("Goal deleted!").into_string())
}

#[post("/admin/goals/edit")]
pub async fn admin_edit_goal_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<EditGoalForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    let target_points: f64 = match form.target_points.parse() {
        Ok(p) if p > 0.0 => p,
        _ => {
            return HttpResponse::BadRequest()
                .body(render_error("Target points must be a positive number").into_string());
        }
    };

    if let Err(e) = auth_state
        .db()
        .update_goal_target_points(&form.id, target_points)
        .await
    {
        log::error!("Failed to update goal: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to update goal").into_string());
    }

    info!("Updated goal {} target to {}", form.id, target_points);

    let invite_links = auth_state
        .db()
        .get_all_invite_links()
        .await
        .unwrap_or_default();
    let exercises = auth_state
        .db()
        .get_all_exercises()
        .await
        .unwrap_or_default();
    let goal = auth_state.db().get_active_goal().await.unwrap_or_default();
    let total_points = auth_state.db().get_total_points().await.unwrap_or(0.0);
    let all_logs = auth_state
        .db()
        .get_all_exercise_logs()
        .await
        .unwrap_or_default();
    let all_users = auth_state.db().get_all_users().await.unwrap_or_default();

    HttpResponse::Ok().content_type("text/html").body(
        crate::view::admin::render_admin_content(
            &invite_links,
            &exercises,
            &goal,
            total_points,
            &all_logs,
            &all_users,
        )
        .into_string(),
    )
}

#[post("/admin/users/toggle-admin")]
pub async fn admin_toggle_admin_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    config: web::Data<Server>,
    form: web::Form<ToggleAdminForm>,
) -> impl Responder {
    if !crate::auth::is_admin_request(&req, &auth_state, &config).await {
        return HttpResponse::Unauthorized()
            .insert_header(("WWW-Authenticate", "Basic realm=\"admin\""))
            .body("Unauthorized");
    }

    let is_admin = form.is_admin == "true";

    if let Err(e) = auth_state
        .db()
        .set_user_admin(&form.user_id, is_admin)
        .await
    {
        log::error!("Failed to toggle admin: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to update user").into_string());
    }

    info!("Set admin={} for user {}", is_admin, form.user_id);

    let invite_links = auth_state
        .db()
        .get_all_invite_links()
        .await
        .unwrap_or_default();
    let exercises = auth_state
        .db()
        .get_all_exercises()
        .await
        .unwrap_or_default();
    let goal = auth_state.db().get_active_goal().await.unwrap_or_default();
    let total_points = auth_state.db().get_total_points().await.unwrap_or(0.0);
    let all_logs = auth_state
        .db()
        .get_all_exercise_logs()
        .await
        .unwrap_or_default();
    let all_users = auth_state.db().get_all_users().await.unwrap_or_default();

    HttpResponse::Ok().content_type("text/html").body(
        crate::view::admin::render_admin_content(
            &invite_links,
            &exercises,
            &goal,
            total_points,
            &all_logs,
            &all_users,
        )
        .into_string(),
    )
}

#[post("/log-exercise")]
pub async fn log_exercise_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    form: web::Form<LogExerciseForm>,
) -> impl Responder {
    let token = match req.cookie("session") {
        Some(cookie) => cookie.value().to_string(),
        None => {
            return HttpResponse::Unauthorized().body(render_error("Not logged in").into_string());
        }
    };

    let user_id = match auth_state.validate_token(&token).await {
        Some(id) => id,
        None => {
            return HttpResponse::Unauthorized()
                .body(render_error("Session expired").into_string());
        }
    };

    let amount: f64 = match form.amount.parse() {
        Ok(a) if a > 0.0 => a,
        _ => {
            return HttpResponse::BadRequest()
                .body(render_error("Amount must be a positive number").into_string());
        }
    };

    let exercise = match auth_state.db().get_exercise_by_id(&form.exercise_id).await {
        Ok(Some(e)) => e,
        Ok(None) => {
            return HttpResponse::BadRequest()
                .body(render_error("Exercise not found").into_string());
        }
        Err(e) => {
            log::error!("Database error: {e}");
            return HttpResponse::InternalServerError()
                .body(render_error("Database error").into_string());
        }
    };

    let points_earned = amount * exercise.points_per_unit;
    let log = crate::auth::ExerciseLog {
        id: uuid::Uuid::new_v4().to_string(),
        user_id: user_id.clone(),
        exercise_id: exercise.id.clone(),
        amount,
        points_earned,
        created_at: chrono::Utc::now().to_rfc3339(),
    };

    if let Err(e) = auth_state.db().create_exercise_log(&log).await {
        log::error!("Failed to log exercise: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to log exercise").into_string());
    }

    let goal = auth_state.db().get_active_goal().await.unwrap_or_default();
    let total_points = auth_state.db().get_total_points().await.unwrap_or(0.0);
    let user_points = auth_state
        .db()
        .get_points_for_user(&user_id)
        .await
        .unwrap_or(0.0);

    let exercises = auth_state
        .db()
        .get_all_exercises()
        .await
        .unwrap_or_default();
    let recent_logs = auth_state
        .db()
        .get_exercise_logs_for_user(&user_id)
        .await
        .unwrap_or_default();
    let favorite_ids = auth_state
        .db()
        .get_favorite_exercise_ids(&user_id)
        .await
        .unwrap_or_default();

    let success_msg = format!(
        "Logged {:.1} {} of {} for {:.1} points!",
        amount, exercise.unit, exercise.name, points_earned
    );

    let dashboard = crate::view::dashboard::render_dashboard_content(
        &user_id,
        &exercises,
        &goal,
        total_points,
        user_points,
        &recent_logs,
        &favorite_ids,
        Some(&success_msg),
    );

    HttpResponse::Ok().content_type("text/html").body(
        maud::html! {
            div id="dashboard-content" hx-target="this" hx-swap="outerHTML" {
                (dashboard)
            }
        }
        .into_string(),
    )
}

#[post("/toggle-favorite")]
pub async fn toggle_favorite_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    form: web::Form<ToggleFavoriteForm>,
) -> impl Responder {
    let token = match req.cookie("session") {
        Some(cookie) => cookie.value().to_string(),
        None => {
            return HttpResponse::Unauthorized().body(render_error("Not logged in").into_string());
        }
    };

    let user_id = match auth_state.validate_token(&token).await {
        Some(id) => id,
        None => {
            return HttpResponse::Unauthorized()
                .body(render_error("Session expired").into_string());
        }
    };

    let is_fav = auth_state
        .db()
        .is_favorite_exercise(&user_id, &form.exercise_id)
        .await
        .unwrap_or_default();

    if is_fav {
        if let Err(e) = auth_state
            .db()
            .remove_favorite_exercise(&user_id, &form.exercise_id)
            .await
        {
            log::error!("Failed to remove favorite: {e}");
        }
    } else {
        if let Err(e) = auth_state
            .db()
            .add_favorite_exercise(&user_id, &form.exercise_id)
            .await
        {
            log::error!("Failed to add favorite: {e}");
        }
    }

    let exercises = auth_state
        .db()
        .get_all_exercises()
        .await
        .unwrap_or_default();
    let goal = auth_state.db().get_active_goal().await.unwrap_or_default();
    let total_points = auth_state.db().get_total_points().await.unwrap_or(0.0);
    let user_points = auth_state
        .db()
        .get_points_for_user(&user_id)
        .await
        .unwrap_or(0.0);
    let recent_logs = auth_state
        .db()
        .get_exercise_logs_for_user(&user_id)
        .await
        .unwrap_or_default();
    let favorite_ids = auth_state
        .db()
        .get_favorite_exercise_ids(&user_id)
        .await
        .unwrap_or_default();

    let dashboard = crate::view::dashboard::render_dashboard_content(
        &user_id,
        &exercises,
        &goal,
        total_points,
        user_points,
        &recent_logs,
        &favorite_ids,
        None,
    );

    HttpResponse::Ok().content_type("text/html").body(
        maud::html! {
            div id="dashboard-content" hx-target="this" hx-swap="outerHTML" {
                (dashboard)
            }
        }
        .into_string(),
    )
}

#[post("/delete-log")]
pub async fn delete_log_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<HodisContext>,
    form: web::Form<DeleteExerciseForm>,
) -> impl Responder {
    let token = match req.cookie("session") {
        Some(cookie) => cookie.value().to_string(),
        None => {
            return HttpResponse::Unauthorized().body(render_error("Not logged in").into_string());
        }
    };

    let user_id = match auth_state.validate_token(&token).await {
        Some(id) => id,
        None => {
            return HttpResponse::Unauthorized()
                .body(render_error("Session expired").into_string());
        }
    };

    if let Err(e) = auth_state.db().delete_exercise_log(&form.id).await {
        log::error!("Failed to delete log: {e}");
        return HttpResponse::InternalServerError()
            .body(render_error("Failed to delete log entry").into_string());
    }

    let exercises = auth_state
        .db()
        .get_all_exercises()
        .await
        .unwrap_or_default();
    let goal = auth_state.db().get_active_goal().await.unwrap_or_default();
    let total_points = auth_state.db().get_total_points().await.unwrap_or(0.0);
    let user_points = auth_state
        .db()
        .get_points_for_user(&user_id)
        .await
        .unwrap_or(0.0);
    let recent_logs = auth_state
        .db()
        .get_exercise_logs_for_user(&user_id)
        .await
        .unwrap_or_default();
    let favorite_ids = auth_state
        .db()
        .get_favorite_exercise_ids(&user_id)
        .await
        .unwrap_or_default();

    let dashboard = crate::view::dashboard::render_dashboard_content(
        &user_id,
        &exercises,
        &goal,
        total_points,
        user_points,
        &recent_logs,
        &favorite_ids,
        None,
    );

    HttpResponse::Ok().content_type("text/html").body(
        maud::html! {
            div id="dashboard-content" hx-target="this" hx-swap="outerHTML" {
                (dashboard)
            }
        }
        .into_string(),
    )
}