//! Platform folder pickers without helper processes.

use std::path::PathBuf;

use ironstorage::repository::SecretBytes;

#[cfg(any(target_os = "macos", target_os = "windows"))]
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
    let mut dialog = rfd::AsyncFileDialog::new().set_title("Open Password Store");
    if let Some(initial) = initial.filter(|path| path.is_dir()) {
        dialog = dialog.set_directory(initial);
    }
    Ok(dialog
        .pick_folder()
        .await
        .map(|folder| folder.path().to_owned()))
}

#[cfg(any(target_os = "macos", target_os = "windows"))]
pub async fn pick_qr_image() -> Result<Option<SecretBytes>, String> {
    let selected = rfd::AsyncFileDialog::new()
        .set_title("Import OTP QR Image")
        .add_filter("Image", &["png", "jpg", "jpeg", "gif"])
        .pick_file()
        .await;
    selected
        .map(|file| {
            std::fs::read(file.path())
                .map(SecretBytes::new)
                .map_err(|error| error.to_string())
        })
        .transpose()
}

#[cfg(any(target_os = "macos", target_os = "windows"))]
pub async fn pick_kdbx_file() -> Result<Option<PathBuf>, String> {
    pick_file("Import KeePass Database", "KeePass database", &["kdbx"]).await
}

#[cfg(any(target_os = "macos", target_os = "windows"))]
pub async fn pick_key_file() -> Result<Option<PathBuf>, String> {
    pick_file("Select KeePass Key File", "Key file", &["key", "keyx"]).await
}

#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn pick_file(
    title: &str,
    filter_name: &str,
    extensions: &[&str],
) -> Result<Option<PathBuf>, String> {
    Ok(rfd::AsyncFileDialog::new()
        .set_title(title)
        .add_filter(filter_name, extensions)
        .pick_file()
        .await
        .map(|file| file.path().to_owned()))
}

#[cfg(target_os = "linux")]
pub async fn pick_folder(initial: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
    use ashpd::{
        PortalError,
        desktop::{file_chooser::SelectedFiles, request::ResponseError},
    };

    let mut request = SelectedFiles::open_file()
        .title("Open Password Store")
        .accept_label("Open")
        .modal(true)
        .multiple(false)
        .directory(true);
    if let Some(initial) = initial.filter(|path| path.is_dir()) {
        request = request
            .current_folder(initial)
            .map_err(|error| error.to_string())?;
    }
    let response = request.send().await.and_then(|request| request.response());
    let selected = match response {
        Ok(selected) => selected,
        Err(ashpd::Error::Response(ResponseError::Cancelled))
        | Err(ashpd::Error::Portal(PortalError::Cancelled(_))) => return Ok(None),
        Err(error) => return Err(error.to_string()),
    };
    selected
        .uris()
        .first()
        .ok_or_else(|| "folder portal returned no selection".to_owned())
        .and_then(|uri| file_uri_path(uri.as_str()))
        .map(Some)
}

#[cfg(target_os = "linux")]
pub async fn pick_qr_image() -> Result<Option<SecretBytes>, String> {
    use ashpd::{
        PortalError,
        desktop::{file_chooser::SelectedFiles, request::ResponseError},
    };

    let response = SelectedFiles::open_file()
        .title("Import OTP QR Image")
        .accept_label("Import")
        .modal(true)
        .multiple(false)
        .send()
        .await
        .and_then(|request| request.response());
    let selected = match response {
        Ok(selected) => selected,
        Err(ashpd::Error::Response(ResponseError::Cancelled))
        | Err(ashpd::Error::Portal(PortalError::Cancelled(_))) => return Ok(None),
        Err(error) => return Err(error.to_string()),
    };
    let Some(uri) = selected.uris().first() else {
        return Err("file portal returned no selection".to_owned());
    };
    let path = file_uri_path(uri.as_str())?;
    std::fs::read(path)
        .map(SecretBytes::new)
        .map(Some)
        .map_err(|error| error.to_string())
}

#[cfg(target_os = "linux")]
pub async fn pick_kdbx_file() -> Result<Option<PathBuf>, String> {
    pick_file("Import KeePass Database", "Import").await
}

#[cfg(target_os = "linux")]
pub async fn pick_key_file() -> Result<Option<PathBuf>, String> {
    pick_file("Select KeePass Key File", "Select").await
}

#[cfg(target_os = "linux")]
async fn pick_file(title: &str, accept_label: &str) -> Result<Option<PathBuf>, String> {
    use ashpd::{
        PortalError,
        desktop::{file_chooser::SelectedFiles, request::ResponseError},
    };

    let response = SelectedFiles::open_file()
        .title(title)
        .accept_label(accept_label)
        .modal(true)
        .multiple(false)
        .send()
        .await
        .and_then(|request| request.response());
    let selected = match response {
        Ok(selected) => selected,
        Err(ashpd::Error::Response(ResponseError::Cancelled))
        | Err(ashpd::Error::Portal(PortalError::Cancelled(_))) => return Ok(None),
        Err(error) => return Err(error.to_string()),
    };
    selected
        .uris()
        .first()
        .ok_or_else(|| "file portal returned no selection".to_owned())
        .and_then(|uri| file_uri_path(uri.as_str()))
        .map(Some)
}

#[cfg(target_os = "linux")]
fn file_uri_path(uri: &str) -> Result<PathBuf, String> {
    let uri = url::Url::parse(uri).map_err(|_| "folder portal returned an invalid URI")?;
    uri.to_file_path()
        .map_err(|()| "folder portal returned a non-file URI".to_owned())
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
    use super::*;

    #[test]
    fn portal_file_uris_are_decoded_and_other_schemes_are_rejected() {
        assert_eq!(
            file_uri_path("file:///tmp/password%20store").expect("file URI"),
            PathBuf::from("/tmp/password store")
        );
        assert!(file_uri_path("https://example.test/store").is_err());
    }
}
