use rocket::{
    http::{CookieJar, Status},
    response::{Redirect, status::BadRequest},
    serde::json::Json,
    time::OffsetDateTime,
};
use rocket_db_pools::Connection;
use rocket_dyn_templates::{Template, context};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{auth::session::Session, db::DbConn};

#[derive(Serialize, Deserialize)]
pub struct SignupCredentials {
    pub email: String,
    pub username: String,
    pub password: String,
    pub access_token: String,
}

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

#[get("/signup")]
pub async fn signup_page() -> Template {
    Template::render("signup", context!())
}

#[post("/signup", data = "<cred>")]
pub async fn signup(
    cred: Json<SignupCredentials>,
    jar: &CookieJar<'_>,
    mut db: Connection<DbConn>,
) -> Result<Redirect, BadRequest<String>> {
    println!("phase 1 {}", cred.access_token);
    let token_id = AccessToken::validate(&cred.access_token, &mut db).await?;

    println!("phase 2");
    let result = sqlx::query!(
        "INSERT INTO users (email, username, password) VALUES ($1, $2, $3) RETURNING id",
        cred.email,
        cred.username,
        cred.password
    )
    .fetch_one(&mut **db)
    .await
    .map_err(|_| BadRequest(String::from("Failed to create user")))?;

    println!("phase 3");
    let session = Session::new(result.id as usize);
    if let Err(e) = session.commit(&mut db).await {
        eprintln!("Failed to create session: {}", e);
        return Err(BadRequest(String::from("Failed to create session")));
    }

    println!("phase 4");
    jar.add_private(("session", session.token));

    token_id.use_token(&mut db).await?;

    println!("phase 5");
    return Ok(Redirect::to("/chat"));
}

#[get("/login")]
pub async fn login_page() -> Template {
    Template::render("login", context!())
}

#[post("/login", data = "<cred>")]
pub async fn login(
    mut db: Connection<DbConn>,
    jar: &CookieJar<'_>,
    cred: Json<LoginCredentials>,
) -> Result<Redirect, Status> {
    if let Ok(row) = sqlx::query!(
        "SELECT id FROM users WHERE username = $1 AND password = $2",
        cred.username,
        cred.password,
    )
    .fetch_one(&mut **db)
    .await
    {
        let session = Session::new(row.id as usize);
        if let Err(e) = session.commit(&mut db).await {
            eprintln!("Failed to create session: {}", e);
            return Err(Status::InternalServerError);
        }

        jar.add_private(("session", session.token));
        return Ok(Redirect::to("/chat"));
    }

    // TODO: implement actual login logic, e.g. verify password and generate token
    Err(Status::Unauthorized)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessTokenForm {
    pub name: String,
    pub max_uses: usize,
    pub expiry_date: usize,
    pub start_date: usize,
}

#[get("/invite")]
pub async fn invite_page(_s: Session) -> Template {
    Template::render("invite", context! {})
}

#[post("/invite", data = "<form>")]
pub async fn generate_invite(
    session: Session,
    mut db: Connection<DbConn>,
    form: Json<AccessTokenForm>,
) -> Result<String, Status> {
    if form.start_date > form.expiry_date {
        return Err(Status::BadRequest);
    }

    let code = Uuid::new_v4().to_string();
    sqlx::query!(
        "INSERT INTO access_codes (name, code, creator_id, max_uses, created_at, expires_at)
        VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
        form.name,
        code,
        session.user_id as i32,
        form.max_uses as i32,
        OffsetDateTime::from_unix_timestamp_nanos(form.start_date as i128 * 1_000_000).unwrap(),
        OffsetDateTime::from_unix_timestamp_nanos(form.expiry_date as i128 * 1_000_000).unwrap()
    )
    .fetch_one(&mut **db)
    .await
    .map_err(|_| Status::InternalServerError)?;

    Ok(code)
}

pub struct AccessToken {
    id: i32,
    _code: String,
}

impl AccessToken {
    pub async fn validate(
        token: &str,
        db: &mut Connection<DbConn>,
    ) -> Result<AccessToken, BadRequest<String>> {
        match sqlx::query!(
            "SELECT id FROM access_codes
                WHERE code = $1
                AND created_at < NOW()
                AND expires_at > NOW()
                AND uses < max_uses",
            token
        )
        .fetch_one(&mut ***db)
        .await
        {
            Ok(row) => Ok(AccessToken {
                id: row.id,
                _code: token.to_string(),
            }),
            Err(_) => Err(BadRequest(String::from("Invalid or Expired token!"))),
        }
    }

    pub async fn use_token(&self, db: &mut Connection<DbConn>) -> Result<(), BadRequest<String>> {
        sqlx::query!(
            "UPDATE access_codes SET uses = uses + 1 WHERE id = $1",
            self.id
        )
        .execute(&mut ***db)
        .await
        .map_err(|_| BadRequest(String::from("Invalid or Expired token!")))?;
        Ok(())
    }
}
