use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

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

#[derive(Serialize)]
pub struct PublicCred {
    pub host: String,
    pub username: String,
    pub password: String,
}

fn store_path() -> Result<PathBuf> {
    let home = std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME not set"))?;
    let dir = PathBuf::from(home).join(".config").join("adom-chip-fetcher");
    std::fs::create_dir_all(&dir)?;
    Ok(dir.join("credentials.json"))
}

pub fn load() -> Result<Vec<Credential>> {
    let path = store_path()?;
    if !path.exists() {
        return Ok(Vec::new());
    }
    let body = std::fs::read_to_string(&path)?;
    let creds: Vec<Credential> = serde_json::from_str(&body).unwrap_or_default();
    Ok(creds)
}

fn save(creds: &[Credential]) -> Result<()> {
    let path = store_path()?;
    let body = serde_json::to_string_pretty(creds)?;
    std::fs::write(&path, body)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
    }
    Ok(())
}

pub fn list_public() -> Result<Vec<PublicCred>> {
    Ok(load()?
        .into_iter()
        .map(|c| PublicCred { host: c.host, username: c.username, password: c.password })
        .collect())
}

pub fn set(host: &str, username: &str, password: &str) -> Result<()> {
    let mut creds = load()?;
    creds.retain(|c| c.host != host);
    creds.push(Credential {
        host: host.to_string(),
        username: username.to_string(),
        password: password.to_string(),
    });
    save(&creds)
}

pub fn delete(host: &str) -> Result<bool> {
    let mut creds = load()?;
    let before = creds.len();
    creds.retain(|c| c.host != host);
    let removed = creds.len() != before;
    if removed { save(&creds)?; }
    Ok(removed)
}

/// Match a URL's hostname against stored host patterns. Glob `*` matches any subdomain.
/// Longest non-glob suffix wins.
pub fn match_url(url: &str) -> Result<Option<Credential>> {
    let creds = load()?;
    let host = match url_host(url) {
        Some(h) => h,
        None => return Ok(None),
    };
    let mut best: Option<(usize, Credential)> = None;
    for c in creds {
        if let Some(score) = match_score(&c.host, &host) {
            match &best {
                Some((bs, _)) if *bs >= score => {}
                _ => best = Some((score, c)),
            }
        }
    }
    Ok(best.map(|(_, c)| c))
}

fn url_host(url: &str) -> Option<String> {
    let after_scheme = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
    let host = after_scheme.split(['/', '?', '#']).next()?;
    let host = host.split('@').next_back()?;
    let host = host.split(':').next()?;
    Some(host.to_lowercase())
}

fn match_score(pattern: &str, host: &str) -> Option<usize> {
    let pat = pattern.to_lowercase();
    let h = host.to_lowercase();
    if let Some(suffix) = pat.strip_prefix("*.") {
        if h.ends_with(&format!(".{suffix}")) || h == suffix {
            return Some(suffix.len());
        }
        None
    } else if pat == h {
        Some(pat.len() * 2)
    } else {
        None
    }
}

/// Wrap a URL with basic-auth user:pass@ if a credential matches.
pub fn wrap_url_with_auth(url: &str) -> Result<String> {
    let cred = match match_url(url)? {
        Some(c) => c,
        None => return Ok(url.to_string()),
    };
    let after_scheme = match url.split_once("://") {
        Some(x) => x,
        None => return Ok(url.to_string()),
    };
    let scheme = after_scheme.0;
    let rest = after_scheme.1;
    if rest.contains('@') {
        return Ok(url.to_string());
    }
    let user = url_encode(&cred.username);
    let pass = url_encode(&cred.password);
    Ok(format!("{scheme}://{user}:{pass}@{rest}"))
}

fn url_encode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => out.push(b as char),
            _ => out.push_str(&format!("%{:02X}", b)),
        }
    }
    out
}