//! Multi-exchange abstraction for fetching OHLCV candles and price data.
//!
//! Supports Binance and CoinGecko with a unified interface.
//!
//! # Example
//!
//! ```no_run
//! use termichart_data::exchange::{Exchange, ExchangeClient};
//!
//! let client = ExchangeClient::binance();
//! let candles = client.fetch_candles("BTCUSDT", "1m", 60);
//! let price = client.fetch_spot_price("BTCUSDT");
//! ```

use termichart_core::{Candle, Point};

/// Supported exchanges.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Exchange {
    /// Binance public REST API (no API key required for market data).
    Binance,
    /// CoinGecko free API (no API key required).
    CoinGecko,
}

/// A client that fetches market data from a specific exchange.
///
/// All methods return `Option` and never panic; network or parse errors
/// are silently mapped to `None` so callers can retry or fall back.
pub struct ExchangeClient {
    exchange: Exchange,
}

impl ExchangeClient {
    /// Create a client for the given exchange.
    pub fn new(exchange: Exchange) -> Self {
        Self { exchange }
    }

    /// Convenience constructor for Binance.
    pub fn binance() -> Self {
        Self::new(Exchange::Binance)
    }

    /// Convenience constructor for CoinGecko.
    pub fn coingecko() -> Self {
        Self::new(Exchange::CoinGecko)
    }

    /// Fetch OHLCV kline candles.
    ///
    /// * **Binance** -- `symbol` is a trading pair like `"BTCUSDT"`, `interval`
    ///   is a Binance interval string (`"1m"`, `"5m"`, `"1h"`, `"1d"`, ...),
    ///   and `limit` is the number of candles (max 1000).
    ///
    /// * **CoinGecko** -- `symbol` is the coin *id* (e.g. `"bitcoin"`),
    ///   `interval` is interpreted as the number of days of history
    ///   (`"1"`, `"7"`, `"30"`, `"365"`), and `limit` is currently
    ///   unused (the API decides granularity from `days`).
    pub fn fetch_candles(&self, symbol: &str, interval: &str, limit: usize) -> Option<Vec<Candle>> {
        match self.exchange {
            Exchange::Binance => fetch_binance_klines(symbol, interval, limit),
            Exchange::CoinGecko => fetch_coingecko_ohlc(symbol, interval),
        }
    }

    /// Fetch close prices as [`Point`]s suitable for line charts.
    ///
    /// Each point has `x = timestamp` (seconds) and `y = close price`.
    ///
    /// * **Binance** -- uses klines; `x` is the candle open time.
    /// * **CoinGecko** -- uses the `market_chart` endpoint which returns
    ///   price samples at varying resolution depending on `days`.
    pub fn fetch_prices(&self, symbol: &str, interval: &str, limit: usize) -> Option<Vec<Point>> {
        match self.exchange {
            Exchange::Binance => {
                let candles = fetch_binance_klines(symbol, interval, limit)?;
                Some(
                    candles
                        .into_iter()
                        .map(|c| Point { x: c.time, y: c.close })
                        .collect(),
                )
            }
            Exchange::CoinGecko => fetch_coingecko_prices(symbol, interval),
        }
    }

    /// Fetch the current spot (last traded) price.
    ///
    /// * **Binance** -- uses `/api/v3/ticker/price`.
    /// * **CoinGecko** -- uses `/api/v3/simple/price`.
    pub fn fetch_spot_price(&self, symbol: &str) -> Option<f64> {
        match self.exchange {
            Exchange::Binance => fetch_binance_spot(symbol),
            Exchange::CoinGecko => fetch_coingecko_spot(symbol),
        }
    }
}

// ---------------------------------------------------------------------------
// Binance helpers
// ---------------------------------------------------------------------------

/// Fetch kline candles from Binance.
///
/// Kline JSON array element:
/// `[open_time_ms, open, high, low, close, volume, close_time_ms, ...]`
fn fetch_binance_klines(symbol: &str, interval: &str, limit: usize) -> Option<Vec<Candle>> {
    let url = format!(
        "https://api.binance.com/api/v3/klines?symbol={}&interval={}&limit={}",
        symbol, interval, limit,
    );
    let resp = ureq::get(&url).call().ok()?;
    let body: serde_json::Value = resp.into_json().ok()?;
    let arr = body.as_array()?;

    let mut candles = Vec::with_capacity(arr.len());
    for kline in arr {
        let k = kline.as_array()?;
        let time = k[0].as_f64()? / 1000.0; // ms -> seconds
        let open = k[1].as_str()?.parse::<f64>().ok()?;
        let high = k[2].as_str()?.parse::<f64>().ok()?;
        let low = k[3].as_str()?.parse::<f64>().ok()?;
        let close = k[4].as_str()?.parse::<f64>().ok()?;
        let volume = k[5].as_str()?.parse::<f64>().ok()?;
        candles.push(Candle {
            time,
            open,
            high,
            low,
            close,
            volume,
        });
    }
    Some(candles)
}

/// Fetch the latest traded price from Binance.
///
/// Response: `{"symbol":"BTCUSDT","price":"12345.67"}`
fn fetch_binance_spot(symbol: &str) -> Option<f64> {
    let url = format!(
        "https://api.binance.com/api/v3/ticker/price?symbol={}",
        symbol,
    );
    let resp = ureq::get(&url).call().ok()?;
    let body: serde_json::Value = resp.into_json().ok()?;
    body["price"].as_str()?.parse::<f64>().ok()
}

// ---------------------------------------------------------------------------
// CoinGecko helpers
// ---------------------------------------------------------------------------

/// Fetch OHLC candles from CoinGecko.
///
/// The `interval` parameter is interpreted as the number of days.
/// Response: `[[timestamp_ms, open, high, low, close], ...]`
fn fetch_coingecko_ohlc(coin_id: &str, days: &str) -> Option<Vec<Candle>> {
    let url = format!(
        "https://api.coingecko.com/api/v3/coins/{}/ohlc?vs_currency=usd&days={}",
        coin_id, days,
    );
    let resp = ureq::get(&url).call().ok()?;
    let body: serde_json::Value = resp.into_json().ok()?;
    let arr = body.as_array()?;

    let mut candles = Vec::with_capacity(arr.len());
    for item in arr {
        let row = item.as_array()?;
        let time = row[0].as_f64()? / 1000.0; // ms -> seconds
        let open = row[1].as_f64()?;
        let high = row[2].as_f64()?;
        let low = row[3].as_f64()?;
        let close = row[4].as_f64()?;
        candles.push(Candle {
            time,
            open,
            high,
            low,
            close,
            volume: 0.0, // CoinGecko OHLC does not include volume
        });
    }
    Some(candles)
}

/// Fetch price history from CoinGecko as [`Point`]s.
///
/// The `days` parameter controls how far back to look.
/// Response: `{"prices":[[timestamp_ms, price], ...], ...}`
fn fetch_coingecko_prices(coin_id: &str, days: &str) -> Option<Vec<Point>> {
    let url = format!(
        "https://api.coingecko.com/api/v3/coins/{}/market_chart?vs_currency=usd&days={}",
        coin_id, days,
    );
    let resp = ureq::get(&url).call().ok()?;
    let body: serde_json::Value = resp.into_json().ok()?;
    let prices = body["prices"].as_array()?;

    let mut points = Vec::with_capacity(prices.len());
    for entry in prices {
        let pair = entry.as_array()?;
        let x = pair[0].as_f64()? / 1000.0; // ms -> seconds
        let y = pair[1].as_f64()?;
        points.push(Point { x, y });
    }
    Some(points)
}

/// Fetch the current price from CoinGecko.
///
/// Response: `{"bitcoin":{"usd":12345.67}}`
fn fetch_coingecko_spot(coin_id: &str) -> Option<f64> {
    let url = format!(
        "https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies=usd",
        coin_id,
    );
    let resp = ureq::get(&url).call().ok()?;
    let body: serde_json::Value = resp.into_json().ok()?;
    body[coin_id]["usd"].as_f64()
}

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

    #[test]
    fn test_exchange_client_constructors() {
        let b = ExchangeClient::binance();
        assert_eq!(b.exchange, Exchange::Binance);

        let c = ExchangeClient::coingecko();
        assert_eq!(c.exchange, Exchange::CoinGecko);

        let n = ExchangeClient::new(Exchange::Binance);
        assert_eq!(n.exchange, Exchange::Binance);
    }
}
