use std::{
    env, fs,
    path::PathBuf,
    process,
    time::{SystemTime, UNIX_EPOCH},
};

use gotcha_gitea::{Client, Provider};
use security_framework::passwords::{
    PasswordOptions, delete_generic_password_options, generic_password, get_generic_password,
    set_generic_password_options,
};

use crate::{
    RepositoryPane,
    domain::{Preferences, Server, open_status},
};

pub fn favorite_key(pane: RepositoryPane, server: &str, owner: &str, repository: &str) -> String {
    format!(
        "{}|{}",
        pane.key(),
        repository_key(server, owner, repository)
    )
}

pub fn repository_key(server: &str, owner: &str, repository: &str) -> String {
    format!("{server}|{owner}/{repository}")
}

pub fn validate_server(
    name: &str,
    url: &str,
    token: &str,
    provider: Provider,
) -> Result<Server, String> {
    let name = name.trim();
    let url = url.trim().trim_end_matches('/');
    let token = token.trim();
    if name.is_empty() {
        return Err("Give this server a name.".into());
    }
    if token.is_empty() {
        return Err("Enter an access token.".into());
    }
    Client::with_provider(url, Some(token), provider).map_err(|error| error.to_string())?;
    Ok(Server {
        name: name.into(),
        url: url.into(),
        provider,
        credential_account: String::new(),
        token: token.into(),
    })
}

const APP_GROUP: &str = "group.de.rfc1437.gotcha";
const KEYCHAIN_SERVICE: &str = "de.rfc1437.gotcha";

pub fn load_preferences(storage_directory: Option<&str>) -> Result<Preferences, String> {
    let path = preferences_path(storage_directory)?;
    let legacy = preferences_path(None)?;
    let source = if path.exists() {
        path.clone()
    } else if path != legacy && legacy.exists() {
        legacy
    } else {
        return Ok(Preferences {
            path,
            ..Preferences::default()
        });
    };
    let mut preferences: Preferences =
        serde_json::from_slice(&fs::read(&source).map_err(|error| error.to_string())?)
            .map_err(|error| format!("Cannot read {}: {error}", source.display()))?;
    preferences.path = path;
    let storage_migrated = source != preferences.path;
    let favorites_migrated = migrate_favorites(&mut preferences.favorites);
    if !matches!(preferences.issue_status.as_str(), "open" | "closed") {
        preferences.issue_status = open_status();
    }
    if !matches!(preferences.pull_status.as_str(), "open" | "closed") {
        preferences.pull_status = open_status();
    }
    let mut credentials_migrated = false;
    for server in &mut preferences.servers {
        if server.credential_account.is_empty() {
            server.credential_account = format!("{}|{}", server.name, server.url);
            credentials_migrated = true;
        }
        server.token = String::from_utf8(load_server_token(server)?)
            .map_err(|_| format!("The token for {} is not valid text.", server.name))?;
    }
    if storage_migrated || favorites_migrated || credentials_migrated {
        save_preferences(&preferences)?;
    }
    Ok(preferences)
}

fn migrate_favorites(favorites: &mut std::collections::BTreeSet<String>) -> bool {
    let legacy: Vec<_> = favorites
        .iter()
        .filter(|favorite| {
            !RepositoryPane::ALL
                .iter()
                .any(|pane| favorite.starts_with(&format!("{}|", pane.key())))
        })
        .cloned()
        .collect();
    for favorite in &legacy {
        favorites.remove(favorite);
        favorites.extend(RepositoryPane::ALL.map(|pane| format!("{}|{favorite}", pane.key())));
    }
    !legacy.is_empty()
}

pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
    let path = if preferences.path.as_os_str().is_empty() {
        preferences_path(None)?
    } else {
        preferences.path.clone()
    };
    let parent = path.parent().ok_or("Invalid app data directory.")?;
    fs::create_dir_all(parent).map_err(|error| error.to_string())?;
    let temporary = path.with_extension("tmp");
    fs::write(
        &temporary,
        serde_json::to_vec(preferences).map_err(|error| error.to_string())?,
    )
    .map_err(|error| error.to_string())?;
    fs::rename(&temporary, &path).map_err(|error| error.to_string())
}

pub fn save_server_token(server: &Server) -> Result<(), String> {
    set_generic_password_options(server.token.as_bytes(), shared_password_options(server))
        .map_err(|error| format!("Cannot save the token for {}: {error}", server.name))
}

pub fn delete_server_token(server: &Server) -> Result<(), String> {
    match delete_generic_password_options(shared_password_options(server)) {
        Ok(()) => Ok(()),
        // A missing Keychain item must not make an otherwise valid profile undeletable.
        Err(error) if error.code() == -25300 => Ok(()), // errSecItemNotFound
        Err(error) => Err(format!(
            "Cannot delete the token for {}: {error}",
            server.name
        )),
    }
}

pub fn assign_server_credential_account(server: &mut Server, existing: &[Server]) {
    if !server.credential_account.is_empty() {
        return;
    }
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let prefix = format!("server-{}-{timestamp}", process::id());
    server.credential_account = (0_u32..)
        .map(|suffix| {
            if suffix == 0 {
                prefix.clone()
            } else {
                format!("{prefix}-{suffix}")
            }
        })
        .find(|candidate| {
            existing
                .iter()
                .all(|saved| saved.credential_account != *candidate)
        })
        .expect("a unique Keychain account must exist");
}

fn preferences_path(storage_directory: Option<&str>) -> Result<PathBuf, String> {
    if let Some(directory) = storage_directory {
        let directory = directory.trim();
        if directory.is_empty() {
            return Err("Invalid shared app data directory.".into());
        }
        return Ok(PathBuf::from(directory).join("preferences.json"));
    }
    let home = env::var_os("HOME").ok_or("Cannot find the app data directory.")?;
    Ok(PathBuf::from(home).join("Library/Application Support/Gotcha/preferences.json"))
}

fn load_server_token(server: &Server) -> Result<Vec<u8>, String> {
    match generic_password(shared_password_options(server)) {
        Ok(token) => Ok(token),
        Err(error) if error.code() == -25300 => {
            let token = get_generic_password(KEYCHAIN_SERVICE, &keychain_account(server))
                .map_err(|error| format!("Cannot read the token for {}: {error}", server.name))?;
            set_generic_password_options(&token, shared_password_options(server))
                .map_err(|error| format!("Cannot share the token for {}: {error}", server.name))?;
            Ok(token)
        }
        Err(error) => Err(format!(
            "Cannot read the token for {}: {error}",
            server.name
        )),
    }
}

fn shared_password_options(server: &Server) -> PasswordOptions {
    let mut options =
        PasswordOptions::new_generic_password(KEYCHAIN_SERVICE, &keychain_account(server));
    options.set_access_group(APP_GROUP);
    options
}

fn keychain_account(server: &Server) -> String {
    if server.credential_account.is_empty() {
        format!("{}|{}", server.name, server.url)
    } else {
        server.credential_account.clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validates_servers_and_builds_stable_favorite_keys() {
        assert!(
            validate_server(
                "Work",
                "https://gitea.example.com/",
                "secret",
                Provider::Gitea
            )
            .is_ok()
        );
        assert!(
            validate_server("", "https://gitea.example.com", "secret", Provider::Gitea).is_err()
        );
        assert!(validate_server("Work", "file:///tmp/gitea", "secret", Provider::Forgejo).is_err());
        assert_eq!(
            favorite_key(
                RepositoryPane::Issues,
                "https://gitea.example.com",
                "octo",
                "demo"
            ),
            "issues|https://gitea.example.com|octo/demo"
        );
        let preferences: Preferences = serde_json::from_str(
            r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}},"pull_filters":{"server":{"milestone":"v2"}}}"#,
        )
        .unwrap();
        assert_eq!(
            preferences.issue_filters["server|octo/demo"].milestone,
            "v1"
        );
        assert!(
            preferences.issue_filters["server|octo/demo"]
                .labels
                .contains("bug")
        );
        assert_eq!(preferences.pull_filters["server"].milestone, "v2");
        assert_eq!(Preferences::default().pull_status, "open");
        assert_eq!(
            Preferences::default().appearance,
            crate::domain::AppearanceMode::Auto
        );
        assert_eq!(
            serde_json::from_str::<Preferences>(r#"{"appearance":"dark"}"#)
                .unwrap()
                .appearance,
            crate::domain::AppearanceMode::Dark
        );
        assert_eq!(
            serde_json::from_str::<Preferences>(r#"{"appearance":"future-mode"}"#)
                .unwrap()
                .appearance,
            crate::domain::AppearanceMode::Auto
        );
        let legacy_server: Server =
            serde_json::from_str(r#"{"name":"Work","url":"https://gitea.example.com"}"#).unwrap();
        assert_eq!(legacy_server.provider, Provider::Gitea);
        assert!(legacy_server.credential_account.is_empty());
        assert_eq!(
            keychain_account(&legacy_server),
            "Work|https://gitea.example.com"
        );
        let mut new_server = legacy_server.clone();
        assign_server_credential_account(&mut new_server, &[]);
        assert!(new_server.credential_account.starts_with("server-"));
        assert_eq!(keychain_account(&new_server), new_server.credential_account);
        assert_eq!(
            crate::domain::AppearanceMode::from_index(1),
            Some(crate::domain::AppearanceMode::Light)
        );
        assert_eq!(crate::domain::AppearanceMode::from_index(3), None);
    }

    #[test]
    fn migrates_global_favorites_to_each_repository_pane() {
        let mut favorites = ["https://gitea.example.com|octo/demo".to_string()]
            .into_iter()
            .collect();

        assert!(migrate_favorites(&mut favorites));
        assert_eq!(
            favorites,
            [
                "commits|https://gitea.example.com|octo/demo".to_string(),
                "issues|https://gitea.example.com|octo/demo".to_string(),
                "milestones|https://gitea.example.com|octo/demo".to_string(),
            ]
            .into_iter()
            .collect()
        );
        assert!(!migrate_favorites(&mut favorites));
    }

    #[test]
    fn saves_preferences_to_the_selected_storage_directory() {
        let directory = env::temp_dir().join(format!(
            "gotcha-preferences-{}-{}",
            process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let path = directory.join("preferences.json");
        let preferences = Preferences {
            path: path.clone(),
            pull_status: "closed".into(),
            ..Preferences::default()
        };

        save_preferences(&preferences).unwrap();
        let stored = fs::read_to_string(&path).unwrap();
        assert!(!stored.contains(path.to_string_lossy().as_ref()));
        assert_eq!(
            serde_json::from_str::<Preferences>(&stored)
                .unwrap()
                .pull_status,
            "closed"
        );

        fs::remove_file(path).unwrap();
        fs::remove_dir(directory).unwrap();
    }
}
