//! Currency and cryptocurrency support for calcpad-engine.
//!
//! This module provides:
//! - A `CurrencyProvider` trait for pluggable rate sources (online/offline)
//! - Fiat currency rates (180+ currencies via Open Exchange Rates / exchangerate.host)
//! - Cryptocurrency rates (60+ coins via CoinGecko API structure)
//! - Symbol/code recognition ($, EUR, "dollars" -> canonical ISO codes)
//! - Rate caching with staleness detection and offline fallback

pub mod crypto;
pub mod fiat;
pub mod rates;
pub mod symbols;

pub use crypto::{CryptoProvider, CryptoProviderConfig, CryptoRate};
#[cfg(feature = "network")]
pub use fiat::{ExchangeRateHostProvider, OpenExchangeRatesProvider};
pub use fiat::{FiatCurrencyProvider, fallback_rates};
pub use rates::{ExchangeRateCache, ExchangeRates, ProviderConfig, RateMetadata, RateSource};
pub use symbols::{is_currency_code, is_crypto_symbol, resolve_currency};

use std::fmt;

/// Errors that can occur when fetching or loading exchange rates.
#[derive(Debug)]
pub enum CurrencyError {
    /// Network request failed.
    NetworkError(String),
    /// API returned an error or unexpected response format.
    ApiError(String),
    /// Cache file could not be read or written.
    CacheError(String),
    /// No rates available (no cache and provider unreachable).
    Unavailable(String),
}

impl fmt::Display for CurrencyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CurrencyError::NetworkError(msg) => write!(f, "Network error: {}", msg),
            CurrencyError::ApiError(msg) => write!(f, "API error: {}", msg),
            CurrencyError::CacheError(msg) => write!(f, "Cache error: {}", msg),
            CurrencyError::Unavailable(msg) => write!(f, "Rates unavailable: {}", msg),
        }
    }
}

impl std::error::Error for CurrencyError {}

/// Trait for exchange rate data sources.
///
/// Implement this trait to provide exchange rates from any source:
/// APIs, local files, hardcoded fallbacks, etc.
pub trait CurrencyProvider {
    /// Fetch current exchange rates (USD-based) from the provider.
    fn fetch_rates(&self) -> Result<ExchangeRates, CurrencyError>;

    /// The human-readable name of this provider.
    fn provider_name(&self) -> &str;
}

/// Result of getting rates: the rates themselves plus metadata about how they were obtained.
#[derive(Debug)]
pub struct RateResult {
    pub rates: ExchangeRates,
    pub metadata: RateMetadata,
}

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

    #[test]
    fn test_currency_error_display() {
        let err = CurrencyError::NetworkError("connection refused".into());
        assert_eq!(err.to_string(), "Network error: connection refused");

        let err = CurrencyError::ApiError("bad response".into());
        assert_eq!(err.to_string(), "API error: bad response");

        let err = CurrencyError::CacheError("disk full".into());
        assert_eq!(err.to_string(), "Cache error: disk full");

        let err = CurrencyError::Unavailable("no rates".into());
        assert_eq!(err.to_string(), "Rates unavailable: no rates");
    }

    #[test]
    fn test_currency_error_is_error_trait() {
        let err: Box<dyn std::error::Error> =
            Box::new(CurrencyError::Unavailable("test".into()));
        assert!(err.to_string().contains("test"));
    }
}
