use std::{collections::HashMap, sync::Arc};

use eyre::{anyhow, bail, Result};
use zbus::{
    blocking::Connection,
    zvariant::{ObjectPath, Value},
    Message,
};

pub const APP_ID: &str = "rookie";

/// Get password from either kdewallet or libsecret (ubuntu)
pub fn get_passwords(unix_crypt_name: &str) -> Result<Vec<String>> {
    // Attempt to get the password from libsecret
    let mut passwords: Vec<String> = vec![];
    for schema in [
        "chrome_libsecret_os_crypt_password_v2",
        "chrome_libsecret_os_crypt_password_v1",
    ] {
        if let Ok(libsecret_pass) =
            get_password_libsecret(schema, unix_crypt_name)
        {
            passwords.push(libsecret_pass);
        }
    }
    // Attempt to get the password from kdewallet
    if let Ok(password) = get_password_kdewallet(unix_crypt_name) {
        passwords.push(password);
    }

    Ok(passwords)
}

fn libsecret_call<T>(
    connection: &Connection,
    method: &str,
    args: T,
) -> zbus::Result<Arc<Message>>
where
    T: serde::ser::Serialize + zvariant::DynamicType,
{
    connection.call_method(
        Some("org.freedesktop.secrets"),
        "/org/freedesktop/secrets",
        Some("org.freedesktop.Secret.Service"),
        method,
        &args,
    )
}

fn kwallet_call<T>(
    connection: &Connection,
    method: &str,
    args: T,
) -> zbus::Result<Arc<Message>>
where
    T: serde::ser::Serialize + zvariant::DynamicType,
{
    connection.call_method(
        Some("org.kde.kwalletd5"),
        "/modules/kwalletd5",
        Some("org.kde.KWallet"),
        method,
        &args,
    )
}

fn get_password_libsecret(schema: &str, crypt_name: &str) -> Result<String> {
    let connection = Connection::session()?;
    let mut content = HashMap::<&str, &str>::new();
    content.insert("xdg:schema", schema);
    content.insert("application", crypt_name);
    let m = libsecret_call(&connection, "SearchItems", &content)?;
    let (reply_paths, _): (Vec<ObjectPath>, Vec<ObjectPath>) = m.body()?;
    let path = reply_paths.first().ok_or(anyhow!("search items empty"))?;

    let m = libsecret_call(&connection, "Unlock", vec![path])?;
    let reply: (Vec<ObjectPath>, ObjectPath) = m.body()?;
    let object_path = reply.0.first().ok_or(anyhow!("Can't unlock"))?;

    let mut content = HashMap::<&str, &str>::new();
    content.insert("plain", "");
    let m =
        libsecret_call(&connection, "OpenSession", &("plain", Value::new("")))?;

    let reply: (Value, ObjectPath) = m.body()?;
    let session = reply.1;

    let m = libsecret_call(
        &connection,
        "GetSecrets",
        &(vec![object_path], session),
    )?;
    type Response<'a> = (ObjectPath<'a>, Vec<u8>, Vec<u8>, String);
    let reply: HashMap<ObjectPath, Response> = m.body()?;
    let inner = reply.get(object_path).ok_or(anyhow!("Can't get secrets"))?;
    let secret = &inner.2;

    Ok(String::from_utf8(secret.clone())?)
}

fn get_password_kdewallet(crypt_name: &str) -> Result<String> {
    let connection = Connection::session()?;
    let folder = format!("{} Keys", capitalize(crypt_name));
    let key = format!("{} Safe Storage", capitalize(crypt_name));

    let m = kwallet_call(&connection, "networkWallet", ())?;
    let network_wallet: String = m.body()?;

    let m = kwallet_call(
        &connection,
        "open",
        (network_wallet.clone(), 0_i64, APP_ID),
    )?;
    let handle: i32 = m.body()?;
    let m = kwallet_call(
        &connection,
        "readPassword",
        (handle, folder, key, APP_ID),
    )?;
    let password: String = m.body()?;
    let m = kwallet_call(&connection, "close", (network_wallet, false))?;
    let close_ok: i32 = m.body()?;
    if close_ok != 1 {
        bail!("Close failed");
    }

    Ok(password)
}

pub fn capitalize(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
    }
}
