//! Price Feed Sources
//!
//! Trait and implementations for fetching prices from various sources.

use crate::error::{EconomicsError, Result};
use crate::oracle::TokenPrice;
use crate::SynorDecimal;
use async_trait::async_trait;
use chrono::Utc;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

/// Price source identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceSource {
    /// Internal DEX prices
    Internal,
    /// Synor native DEX
    SynorDex,
    /// Chainlink oracle
    Chainlink,
    /// Pyth network
    Pyth,
    /// Band protocol
    Band,
    /// CoinGecko API
    CoinGecko,
    /// CoinMarketCap API
    CoinMarketCap,
    /// Binance exchange
    Binance,
    /// Uniswap TWAP
    Uniswap,
    /// Cross-chain via IBC
    CrossChain,
    /// Aggregated from multiple sources
    Aggregated,
    /// Custom/external source
    Custom(u8),
}

impl std::fmt::Display for PriceSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PriceSource::Internal => write!(f, "internal"),
            PriceSource::SynorDex => write!(f, "synor_dex"),
            PriceSource::Chainlink => write!(f, "chainlink"),
            PriceSource::Pyth => write!(f, "pyth"),
            PriceSource::Band => write!(f, "band"),
            PriceSource::CoinGecko => write!(f, "coingecko"),
            PriceSource::CoinMarketCap => write!(f, "coinmarketcap"),
            PriceSource::Binance => write!(f, "binance"),
            PriceSource::Uniswap => write!(f, "uniswap"),
            PriceSource::CrossChain => write!(f, "cross_chain"),
            PriceSource::Aggregated => write!(f, "aggregated"),
            PriceSource::Custom(id) => write!(f, "custom_{}", id),
        }
    }
}

/// Trait for price feed implementations
#[async_trait]
pub trait PriceFeed: Send + Sync {
    /// Get the source identifier
    fn source(&self) -> PriceSource;

    /// Fetch current price for a token pair
    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice>;

    /// Check if the feed is healthy
    async fn health_check(&self) -> bool {
        true
    }

    /// Get supported pairs
    fn supported_pairs(&self) -> Vec<(String, String)> {
        vec![("SYNOR".to_string(), "USD".to_string())]
    }
}

/// Mock price feed for testing
pub struct MockPriceFeed {
    source: PriceSource,
    base_price: SynorDecimal,
    volatility: f64,
}

impl MockPriceFeed {
    pub fn new(source: PriceSource, base_price: SynorDecimal) -> Self {
        Self {
            source,
            base_price,
            volatility: 0.01,
        }
    }

    pub fn with_volatility(mut self, volatility: f64) -> Self {
        self.volatility = volatility;
        self
    }
}

#[async_trait]
impl PriceFeed for MockPriceFeed {
    fn source(&self) -> PriceSource {
        self.source
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        // Add small random variance
        let variance = (rand_simple() * 2.0 - 1.0) * self.volatility;
        let price = self.base_price * (Decimal::ONE + Decimal::from_f64_retain(variance).unwrap_or_default());

        Ok(TokenPrice {
            token: token.to_string(),
            quote: quote.to_string(),
            price,
            timestamp: Utc::now(),
            source: self.source,
            confidence: 0.95,
        })
    }
}

/// Simple random number generator (no external deps)
fn rand_simple() -> f64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .subsec_nanos();
    (nanos as f64 % 1000.0) / 1000.0
}

/// Internal DEX price feed
pub struct InternalDexFeed {
    dex_endpoint: String,
}

impl InternalDexFeed {
    pub fn new(endpoint: impl Into<String>) -> Self {
        Self {
            dex_endpoint: endpoint.into(),
        }
    }
}

#[async_trait]
impl PriceFeed for InternalDexFeed {
    fn source(&self) -> PriceSource {
        PriceSource::SynorDex
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        // In production, this would query the Synor DEX contract
        #[cfg(feature = "http-feeds")]
        {
            let url = format!(
                "{}/api/v1/price?token={}&quote={}",
                self.dex_endpoint, token, quote
            );

            let response = reqwest::get(&url)
                .await
                .map_err(|e| EconomicsError::PriceFeedUnavailable(e.to_string()))?;

            #[derive(Deserialize)]
            struct DexPriceResponse {
                price: String,
                timestamp: i64,
            }

            let data: DexPriceResponse = response
                .json()
                .await
                .map_err(|e| EconomicsError::InvalidPrice(e.to_string()))?;

            let price = data
                .price
                .parse::<Decimal>()
                .map_err(|e| EconomicsError::InvalidPrice(e.to_string()))?;

            Ok(TokenPrice {
                token: token.to_string(),
                quote: quote.to_string(),
                price,
                timestamp: chrono::DateTime::from_timestamp(data.timestamp, 0)
                    .unwrap_or_else(Utc::now),
                source: PriceSource::SynorDex,
                confidence: 0.99,
            })
        }

        #[cfg(not(feature = "http-feeds"))]
        {
            let _ = (token, quote);
            Err(EconomicsError::PriceFeedUnavailable(
                "HTTP feeds not enabled".to_string(),
            ))
        }
    }
}

/// Chainlink price feed adapter
pub struct ChainlinkFeed {
    contract_address: String,
    rpc_url: String,
}

impl ChainlinkFeed {
    pub fn new(contract_address: impl Into<String>, rpc_url: impl Into<String>) -> Self {
        Self {
            contract_address: contract_address.into(),
            rpc_url: rpc_url.into(),
        }
    }
}

#[async_trait]
impl PriceFeed for ChainlinkFeed {
    fn source(&self) -> PriceSource {
        PriceSource::Chainlink
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        // In production, this would call the Chainlink price feed contract
        // via eth_call to latestRoundData()
        let _ = (&self.contract_address, &self.rpc_url, token, quote);

        Err(EconomicsError::PriceFeedUnavailable(
            "Chainlink integration pending".to_string(),
        ))
    }
}

/// CoinGecko API price feed
pub struct CoinGeckoFeed {
    api_key: Option<String>,
}

impl CoinGeckoFeed {
    pub fn new(api_key: Option<String>) -> Self {
        Self { api_key }
    }
}

#[async_trait]
impl PriceFeed for CoinGeckoFeed {
    fn source(&self) -> PriceSource {
        PriceSource::CoinGecko
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        #[cfg(feature = "http-feeds")]
        {
            let token_id = match token.to_uppercase().as_str() {
                "SYNOR" => "synor", // Would need actual CoinGecko ID
                "BTC" => "bitcoin",
                "ETH" => "ethereum",
                _ => return Err(EconomicsError::PriceFeedUnavailable(
                    format!("Token {} not supported on CoinGecko", token)
                )),
            };

            let quote_currency = quote.to_lowercase();

            let url = format!(
                "https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies={}",
                token_id, quote_currency
            );

            let client = reqwest::Client::new();
            let mut request = client.get(&url);

            if let Some(ref key) = self.api_key {
                request = request.header("x-cg-demo-api-key", key);
            }

            let response = request
                .send()
                .await
                .map_err(|e| EconomicsError::PriceFeedUnavailable(e.to_string()))?;

            let data: serde_json::Value = response
                .json()
                .await
                .map_err(|e| EconomicsError::InvalidPrice(e.to_string()))?;

            let price = data[token_id][&quote_currency]
                .as_f64()
                .ok_or_else(|| EconomicsError::InvalidPrice("Price not found".to_string()))?;

            Ok(TokenPrice {
                token: token.to_string(),
                quote: quote.to_string(),
                price: Decimal::from_f64_retain(price)
                    .unwrap_or_default(),
                timestamp: Utc::now(),
                source: PriceSource::CoinGecko,
                confidence: 0.90,
            })
        }

        #[cfg(not(feature = "http-feeds"))]
        {
            let _ = (token, quote);
            Err(EconomicsError::PriceFeedUnavailable(
                "HTTP feeds not enabled".to_string(),
            ))
        }
    }

    fn supported_pairs(&self) -> Vec<(String, String)> {
        vec![
            ("SYNOR".to_string(), "USD".to_string()),
            ("BTC".to_string(), "USD".to_string()),
            ("ETH".to_string(), "USD".to_string()),
        ]
    }
}

/// Aggregate multiple price feeds
pub struct AggregateFeed {
    feeds: Vec<Box<dyn PriceFeed + Send + Sync>>,
}

impl AggregateFeed {
    pub fn new(feeds: Vec<Box<dyn PriceFeed + Send + Sync>>) -> Self {
        Self { feeds }
    }

    /// Fetch prices from all feeds and return median
    pub async fn fetch_median(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        let mut prices = Vec::new();

        for feed in &self.feeds {
            match feed.fetch_price(token, quote).await {
                Ok(price) => prices.push(price),
                Err(e) => {
                    tracing::warn!("Feed {} failed: {}", feed.source(), e);
                }
            }
        }

        if prices.is_empty() {
            return Err(EconomicsError::PriceFeedUnavailable(
                "All feeds failed".to_string(),
            ));
        }

        // Sort by price and take median
        prices.sort_by(|a, b| a.price.cmp(&b.price));
        let median_idx = prices.len() / 2;
        let mut median = prices.remove(median_idx);

        // Adjust confidence based on source count
        median.confidence *= prices.len() as f64 / self.feeds.len() as f64;

        Ok(median)
    }
}

#[async_trait]
impl PriceFeed for AggregateFeed {
    fn source(&self) -> PriceSource {
        PriceSource::Internal
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<TokenPrice> {
        self.fetch_median(token, quote).await
    }

    async fn health_check(&self) -> bool {
        let mut healthy_count = 0;
        for feed in &self.feeds {
            if feed.health_check().await {
                healthy_count += 1;
            }
        }
        healthy_count >= self.feeds.len() / 2
    }
}

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

    #[tokio::test]
    async fn test_mock_feed() {
        let feed = MockPriceFeed::new(PriceSource::Internal, dec!(1.50));
        let price = feed.fetch_price("SYNOR", "USD").await.unwrap();

        assert_eq!(price.token, "SYNOR");
        assert_eq!(price.quote, "USD");
        assert!(price.price > dec!(1.40) && price.price < dec!(1.60));
    }
}
