use std::time::SystemTime;

use ed25519_dalek::SecretKey;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use sqlx::PgExecutor;

use crate::hostname::{generate_hostname, generate_id, Hostname};
use crate::net::keys::Key;
use crate::net::ssl::{generate_key, make_root_cert};
use crate::prelude::*;
use crate::util::crypto::ed25519_expand_key;

fn hash_password(password: &str) -> Result<String, Error> {
    argon2::hash_encoded(
        password.as_bytes(),
        &rand::random::<[u8; 16]>()[..],
        &argon2::Config::rfc9106_low_mem(),
    )
    .with_kind(crate::ErrorKind::PasswordHashGeneration)
}

#[derive(Debug, Clone)]
pub struct AccountInfo {
    pub server_id: String,
    pub hostname: Hostname,
    pub password: String,
    pub key: Key,
    pub root_ca_key: PKey<Private>,
    pub root_ca_cert: X509,
}
impl AccountInfo {
    pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
        let server_id = generate_id();
        let hostname = generate_hostname();
        let root_ca_key = generate_key()?;
        let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
        Ok(Self {
            server_id,
            hostname,
            password: hash_password(password)?,
            key: Key::new(None),
            root_ca_key,
            root_ca_cert,
        })
    }

    pub async fn load(secrets: impl PgExecutor<'_>) -> Result<Self, Error> {
        let r = sqlx::query!("SELECT * FROM account WHERE id = 0")
            .fetch_one(secrets)
            .await?;

        let server_id = r.server_id.unwrap_or_else(generate_id);
        let hostname = r.hostname.map(Hostname).unwrap_or_else(generate_hostname);
        let password = r.password;
        let network_key = SecretKey::try_from(r.network_key).map_err(|e| {
            Error::new(
                eyre!("expected vec of len 32, got len {}", e.len()),
                ErrorKind::ParseDbField,
            )
        })?;
        let tor_key = if let Some(k) = &r.tor_key {
            <[u8; 64]>::try_from(&k[..]).map_err(|_| {
                Error::new(
                    eyre!("expected vec of len 64, got len {}", k.len()),
                    ErrorKind::ParseDbField,
                )
            })?
        } else {
            ed25519_expand_key(&network_key)
        };
        let key = Key::from_pair(None, network_key, tor_key);
        let root_ca_key = PKey::private_key_from_pem(r.root_ca_key_pem.as_bytes())?;
        let root_ca_cert = X509::from_pem(r.root_ca_cert_pem.as_bytes())?;

        Ok(Self {
            server_id,
            hostname,
            password,
            key,
            root_ca_key,
            root_ca_cert,
        })
    }

    pub async fn save(&self, secrets: impl PgExecutor<'_>) -> Result<(), Error> {
        let server_id = self.server_id.as_str();
        let hostname = self.hostname.0.as_str();
        let password = self.password.as_str();
        let network_key = self.key.as_bytes();
        let network_key = network_key.as_slice();
        let root_ca_key = String::from_utf8(self.root_ca_key.private_key_to_pem_pkcs8()?)?;
        let root_ca_cert = String::from_utf8(self.root_ca_cert.to_pem()?)?;

        sqlx::query!(
            r#"
            INSERT INTO account (
                id,
                server_id,
                hostname,
                password,
                network_key,
                root_ca_key_pem,
                root_ca_cert_pem
            ) VALUES (
                0, $1, $2, $3, $4, $5, $6
            ) ON CONFLICT (id) DO UPDATE SET
                server_id = EXCLUDED.server_id,
                hostname = EXCLUDED.hostname,
                password = EXCLUDED.password,
                network_key = EXCLUDED.network_key,
                root_ca_key_pem = EXCLUDED.root_ca_key_pem,
                root_ca_cert_pem = EXCLUDED.root_ca_cert_pem
            "#,
            server_id,
            hostname,
            password,
            network_key,
            root_ca_key,
            root_ca_cert,
        )
        .execute(secrets)
        .await?;

        Ok(())
    }

    pub fn set_password(&mut self, password: &str) -> Result<(), Error> {
        self.password = hash_password(password)?;
        Ok(())
    }
}
