//! On-demand TLS — provision certificates at handshake time.
//!
//! When a client connects with SNI for a domain not in the cert store,
//! the on-demand provisioner validates the domain and triggers ACME
//! provisioning inline. A temporary self-signed cert is used as fallback
//! while provisioning is in flight.

use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Tracks domains currently being provisioned to avoid duplicate ACME requests.
pub struct OnDemandProvisioner {
    /// Domains currently being provisioned (dedup).
    in_flight: DashMap<String, ()>,
    /// Domain validation callback — return true to allow provisioning.
    /// Prevents abuse (random domains triggering ACME).
    validator: Box<dyn Fn(&str) -> bool + Send + Sync>,
    /// ACME contact email for cert provisioning.
    acme_email: Option<String>,
    /// Maximum number of certificates to provision (prevent abuse).
    max_certs: usize,
    /// Current provisioned count.
    cert_count: AtomicUsize,
}

impl OnDemandProvisioner {
    /// Create a new on-demand provisioner.
    ///
    /// `max_certs` limits the total number of certificates that can be
    /// provisioned on-demand. `acme_email` is the contact email for
    /// ACME account registration.
    ///
    /// By default, **no** domains are allowed. Use [`with_validator`] to
    /// configure which domains can trigger provisioning.
    ///
    /// Security: the default is deny-all to prevent an attacker from
    /// triggering certificate issuance for arbitrary domains.
    pub fn new(max_certs: usize, acme_email: Option<String>) -> Self {
        Self {
            in_flight: DashMap::new(),
            validator: Box::new(|_| false), // deny-all by default
            acme_email,
            max_certs,
            cert_count: AtomicUsize::new(0),
        }
    }

    /// Set a domain validator function. Called before ACME provisioning.
    /// Return true to allow, false to reject.
    pub fn with_validator(mut self, f: impl Fn(&str) -> bool + Send + Sync + 'static) -> Self {
        self.validator = Box::new(f);
        self
    }

    /// Check if a domain should be provisioned on-demand.
    ///
    /// Returns false if:
    /// - Domain fails validation
    /// - Already at max_certs limit
    /// - Domain is already being provisioned
    pub fn should_provision(&self, domain: &str) -> bool {
        // Check validator first.
        if !(self.validator)(domain) {
            tracing::debug!(domain, "on-demand: domain rejected by validator");
            return false;
        }

        // Check max_certs limit.
        if self.cert_count.load(Ordering::Relaxed) >= self.max_certs {
            tracing::warn!(
                domain,
                max = self.max_certs,
                "on-demand: max certs limit reached"
            );
            return false;
        }

        // Check if already in flight.
        if self.in_flight.contains_key(domain) {
            tracing::debug!(domain, "on-demand: already being provisioned");
            return false;
        }

        true
    }

    /// Mark a domain as being provisioned (dedup).
    ///
    /// Returns `true` if the domain was successfully marked (i.e., no
    /// other task is already provisioning it). Returns `false` if
    /// another task already started provisioning this domain.
    pub fn start_provisioning(&self, domain: &str) -> bool {
        // Use DashMap's atomic entry() API to avoid TOCTOU race between
        // contains_key() and insert().
        match self.in_flight.entry(domain.to_string()) {
            Entry::Occupied(_) => false,
            Entry::Vacant(v) => {
                v.insert(());
                true
            }
        }
    }

    /// Mark provisioning as complete and increment the cert count.
    pub fn finish_provisioning(&self, domain: &str) {
        self.in_flight.remove(domain);
        self.cert_count.fetch_add(1, Ordering::Relaxed);
        tracing::info!(domain, "on-demand: provisioning complete");
    }

    /// Provisioned cert count.
    pub fn cert_count(&self) -> usize {
        self.cert_count.load(Ordering::Relaxed)
    }

    /// Return the ACME contact email.
    pub fn acme_email(&self) -> Option<&str> {
        self.acme_email.as_deref()
    }

    /// Return the maximum number of certs allowed.
    pub fn max_certs(&self) -> usize {
        self.max_certs
    }

    /// Return the number of domains currently being provisioned.
    pub fn in_flight_count(&self) -> usize {
        self.in_flight.len()
    }
}

/// On-demand TLS configuration.
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct OnDemandConfig {
    /// Enable on-demand TLS provisioning.
    pub enabled: bool,
    /// Maximum certificates to provision.
    pub max_certs: usize,
    /// Allowed domain patterns (glob-style). Empty = allow all.
    pub allowed_domains: Vec<String>,
}

impl Default for OnDemandConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            max_certs: 100,
            allowed_domains: Vec::new(),
        }
    }
}

/// Check if a domain matches any of the allowed patterns.
///
/// Patterns support simple glob matching:
/// - `*` matches any sequence of characters within a label
/// - `*.example.com` matches `foo.example.com` but not `example.com`
///
/// If `patterns` is empty, all domains are allowed.
pub fn domain_matches_patterns(domain: &str, patterns: &[String]) -> bool {
    if patterns.is_empty() {
        return true;
    }
    let domain_lower = domain.to_lowercase();
    for pattern in patterns {
        let pat_lower = pattern.to_lowercase();
        if pattern_matches(&domain_lower, &pat_lower) {
            return true;
        }
    }
    false
}

/// Simple glob pattern matching for domain names.
fn pattern_matches(domain: &str, pattern: &str) -> bool {
    if pattern == "*" {
        return true;
    }

    // Handle *.suffix pattern (wildcard prefix).
    if let Some(suffix) = pattern.strip_prefix("*.") {
        // Domain must end with .suffix and have at least one label before it.
        if let Some(stripped) = domain.strip_suffix(suffix) {
            // The stripped part should end with '.' and have at least one char before it.
            return stripped.ends_with('.') && stripped.len() > 1;
        }
        return false;
    }

    // Exact match.
    domain == pattern
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use std::thread;

    #[test]
    fn new_provisioner_defaults() {
        let p = OnDemandProvisioner::new(50, Some("test@example.com".into()));
        assert_eq!(p.max_certs(), 50);
        assert_eq!(p.cert_count(), 0);
        assert_eq!(p.acme_email(), Some("test@example.com"));
        assert_eq!(p.in_flight_count(), 0);
    }

    #[test]
    fn new_provisioner_no_email() {
        let p = OnDemandProvisioner::new(100, None);
        assert!(p.acme_email().is_none());
    }

    #[test]
    fn should_provision_denies_by_default() {
        // Default validator is deny-all for security.
        let p = OnDemandProvisioner::new(100, None);
        assert!(!p.should_provision("example.com"));
        assert!(!p.should_provision("anything.test"));
    }

    #[test]
    fn should_provision_allows_with_validator() {
        let p = OnDemandProvisioner::new(100, None)
            .with_validator(|_| true);
        assert!(p.should_provision("example.com"));
        assert!(p.should_provision("anything.test"));
    }

    #[test]
    fn should_provision_rejects_when_validator_returns_false() {
        let p = OnDemandProvisioner::new(100, None)
            .with_validator(|domain| domain.ends_with(".allowed.com"));

        assert!(p.should_provision("app.allowed.com"));
        assert!(!p.should_provision("evil.attacker.com"));
    }

    #[test]
    fn should_provision_rejects_at_max_certs() {
        let p = OnDemandProvisioner::new(2, None);

        // Simulate having already provisioned 2 certs.
        p.cert_count.store(2, Ordering::Relaxed);

        assert!(!p.should_provision("new-domain.com"));
    }

    #[test]
    fn should_provision_rejects_in_flight() {
        let p = OnDemandProvisioner::new(100, None)
            .with_validator(|_| true); // allow-all for this test

        p.start_provisioning("busy.com");
        assert!(!p.should_provision("busy.com"));

        // Other domains still allowed.
        assert!(p.should_provision("free.com"));
    }

    #[test]
    fn start_provisioning_dedup() {
        let p = OnDemandProvisioner::new(100, None);

        // First call succeeds.
        assert!(p.start_provisioning("test.com"));
        assert_eq!(p.in_flight_count(), 1);

        // Second call for same domain returns false.
        assert!(!p.start_provisioning("test.com"));
        assert_eq!(p.in_flight_count(), 1);
    }

    #[test]
    fn finish_provisioning_increments_count() {
        let p = OnDemandProvisioner::new(100, None);

        p.start_provisioning("test.com");
        assert_eq!(p.cert_count(), 0);
        assert_eq!(p.in_flight_count(), 1);

        p.finish_provisioning("test.com");
        assert_eq!(p.cert_count(), 1);
        assert_eq!(p.in_flight_count(), 0);
    }

    #[test]
    fn finish_provisioning_allows_reprovisioning() {
        let p = OnDemandProvisioner::new(100, None);

        p.start_provisioning("test.com");
        p.finish_provisioning("test.com");

        // Can start again for the same domain (e.g., renewal).
        assert!(p.start_provisioning("test.com"));
    }

    #[test]
    fn concurrent_start_provisioning() {
        let p = Arc::new(OnDemandProvisioner::new(100, None));
        let successes = Arc::new(AtomicUsize::new(0));

        let mut handles = Vec::new();
        for _ in 0..10 {
            let p = Arc::clone(&p);
            let successes = Arc::clone(&successes);
            handles.push(thread::spawn(move || {
                if p.start_provisioning("race.com") {
                    successes.fetch_add(1, Ordering::Relaxed);
                }
            }));
        }

        for h in handles {
            h.join().unwrap();
        }

        // Exactly one thread should have won the race.
        // With DashMap's atomic entry() API, only one thread can insert
        // for a given key — the rest see Entry::Occupied and return false.
        let count = successes.load(Ordering::Relaxed);
        assert_eq!(count, 1, "exactly one thread should win the race");
    }

    #[test]
    fn multiple_domains_provisioned() {
        let p = OnDemandProvisioner::new(100, None);

        assert!(p.start_provisioning("a.com"));
        assert!(p.start_provisioning("b.com"));
        assert!(p.start_provisioning("c.com"));
        assert_eq!(p.in_flight_count(), 3);

        p.finish_provisioning("a.com");
        p.finish_provisioning("b.com");
        p.finish_provisioning("c.com");
        assert_eq!(p.cert_count(), 3);
        assert_eq!(p.in_flight_count(), 0);
    }

    #[test]
    fn max_certs_boundary() {
        let p = OnDemandProvisioner::new(3, None);

        // Provision exactly max_certs.
        for i in 0..3 {
            let domain = format!("d{i}.com");
            assert!(
                p.start_provisioning(&domain),
                "domain {domain} should be allowed"
            );
            p.finish_provisioning(&domain);
        }

        assert_eq!(p.cert_count(), 3);

        // Next one should be rejected.
        assert!(!p.should_provision("d3.com"));
    }

    #[test]
    fn validator_with_closure_state() {
        let allowed: Vec<String> = vec!["ok.com".into(), "yes.com".into()];
        let p = OnDemandProvisioner::new(100, None)
            .with_validator(move |domain| allowed.iter().any(|a| a == domain));

        assert!(p.should_provision("ok.com"));
        assert!(p.should_provision("yes.com"));
        assert!(!p.should_provision("no.com"));
    }

    // --- OnDemandConfig tests ---

    #[test]
    fn on_demand_config_default() {
        let cfg = OnDemandConfig::default();
        assert!(!cfg.enabled);
        assert_eq!(cfg.max_certs, 100);
        assert!(cfg.allowed_domains.is_empty());
    }

    #[test]
    fn on_demand_config_deserialize() {
        let toml_str = r#"
enabled = true
max_certs = 50
allowed_domains = ["*.example.com", "specific.com"]
"#;
        let cfg: OnDemandConfig = toml::from_str(toml_str).unwrap();
        assert!(cfg.enabled);
        assert_eq!(cfg.max_certs, 50);
        assert_eq!(cfg.allowed_domains.len(), 2);
    }

    // --- domain_matches_patterns tests ---

    #[test]
    fn empty_patterns_allows_all() {
        assert!(domain_matches_patterns("anything.com", &[]));
    }

    #[test]
    fn exact_pattern_match() {
        let patterns = vec!["example.com".into()];
        assert!(domain_matches_patterns("example.com", &patterns));
        assert!(!domain_matches_patterns("other.com", &patterns));
    }

    #[test]
    fn wildcard_pattern_match() {
        let patterns = vec!["*.example.com".into()];
        assert!(domain_matches_patterns("app.example.com", &patterns));
        assert!(domain_matches_patterns("api.example.com", &patterns));
        assert!(!domain_matches_patterns("example.com", &patterns));
    }

    #[test]
    fn wildcard_star_alone_matches_all() {
        let patterns = vec!["*".into()];
        assert!(domain_matches_patterns("anything.com", &patterns));
        assert!(domain_matches_patterns("x.y.z", &patterns));
    }

    #[test]
    fn pattern_case_insensitive() {
        let patterns = vec!["*.Example.COM".into()];
        assert!(domain_matches_patterns("APP.EXAMPLE.COM", &patterns));
        assert!(domain_matches_patterns("app.example.com", &patterns));
    }

    #[test]
    fn multiple_patterns_any_match() {
        let patterns = vec!["*.example.com".into(), "specific.org".into()];
        assert!(domain_matches_patterns("app.example.com", &patterns));
        assert!(domain_matches_patterns("specific.org", &patterns));
        assert!(!domain_matches_patterns("other.org", &patterns));
    }
}
