/* credentials.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use oo7::Keyring;

const APP_ATTR: &str = "de.wwwtech.gitte";

#[derive(Debug, Clone)]
pub struct Credential {
    pub username: String,
    pub password: String,
}

pub struct CredentialStore {
    keyring: Keyring,
}

impl CredentialStore {
    pub async fn load() -> Result<Self, oo7::Error> {
        let keyring = Keyring::new().await?;
        Ok(Self { keyring })
    }

    pub async fn lookup(&self, url: &str, user: Option<&str>) -> Result<Option<Credential>, oo7::Error> {
        let url = normalize_url(url);
        let attrs = base_attrs(&url, user);
        let items = self.keyring.search_items(&attrs).await?;

        for item in items {
            let attrs = item.attributes().await?;

            let Some(username) = attrs.get("user").cloned() else {
                continue;
            };

            let secret = item.secret().await?;
            let password = String::from_utf8_lossy(secret.as_ref()).into_owned();

            return Ok(Some(Credential { username, password }));
        }

        Ok(None)
    }

    pub async fn store(&self, url: &str, user: &str, password: &str) -> Result<(), oo7::Error> {
        let url = normalize_url(url);
        let attrs = base_attrs(&url, Some(user));
        let label = format!("Gitte: {url}");

        self.keyring
            .create_item(&label, &attrs, password.as_bytes(), true)
            .await
    }

    pub async fn forget(&self, url: &str, user: Option<&str>) -> Result<(), oo7::Error> {
        let url = normalize_url(url);
        let attrs = base_attrs(&url, user);

        self.keyring.delete(&attrs).await
    }
}

fn base_attrs<'a>(url: &'a str, user: Option<&'a str>) -> Vec<(&'static str, &'a str)> {
    let mut v = vec![("app", APP_ATTR), ("url", url)];

    if let Some(u) = user {
        v.push(("user", u));
    }

    v
}

pub fn forget_blocking(url: &str) {
    let url = url.to_string();

    let Ok(rt) = tokio::runtime::Builder::new_current_thread().enable_all().build() else {
        return;
    };

    let _ = rt.block_on(async move {
        let store = CredentialStore::load().await?;
        store.forget(&url, None).await
    });
}

pub fn looks_like_auth_failure(err: &str) -> bool {
    let lower = err.to_lowercase();

    lower.contains("authentication failed")
        || lower.contains("access denied")
        || lower.contains("could not read username")
        || lower.contains("could not read password")
}

pub fn normalize_url(raw: &str) -> String {
    if let Ok(parsed) = url::Url::parse(raw) {
        let scheme = parsed.scheme();
        let host = parsed.host_str().unwrap_or("");

        match parsed.port() {
            Some(p) => format!("{scheme}://{host}:{p}"),
            None => format!("{scheme}://{host}"),
        }
    } else {
        raw.to_string()
    }
}
