use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use futures::future::BoxFuture;

use crate::terminal::Company;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SecurityKind {
    Equity,
    Fund,
    Other(String),
}

impl SecurityKind {
    #[must_use]
    pub(crate) const fn is_supported(&self) -> bool {
        matches!(self, Self::Equity | Self::Fund)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SecurityMatch {
    pub symbol: String,
    pub name: Option<String>,
    pub exchange: Option<String>,
    pub kind: SecurityKind,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SecurityLookupError {
    SearchUnavailable { query: String, detail: String },
    DetailUnavailable { symbol: String, detail: String },
}

pub(crate) trait SecurityLookup: Send + Sync {
    fn provider_name(&self) -> &'static str;

    fn search<'a>(
        &'a self,
        query: &'a str,
    ) -> BoxFuture<'a, Result<Vec<SecurityMatch>, SecurityLookupError>>;

    fn load_company<'a>(
        &'a self,
        security_match: &'a SecurityMatch,
    ) -> BoxFuture<'a, Result<Company, SecurityLookupError>>;
}

#[derive(Debug, Clone)]
pub(crate) struct CacheEntry<T> {
    cached_at: Instant,
    value: T,
}

impl<T> CacheEntry<T> {
    #[must_use]
    pub(crate) fn new(value: T) -> Self {
        Self {
            cached_at: Instant::now(),
            value,
        }
    }

    #[must_use]
    pub(crate) fn is_fresh(&self, ttl: Duration) -> bool {
        self.cached_at.elapsed() <= ttl
    }

    #[must_use]
    pub(crate) fn is_within(&self, max_age: Duration) -> bool {
        self.cached_at.elapsed() <= max_age
    }

    #[must_use]
    pub(crate) fn cloned_value(&self) -> T
    where
        T: Clone,
    {
        self.value.clone()
    }
}

pub(crate) fn get_cached_value<T: Clone>(
    cache: &Mutex<HashMap<String, CacheEntry<T>>>,
    key: &str,
    ttl: Duration,
) -> Option<T> {
    let mut guard = cache.lock().ok()?;

    match guard.get(key) {
        Some(entry) if entry.is_fresh(ttl) => Some(entry.value.clone()),
        Some(_) => {
            guard.remove(key);
            None
        }
        None => None,
    }
}

pub(crate) fn get_cached_value_within<T: Clone>(
    cache: &Mutex<HashMap<String, CacheEntry<T>>>,
    key: &str,
    max_age: Duration,
) -> Option<T> {
    let guard = cache.lock().ok()?;
    guard
        .get(key)
        .filter(|entry| entry.is_within(max_age))
        .map(|entry| entry.value.clone())
}

pub(crate) fn store_cached_value<T>(
    cache: &Mutex<HashMap<String, CacheEntry<T>>>,
    key: String,
    value: T,
) {
    if let Ok(mut guard) = cache.lock() {
        guard.insert(key, CacheEntry::new(value));
    }
}

#[must_use]
pub(crate) fn normalize_search_query(query: &str) -> String {
    query.trim().to_ascii_lowercase()
}

#[must_use]
pub(crate) fn normalize_symbol(symbol: &str) -> String {
    symbol.trim().to_ascii_uppercase()
}

#[derive(Debug)]
pub(crate) struct RequestGate {
    next_allowed_at: Instant,
    min_spacing: Duration,
    min_jitter: Duration,
    max_jitter: Duration,
}

impl RequestGate {
    #[must_use]
    pub(crate) fn new(min_spacing: Duration, min_jitter: Duration, max_jitter: Duration) -> Self {
        Self {
            next_allowed_at: Instant::now(),
            min_spacing,
            min_jitter,
            max_jitter,
        }
    }

    #[must_use]
    pub(crate) fn reserve_slot(&mut self) -> Duration {
        let now = Instant::now();
        let scheduled_at = self.next_allowed_at.max(now);
        self.next_allowed_at = scheduled_at + self.min_spacing + self.jitter();
        scheduled_at.saturating_duration_since(now)
    }

    pub(crate) fn extend_cooldown(&mut self, cooldown: Duration) {
        self.next_allowed_at = self
            .next_allowed_at
            .max(Instant::now() + cooldown + self.jitter());
    }

    #[must_use]
    fn jitter(&self) -> Duration {
        if self.max_jitter <= self.min_jitter {
            return self.min_jitter;
        }

        let span_ms = self.max_jitter.saturating_sub(self.min_jitter).as_millis() as u64;
        if span_ms == 0 {
            return self.min_jitter;
        }

        let seed = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|duration| duration.as_nanos() as u64)
            .unwrap_or(0);

        self.min_jitter + Duration::from_millis(seed % (span_ms + 1))
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Mutex;
    use std::time::{Duration, Instant};

    use super::{
        get_cached_value, get_cached_value_within, normalize_search_query, normalize_symbol,
        store_cached_value, CacheEntry, RequestGate,
    };

    #[test]
    fn normalizes_cache_keys() {
        assert_eq!(normalize_search_query("  MicRoSoft  "), "microsoft");
        assert_eq!(normalize_symbol("  msft "), "MSFT");
    }

    #[test]
    fn returns_stale_entries_within_extended_window() {
        let cache = Mutex::new(HashMap::from([(
            "msft".to_string(),
            CacheEntry {
                cached_at: Instant::now() - Duration::from_secs(120),
                value: "cached".to_string(),
            },
        )]));

        assert_eq!(
            get_cached_value_within(&cache, "msft", Duration::from_secs(180)),
            Some("cached".to_string())
        );
        assert_eq!(
            get_cached_value(&cache, "msft", Duration::from_secs(60)),
            None
        );
    }

    #[test]
    fn stores_and_returns_fresh_entries() {
        let cache = Mutex::new(HashMap::new());
        store_cached_value(&cache, "msft".to_string(), "fresh".to_string());

        assert_eq!(
            get_cached_value(&cache, "msft", Duration::from_secs(60)),
            Some("fresh".to_string())
        );
    }

    #[test]
    fn cooldown_pushes_request_gate_forward() {
        let mut gate = RequestGate::new(
            Duration::from_millis(1_500),
            Duration::from_millis(0),
            Duration::from_millis(0),
        );

        assert!(gate.reserve_slot() <= Duration::from_millis(50));
        gate.extend_cooldown(Duration::from_secs(1));
        assert!(gate.reserve_slot() >= Duration::from_millis(900));
    }
}
