/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/

use crate::State;
use anyhow::{Result, anyhow, bail};
use argon2::{Argon2, PasswordHasher, password_hash::Salt};
use jellycommon::{
    USER_LOGIN, USER_PASSWORD,
    jellyobject::{ObjectBuffer, Path},
};
use jellydb::{Filter, Query, Sort};

pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> {
    let user_row = token::validate(&state.session_key, token)?;

    let mut user = None;
    state.database.transaction(&mut |txn| {
        user = txn.get(user_row)?;
        Ok(())
    })?;

    user.ok_or(anyhow!("user was deleted"))
}

pub fn login(state: &State, username: &str, password: &str, expire: Option<i64>) -> Result<String> {
    let password = hash_password(username, password);

    let mut user_row = None;
    let mut user = None;
    state.database.transaction(&mut |txn| {
        user_row = txn.query_single(Query {
            filter: Filter::Match(Path(vec![USER_LOGIN.0]), username.into()),
            sort: Sort::None,
        })?;
        if let Some(ur) = user_row {
            user = txn.get(ur)?;
        }
        Ok(())
    })?;

    let (Some(user_row), Some(user)) = (user_row, user) else {
        bail!("unknown user");
    };
    let Some(correct_pw) = user.as_object().get(USER_PASSWORD) else {
        bail!("password login is disabled")
    };
    if password != correct_pw {
        bail!("incorrect password")
    }

    Ok(token::create(
        &state.session_key,
        user_row,
        expire.unwrap_or(60 * 60 * 24 * 30),
    ))
}

pub fn hash_password(username: &str, password: &str) -> Vec<u8> {
    Argon2::default()
        .hash_password(
            format!("{username}\0{password}").as_bytes(),
            <&str as TryInto<Salt>>::try_into("IYMa13osbNeLJKnQ1T8LlA").unwrap(),
        )
        .unwrap()
        .hash
        .unwrap()
        .as_bytes()
        .to_vec()
}

pub mod token {
    use aes_gcm_siv::{
        Aes256GcmSiv, KeyInit,
        aead::{Aead, generic_array::GenericArray},
    };
    use anyhow::{Result, anyhow, bail};
    use base64::{
        Engine,
        engine::general_purpose::{STANDARD, URL_SAFE},
    };
    use chrono::Utc;
    use jellydb::RowNum;

    pub struct SessionKey(Aes256GcmSiv);

    impl SessionKey {
        pub fn parse(s: &str) -> Result<Self> {
            let k = STANDARD.decode(s)?;
            Ok(Self(Aes256GcmSiv::new_from_slice(&k)?))
        }
    }

    pub fn create(sk: &SessionKey, user: RowNum, expire: i64) -> String {
        let expire_ts = Utc::now().timestamp() + expire;
        let mut plain = Vec::new();
        plain.extend(user.to_be_bytes());
        plain.extend(expire_ts.to_be_bytes());

        let nonce = [(); 12].map(|_| rand::random());
        let mut ciper =
            sk.0.encrypt(&GenericArray::from(nonce), plain.as_slice())
                .unwrap();
        ciper.extend(nonce);
        URL_SAFE.encode(&ciper)
    }
    pub fn validate(sk: &SessionKey, token: &str) -> Result<RowNum> {
        let cipher = URL_SAFE.decode(token)?;
        let (cipher, nonce) = cipher.split_at(cipher.len() - 12);
        let plain =
            sk.0.decrypt(nonce.into(), cipher)
                .map_err(|_| anyhow!("invalid session"))?;

        let user = RowNum::from_be_bytes(plain[0..8].try_into().unwrap());
        let expire_ts = i64::from_be_bytes(plain[8..16].try_into().unwrap());

        if Utc::now().timestamp() > expire_ts {
            bail!("session expired")
        } else {
            Ok(user)
        }
    }
}
