use std::{collections::HashSet, str::FromStr};

use axum::async_trait;
use axum_login::{AuthUser, AuthnBackend, AuthzBackend, UserId};
use password_auth::{generate_hash, verify_password, VerifyError};
use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, Secret, SerializableSecret, Zeroize};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal};
use ulid::Ulid;

// A wrapper around a string meant for password hashes
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Password(String);

impl Password {
    pub fn new<S: AsRef<str>>(password: S) -> Self {
        Self(generate_hash(password.as_ref()))
    }

    pub fn verify<S: AsRef<str>>(&self, attempt: S) -> bool {
        match verify_password(attempt.as_ref(), &self.0) {
            Ok(()) => true,
            Err(VerifyError::PasswordInvalid) => false,
            Err(VerifyError::Parse(_)) => false,
        }
    }
}

impl Zeroize for Password {
    fn zeroize(&mut self) {
        self.0.zeroize();
    }
}
impl SerializableSecret for Password {}
impl CloneableSecret for Password {}
impl DebugSecret for Password {}

pub type SecretPassword = Secret<Password>;

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct User {
    id: Thing,
    email: String,
    pub username: String,
    password: SecretPassword,
}

impl User {
    pub fn code(&self) -> u64 {
        const DEFAULT_CODE: u64 = 3033132;
        const MAX_CODE: u64 = 999999;
        // Assume if we aren't parseable as a ULID, that our "code" is 3033132
        let uuid: uuid::Uuid = match uuid::Uuid::from_str(&self.id.id.to_raw()) {
            Ok(uuid) => uuid,
            Err(_) => return DEFAULT_CODE,
        };
        let ulid: Ulid = uuid.into();
        ulid.timestamp_ms() % MAX_CODE
    }
}

impl AuthUser for User {
    type Id = Thing;

    fn id(&self) -> Self::Id {
        self.id.clone()
    }

    fn session_auth_hash(&self) -> &[u8] {
        self.password.expose_secret().0.as_bytes()
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
    pub email: String,
    pub password: String,
    /// Where do we want to redirect to?
    pub next: Option<String>,
}

#[derive(Debug, Clone)]
pub struct Backend {
    db: Surreal<Client>,
}

impl Backend {
    pub fn new(db: Surreal<Client>) -> Self {
        Self { db }
    }
}

#[async_trait]
impl AuthnBackend for Backend {
    type User = User;
    type Credentials = Credentials;
    type Error = surrealdb::Error;

    async fn authenticate(
        &self,
        creds: Self::Credentials,
    ) -> Result<Option<Self::User>, Self::Error> {
        let user: Option<User> = self
            .db
            .query("SELECT email, username, password, id, code FROM user WHERE email = <string> $email")
            .bind(("email", creds.email.to_ascii_lowercase()))
            .await?
            .take(0)?;

        Ok(user.filter(|user| user.password.expose_secret().verify(creds.password)))
    }

    async fn get_user(&self, user_id: &UserId<Backend>) -> Result<Option<Self::User>, Self::Error> {
        let user: Option<User> = self
            .db
            .query("SELECT username, password, email, code, id FROM user WHERE id = $id")
            .bind(("id", user_id))
            .await?
            .take(0)?;

        Ok(user)
    }
}

#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone, Serialize)]
struct Record {
    #[allow(dead_code)]
    id: Thing,
}

#[derive(Hash, PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct Permission {
    id: Thing,
    aspect: String,
    subject: Option<Record>,
}

#[async_trait]
impl AuthzBackend for Backend {
    type Permission = Permission;

    async fn get_user_permissions(
        &self,
        user: &Self::User,
    ) -> Result<HashSet<Permission>, Self::Error> {
        let user_permissions: Vec<Permission> = self
            .db
            .query(r#"SELECT id, aspect, ->granted->user FROM permission WHERE user == $id"#)
            .bind(("id", user.id.clone()))
            .await?
            .take(0)?;
        Ok(user_permissions.into_iter().collect())
    }

    async fn get_group_permissions(
        &self,
        user: &Self::User,
    ) -> Result<HashSet<Permission>, Self::Error> {
        let group_permissions: Vec<Permission> = self.db.query(r#"SELECT id, aspect, ->granted->group<-member_of<-user FROM permission WHERE user == $id"#).bind(("id", user.id.clone())).await?.take(0)?;
        Ok(group_permissions.into_iter().collect())
    }
}

pub type AuthSession = axum_login::AuthSession<Backend>;