use std::ops::Add;

use jsonwebtoken::{encode, EncodingKey, Header};
use tracing::instrument;

use crate::api::api_prelude::*;
use crate::auth::{generate_secret, get_secret, set_password, verify_password};
use crate::error::{Error, Result};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthRequest {
    password: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthResponse {
    token: String,
}

#[instrument]
#[post("authorize")]
pub async fn authorize(
    state: web::Data<AppState>,
    req: web::Json<AuthRequest>,
) -> Result<HttpResponse> {
    if verify_password(&state.db, &req.password).await {
        let secret = get_secret(&state.db).await?;
        let exp = chrono::Utc::now()
            .add(chrono::Duration::days(30))
            .timestamp() as usize;
        let token = encode(
            &Header::default(),
            &crate::auth::AuthClaims { exp },
            &EncodingKey::from_secret(&secret),
        )
        .map_err(|_| Error::unauthorized())?;
        Ok(HttpResponse::Ok().json(AuthResponse { token }))
    } else {
        Err(Error::unauthorized())
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePasswordRequest {
    password: String,
}

#[instrument]
#[put("password")]
pub async fn update_password(
    state: web::Data<AppState>,
    req: web::Json<UpdatePasswordRequest>,
) -> Result<HttpResponse> {
    set_password(&state.db, &req.password).await?;
    Ok(HttpResponse::Ok().body(""))
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupRequest {
    password: String,
}

#[instrument]
#[post("setup")]
pub async fn initial_setup(
    state: web::Data<AppState>,
    req: web::Json<SetupRequest>,
) -> Result<HttpResponse> {
    let count = setting::Entity::find()
        .filter(setting::Column::SettingName.eq("password_hash".to_string()))
        .count(&state.db)
        .await?;
    if count == 0 {
        set_password(&state.db, &req.password).await?;
        generate_secret(&state.db).await?;
        Ok(HttpResponse::Ok().body(""))
    } else {
        Err(Error::new(
            crate::error::ErrorCode::UnAuthorized,
            "Setup has already been run",
        ))
    }
}

#[cfg(test)]
mod tests {

    use crate::{
        api::test_prelude::*,
        auth::{get_secret, AuthClaims},
    };
    use actix_web::http::Method;

    #[actix_rt::test]
    async fn test_authorize() -> Result<()> {
        let state = setup_state().await?;

        let test_password = "sshh a secret".to_string();
        let test_hash = bcrypt::hash(test_password.clone(), bcrypt::DEFAULT_COST).unwrap();

        setting::ActiveModel {
            id: NotSet,
            setting_name: Set("password_hash".into()),
            setting_value: Set(test_hash),
        }
        .insert(&state.db)
        .await?;

        let mut req = actix_web::test::TestRequest::with_uri("/api/authorize")
            .method(Method::POST)
            .set_json(super::AuthRequest {
                password: test_password,
            })
            .to_request();
        let resp = call_endpoint!(req, state);

        let jwt_secret = get_secret(&state.db).await?;

        let status = resp.status();
        assert_eq!(status, 200);
        let data = get_response!(resp, super::AuthResponse);
        assert_eq!(status, 200);

        let decoded = jsonwebtoken::decode::<AuthClaims>(
            &data.token,
            &jsonwebtoken::DecodingKey::from_secret(&jwt_secret),
            &jsonwebtoken::Validation::default(),
        );

        decoded.expect("Decode failure");

        Ok(())
    }
}
