//! Server-address prefetch cache and policy for Chanora.
//!
//! This crate owns speculative server-resolution warming. It does not
//! decide whether a connection should use a prefetched address; callers
//! must still apply their own trust boundary before dialing.

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};

use thiserror::Error;
use tokio::sync::Mutex;
use tracing::{debug, info, warn};

const SERVER_PREFETCH_TTL: Duration = Duration::from_secs(120);

#[derive(Debug, Error)]
pub enum ServerPrefetchError {
    #[error("resolver initialization failed: {0}")]
    ResolverInit(String),
    #[error("resolution failed: {0}")]
    Resolution(String),
    #[error("resolver returned invalid socket address '{resolved}': {reason}")]
    InvalidSocketAddress { resolved: String, reason: String },
}

#[derive(Debug, Clone)]
struct ServerPrefetchEntry {
    normalized_host: String,
    resolved_address: SocketAddr,
    completed_at: Instant,
}

#[derive(Debug, Default)]
struct ServerPrefetchCache {
    latest_generation: u64,
    entry: Option<ServerPrefetchEntry>,
}

impl ServerPrefetchCache {
    fn begin(&mut self, host: &str) -> u64 {
        if normalize_host(host).is_empty() {
            return self.latest_generation;
        }
        self.latest_generation = self.latest_generation.saturating_add(1);
        self.latest_generation
    }

    fn store_success(
        &mut self,
        generation: u64,
        host: &str,
        resolved_address: SocketAddr,
        completed_at: Instant,
    ) {
        if generation != self.latest_generation {
            return;
        }
        self.entry = Some(ServerPrefetchEntry {
            normalized_host: normalize_host(host),
            resolved_address,
            completed_at,
        });
    }

    fn fresh_match(&self, host: &str, now: Instant) -> Option<SocketAddr> {
        let normalized = normalize_host(host);
        let entry = self.entry.as_ref()?;
        if entry.normalized_host != normalized {
            return None;
        }
        if now.duration_since(entry.completed_at) > SERVER_PREFETCH_TTL {
            return None;
        }
        // Generation is not checked here: a fresh entry remains usable while
        // a newer prefetch is in flight. Stale async completions are still
        // rejected in store_success via the generation guard.
        Some(entry.resolved_address)
    }
}

#[derive(Debug, Clone, Default)]
pub struct ServerPrefetcher {
    cache: Arc<Mutex<ServerPrefetchCache>>,
    #[cfg(any(test, feature = "test-support"))]
    setup_error_for_test: Arc<Mutex<Option<String>>>,
}

impl ServerPrefetcher {
    pub fn new() -> Self {
        Self::default()
    }

    /// Schedules a fire-and-forget prefetch for `host`.
    ///
    /// The result only reports synchronous setup failures before scheduling,
    /// such as resolver initialization. DNS/resolution failures after the task
    /// is spawned are logged and do not complete this returned `Result`.
    pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError> {
        let normalized = normalize_host(&host);
        if normalized.is_empty() {
            return Ok(());
        }

        #[cfg(any(test, feature = "test-support"))]
        if let Some(err) = self.setup_error_for_test.lock().await.take() {
            return Err(ServerPrefetchError::ResolverInit(err));
        }

        let resolver = chanora_resolver::ChanoraResolver::new()
            .map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?;

        let generation = {
            let mut cache = self.cache.lock().await;
            cache.begin(&normalized)
        };
        let cache = self.cache.clone();
        tokio::spawn(async move {
            info!(target: "chanora_server_prefetch", host = %normalized, "resolution prefetch started");
            let result = resolve_socket(resolver, &normalized).await;
            match result {
                Ok(addr) => {
                    info!(
                        target: "chanora_server_prefetch",
                        host = %normalized,
                        resolved = %addr,
                        "resolution prefetch result"
                    );
                    let mut guard = cache.lock().await;
                    guard.store_success(generation, &normalized, addr, Instant::now());
                }
                Err(err) => {
                    warn!(
                        target: "chanora_server_prefetch",
                        host = %normalized,
                        error = %err,
                        "resolution prefetch failed"
                    );
                }
            }
        });
        Ok(())
    }

    pub async fn fresh_match(&self, host: &str) -> Option<SocketAddr> {
        let resolved = {
            let cache = self.cache.lock().await;
            cache.fresh_match(host, Instant::now())
        };
        match resolved {
            Some(addr) => {
                info!(
                    target: "chanora_server_prefetch",
                    host = %host,
                    resolved = %addr,
                    "connect using prefetched resolution"
                );
                Some(addr)
            }
            None => {
                debug!(target: "chanora_server_prefetch", host = %host, "connect prefetch miss or stale");
                None
            }
        }
    }

    #[cfg(any(test, feature = "test-support"))]
    pub async fn begin_for_test(&self, host: &str) -> u64 {
        let mut cache = self.cache.lock().await;
        cache.begin(host)
    }

    #[cfg(any(test, feature = "test-support"))]
    pub async fn store_success_for_test(
        &self,
        generation: u64,
        host: &str,
        resolved_address: SocketAddr,
        completed_at: Instant,
    ) {
        let mut cache = self.cache.lock().await;
        cache.store_success(generation, host, resolved_address, completed_at);
    }

    #[cfg(any(test, feature = "test-support"))]
    pub async fn latest_generation_for_test(&self) -> u64 {
        let cache = self.cache.lock().await;
        cache.latest_generation
    }

    #[cfg(any(test, feature = "test-support"))]
    pub async fn fail_next_prefetch_setup_for_test(&self, error: impl Into<String>) {
        *self.setup_error_for_test.lock().await = Some(error.into());
    }
}

fn normalize_host(host: &str) -> String {
    host.trim().to_lowercase()
}

async fn resolve_socket(
    resolver: chanora_resolver::ChanoraResolver,
    host: &str,
) -> Result<SocketAddr, ServerPrefetchError> {
    let resolved = resolver
        .resolve_client_address(host)
        .await
        .map_err(|err| ServerPrefetchError::Resolution(err.to_string()))?;
    resolved
        .parse::<SocketAddr>()
        .map_err(|err| ServerPrefetchError::InvalidSocketAddress {
            resolved,
            reason: err.to_string(),
        })
}

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

    #[tokio::test]
    async fn fresh_exact_match_returns_socket_address() {
        let prefetcher = ServerPrefetcher::new();
        let generation = prefetcher.begin_for_test(" Example.COM ").await;
        let addr = "127.0.0.1:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(generation, "example.com", addr, Instant::now())
            .await;

        assert_eq!(prefetcher.fresh_match("example.com").await, Some(addr));
        assert_eq!(prefetcher.fresh_match(" EXAMPLE.com ").await, Some(addr));
    }

    #[tokio::test]
    async fn stale_entries_are_ignored() {
        let prefetcher = ServerPrefetcher::new();
        let generation = prefetcher.begin_for_test("example.com").await;
        let addr = "127.0.0.1:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(
                generation,
                "example.com",
                addr,
                Instant::now() - SERVER_PREFETCH_TTL - Duration::from_secs(1),
            )
            .await;

        assert_eq!(prefetcher.fresh_match("example.com").await, None);
    }

    #[tokio::test]
    async fn different_hosts_are_ignored() {
        let prefetcher = ServerPrefetcher::new();
        let generation = prefetcher.begin_for_test("example.com").await;
        let addr = "127.0.0.1:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(generation, "example.com", addr, Instant::now())
            .await;

        assert_eq!(prefetcher.fresh_match("other.example.com").await, None);
    }

    #[tokio::test]
    async fn stale_generation_completions_are_ignored() {
        let prefetcher = ServerPrefetcher::new();
        let old_generation = prefetcher.begin_for_test("old.example.com").await;
        let _new_generation = prefetcher.begin_for_test("new.example.com").await;
        let old_addr = "127.0.0.1:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(old_generation, "old.example.com", old_addr, Instant::now())
            .await;

        assert_eq!(prefetcher.fresh_match("old.example.com").await, None);
    }

    #[tokio::test]
    async fn fresh_entry_usable_during_warming() {
        let prefetcher = ServerPrefetcher::new();
        let first_generation = prefetcher.begin_for_test("example.com").await;
        let cached_addr = "127.0.0.1:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(first_generation, "example.com", cached_addr, Instant::now())
            .await;

        let _pending_generation = prefetcher.begin_for_test("example.com").await;

        assert_eq!(
            prefetcher.fresh_match("example.com").await,
            Some(cached_addr)
        );

        let stale_addr = "127.0.0.2:9987".parse().unwrap();
        prefetcher
            .store_success_for_test(first_generation, "example.com", stale_addr, Instant::now())
            .await;

        assert_eq!(
            prefetcher.fresh_match("example.com").await,
            Some(cached_addr)
        );
    }

    #[tokio::test]
    async fn blank_host_skips_prefetch() {
        let prefetcher = ServerPrefetcher::new();
        let before = prefetcher.latest_generation_for_test().await;

        prefetcher.prefetch("   ".to_string()).await.unwrap();

        assert_eq!(prefetcher.latest_generation_for_test().await, before);
    }
}
