use crate::{
    entity::setting,
    error::{Error, ErrorCode},
    AppState,
};
use actix_web::{dev::ServiceRequest, web::Data};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use bcrypt::verify;
use jsonwebtoken::{decode, DecodingKey, Validation};
use sea_orm::{prelude::*, ActiveValue::NotSet, Set};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AuthClaims {
    pub exp: usize,
}

/// JWT validation middleware. Used to wrap protected routes in Actix .
pub async fn validator(
    req: ServiceRequest,
    credentials: BearerAuth,
) -> Result<ServiceRequest, actix_web::error::Error> {
    // Retrieve shared app state
    let state = req
        .app_data::<Data<AppState>>()
        .ok_or_else(|| Error::new(ErrorCode::Internal, "Could not retrieve state"))?;
    // Get the secret from the database
    let secret = get_secret(&state.db).await?;
    // decode the JWT token from the header
    let decoded = decode::<AuthClaims>(
        credentials.token(),
        &DecodingKey::from_secret(&secret),
        &Validation::default(),
    );
    // We have no users or anything else to do, if the token is properly formed we are good to go.
    match decoded {
        Ok(_) => Ok(req),
        Err(e) => Err(Error::new(ErrorCode::UnAuthorized, &e.to_string()).into()),
    }
}

/// Verifies the passed password against the password hash stored in the settings table in the database.
pub async fn verify_password(db: &DatabaseConnection, password: &str) -> bool {
    let rec: Option<crate::entity::setting::Model> = crate::entity::setting::Entity::find()
        .filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string()))
        .one(db)
        .await
        .unwrap_or_default();
    if let Some(rec) = rec {
        verify(password, &rec.setting_value).unwrap_or(false)
    } else {
        false
    }
}

pub async fn set_password(db: &DatabaseConnection, password: &str) -> crate::error::Result<()> {
    let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST)
        .map_err(|e| Error::new(ErrorCode::Internal, &format!("Hash error: {}", e)))?;
    let rec = crate::entity::setting::Entity::find()
        .filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string()))
        .one(db)
        .await?;

    if let Some(rec) = rec {
        setting::ActiveModel {
            id: Set(rec.id),
            setting_value: Set(password_hash),
            setting_name: NotSet,
        }
        .update(db)
        .await?;
    } else {
        setting::ActiveModel {
            id: NotSet,
            setting_name: Set("password_hash".into()),
            setting_value: Set(password_hash),
        }
        .insert(db)
        .await?;
    }
    Ok(())
}

use rand::Rng;
use tracing::instrument;
#[instrument(skip_all)]
/// Generate a new secret and store it to the settings table. This is used during intiial configuration.
pub async fn generate_secret(db: &DatabaseConnection) -> crate::error::Result<()> {
    let b = rand::thread_rng().gen::<[u8; 32]>();
    setting::ActiveModel {
        id: NotSet,
        setting_name: Set("secret".to_string()),
        setting_value: Set(base64::encode(b)),
    }
    .insert(db)
    .await?;
    Ok(())
}

#[instrument(skip_all)]
/// Retrieves the JWT secret from the database.
pub async fn get_secret(db: &DatabaseConnection) -> crate::error::Result<Vec<u8>> {
    let rec = setting::Entity::find()
        .filter(setting::Column::SettingName.eq("secret".to_string()))
        .one(db)
        .await?;

    if let Some(rec) = rec {
        if let Ok(b64) = base64::decode(rec.setting_value) {
            Ok(b64)
        } else {
            Err(Error::new(ErrorCode::Internal, "Could not decode secret"))
        }
    } else {
        Err(Error::new(ErrorCode::NoSetup, "Not setup"))
    }
}

#[cfg(test)]
mod tests {
    use jsonwebtoken::{decode, DecodingKey, Validation};

    use super::{get_secret, AuthClaims};

    #[actix_rt::test]
    async fn test_tokens() {
        let state = crate::api::test_prelude::setup_state().await.unwrap();
        let jwt_secret = get_secret(&state.db).await.unwrap();
        let token = jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            &crate::auth::AuthClaims {
                exp: 10_000_000_000,
            },
            &jsonwebtoken::EncodingKey::from_secret(&jwt_secret),
        )
        .unwrap();

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

        decoded.expect("Decode failure");
    }
}
