//! AINS resolver — looks up an `.aint` handle's authoritative Ed25519
//! verifying key.
//!
//! Used by [`crate::validator`] Rule 4 (§8.4 claim provenance) so that a
//! sealed iddrop response can be confirmed against the AInternet Name
//! Service registry. Two implementations:
//!
//! - [`HttpAinsResolver`] — live HTTP client (default endpoint =
//!   `https://brein.jaspervandemeent.nl`). Requires the `wire` feature.
//! - [`StubAinsResolver`] — in-memory map for tests and offline demos.

use crate::errors::{IddropError, Result};
use std::collections::HashMap;

/// Resolves an `.aint` handle to its authoritative Ed25519 verifying key
/// (lowercase hex, 64 chars).
pub trait AinsResolver {
    /// Resolve `aint` → `verifying_key_hex`. Returns `Ok(None)` when the
    /// registry has no record; `Err(...)` for transport or parse failure.
    fn resolve(&self, aint: &str) -> Result<Option<String>>;
}

/// Map-backed resolver for unit tests + offline demos.
#[derive(Default, Debug, Clone)]
pub struct StubAinsResolver {
    table: HashMap<String, String>,
}

impl StubAinsResolver {
    /// New empty stub.
    pub fn new() -> Self {
        Self::default()
    }

    /// Pre-populate with one or more handles. Keys are lowercased + the
    /// trailing `.aint` stripped, matching the brain-api normalisation.
    pub fn with_entry(mut self, aint: impl Into<String>, pubkey_hex: impl Into<String>) -> Self {
        self.table.insert(canonical(&aint.into()), pubkey_hex.into().to_lowercase());
        self
    }
}

impl AinsResolver for StubAinsResolver {
    fn resolve(&self, aint: &str) -> Result<Option<String>> {
        Ok(self.table.get(&canonical(aint)).cloned())
    }
}

/// HTTP resolver against the AInternet brain-api.
///
/// Calls `GET {base_url}/api/ains/resolve/{aint}` and expects a JSON
/// response containing either `verifying_key` or a top-level
/// `pubkey` / `ed25519` field.
#[cfg(feature = "wire")]
pub struct HttpAinsResolver {
    /// Base URL, e.g. `"https://brein.jaspervandemeent.nl"`.
    pub base_url: String,
}

#[cfg(feature = "wire")]
impl HttpAinsResolver {
    /// New resolver against the given base URL.
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
        }
    }
}

#[cfg(feature = "wire")]
impl AinsResolver for HttpAinsResolver {
    fn resolve(&self, aint: &str) -> Result<Option<String>> {
        let canon = canonical(aint);

        // Prefer the dedicated identity endpoint (= public, narrow surface).
        let identity_url = format!("{}/api/ains/identity/{}", self.base_url, canon);
        match ureq::get(&identity_url).call() {
            Ok(resp) => {
                let body: serde_json::Value = resp
                    .into_json()
                    .map_err(|e| IddropError::Transport(format!("identity parse: {}", e)))?;
                if let Some(vk) = body.get("verifying_key").and_then(|v| v.as_str()) {
                    return Ok(Some(normalize_pubkey(vk)));
                }
                if body.get("status").and_then(|s| s.as_str()) == Some("not_found") {
                    // Fall through to /resolve in case the broader endpoint knows.
                }
            }
            Err(ureq::Error::Status(404, _)) => {
                // Old brain-api without /identity endpoint — fall through.
            }
            Err(e) => {
                return Err(IddropError::Transport(format!(
                    "AINS identity HTTP {}: {}",
                    identity_url, e
                )))
            }
        }

        // Fallback: legacy /resolve shape.
        let url = format!("{}/api/ains/resolve/{}", self.base_url, canon);
        let resp = match ureq::get(&url).call() {
            Ok(r) => r,
            Err(ureq::Error::Status(404, _)) => return Ok(None),
            Err(e) => {
                return Err(IddropError::Transport(format!(
                    "AINS resolve HTTP {}: {}",
                    url, e
                )))
            }
        };
        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| IddropError::Transport(format!("AINS resolve parse: {}", e)))?;
        let record = body.get("record");
        let pk = record
            .and_then(|r| r.get("public_key"))
            .or_else(|| record.and_then(|r| r.get("verifying_key")))
            .or_else(|| record.and_then(|r| r.get("ed25519")))
            .or_else(|| body.get("verifying_key"))
            .or_else(|| body.get("pubkey"))
            .or_else(|| body.get("ed25519"))
            .and_then(|v| v.as_str())
            .map(normalize_pubkey);
        Ok(pk)
    }
}

/// Normalise a verifying-key string to lowercase hex.
///
/// AInternet AINS records carry public keys in either hex (`64` chars)
/// or base64 (`44` chars with `=` padding) form, depending on when and
/// how the handle was registered. iddrop callers treat the key as an
/// opaque comparator, so we settle on lowercase hex to make later
/// equality checks straightforward.
#[cfg(feature = "wire")]
fn normalize_pubkey(s: &str) -> String {
    let trimmed = s.trim();
    let lower = trimmed.to_lowercase();
    // Hex if 64 chars and all hex digits.
    if lower.len() == 64 && lower.chars().all(|c| c.is_ascii_hexdigit()) {
        return lower;
    }
    // Try base64 → 32-byte key → hex.
    use base64::Engine as _;
    let engines = [
        base64::engine::general_purpose::STANDARD,
        base64::engine::general_purpose::URL_SAFE,
        base64::engine::general_purpose::STANDARD_NO_PAD,
        base64::engine::general_purpose::URL_SAFE_NO_PAD,
    ];
    for eng in engines.iter() {
        if let Ok(bytes) = eng.decode(trimmed) {
            if bytes.len() == 32 {
                return hex::encode(bytes);
            }
        }
    }
    // Leave unchanged (= caller may surface a clearer error downstream).
    lower
}

#[cfg(not(feature = "wire"))]
fn normalize_pubkey(s: &str) -> String {
    s.trim().to_lowercase()
}

/// Normalise an `.aint` handle to lowercase without the trailing `.aint`.
pub fn canonical(aint: &str) -> String {
    let s = aint.trim().to_lowercase();
    s.strip_suffix(".aint").map(str::to_string).unwrap_or(s)
}

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

    #[test]
    fn canonical_strips_aint_and_lowercases() {
        assert_eq!(canonical("Jasper.AINT"), "jasper");
        assert_eq!(canonical("jasper"), "jasper");
        assert_eq!(canonical("  Storm.aint "), "storm");
    }

    #[test]
    fn stub_resolver_hits_and_misses() {
        let r = StubAinsResolver::new()
            .with_entry("jasper.aint", "AABB".to_string() + &"c".repeat(60));
        assert!(r.resolve("jasper").unwrap().is_some());
        assert!(r.resolve("jasper.aint").unwrap().is_some());
        assert!(r.resolve("Jasper.AINT").unwrap().is_some());
        assert!(r.resolve("nobody").unwrap().is_none());
    }

    #[test]
    fn stub_resolver_returns_lowercased_hex() {
        let r = StubAinsResolver::new().with_entry("a.aint", "ABCDEF");
        assert_eq!(r.resolve("a").unwrap(), Some("abcdef".to_string()));
    }
}
