use barter_instrument::exchange::ExchangeId;
use chrono::{DateTime, TimeZone, Utc};
use serde::Deserialize;
use serde_json::Value;

use crate::error::ApiError;
use super::{InstrumentPair, KlineCandle};

#[derive(Deserialize)]
struct ExchangeInfo {
    symbols: Vec<SymbolInfo>,
}

#[derive(Deserialize)]
struct SymbolInfo {
    #[serde(alias = "baseAsset")]
    base_asset: String,
    #[serde(alias = "quoteAsset")]
    quote_asset: String,
    status: String,
    /// Present on futures exchanges only. Absent on spot (serde defaults to None).
    #[serde(alias = "contractType", default)]
    contract_type: Option<String>,
}

fn exchange_info_url(exchange: ExchangeId) -> &'static str {
    match exchange {
        ExchangeId::BinanceSpot => "https://api.binance.com/api/v3/exchangeInfo",
        ExchangeId::BinanceUs => "https://api.binance.us/api/v3/exchangeInfo",
        ExchangeId::BinanceFuturesUsd => "https://fapi.binance.com/fapi/v1/exchangeInfo",
        ExchangeId::BinanceFuturesCoin => "https://dapi.binance.com/dapi/v1/exchangeInfo",
        _ => unreachable!(),
    }
}

fn klines_url(exchange: ExchangeId) -> &'static str {
    match exchange {
        ExchangeId::BinanceSpot => "https://api.binance.com/api/v3/klines",
        ExchangeId::BinanceUs => "https://api.binance.us/api/v3/klines",
        ExchangeId::BinanceFuturesUsd => "https://fapi.binance.com/fapi/v1/klines",
        ExchangeId::BinanceFuturesCoin => "https://dapi.binance.com/dapi/v1/klines",
        _ => unreachable!(),
    }
}

pub async fn fetch(
    client: &reqwest::Client,
    exchange: ExchangeId,
) -> Result<Vec<InstrumentPair>, ApiError> {
    let url = exchange_info_url(exchange);
    let info: ExchangeInfo = client
        .get(url)
        .send()
        .await
        .map_err(|e| ApiError::Internal(format!("binance request failed: {e}")))?
        .json()
        .await
        .map_err(|e| ApiError::Internal(format!("binance json parse failed: {e}")))?;

    let pairs = info
        .symbols
        .into_iter()
        .filter(|s| {
            s.status == "TRADING"
                // On futures exchanges, only include perpetual contracts; skip
                // quarterly/delivery futures which share the same base-quote pair
                // but are not useful for backtesting.
                && s.contract_type.as_deref().is_none_or(|ct| ct == "PERPETUAL")
        })
        .map(|s| InstrumentPair {
            base: s.base_asset,
            quote: s.quote_asset,
        })
        .collect();

    Ok(pairs)
}

/// Fetch historical klines from Binance REST API, paginating automatically.
/// Binance returns up to 1000 candles per request; we page using `startTime`.
pub async fn fetch_klines(
    client: &reqwest::Client,
    exchange: ExchangeId,
    symbol: &str,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<Vec<KlineCandle>, ApiError> {
    let url = klines_url(exchange);
    const LIMIT: u64 = 1000;
    let mut candles = Vec::new();
    let mut start_ms = from.timestamp_millis();
    let end_ms = to.timestamp_millis();

    loop {
        let resp: Value = client
            .get(url)
            .query(&[
                ("symbol", symbol),
                ("interval", interval),
                ("startTime", &start_ms.to_string()),
                ("endTime", &end_ms.to_string()),
                ("limit", &LIMIT.to_string()),
            ])
            .send()
            .await
            .map_err(|e| ApiError::Internal(format!("binance klines request failed: {e}")))?
            .json()
            .await
            .map_err(|e| ApiError::Internal(format!("binance klines json parse failed: {e}")))?;

        let arr = resp.as_array().ok_or_else(|| {
            ApiError::Internal("binance klines: unexpected response shape".into())
        })?;

        if arr.is_empty() {
            break;
        }

        for row in arr {
            let row = row.as_array().ok_or_else(|| {
                ApiError::Internal("binance klines: row is not an array".into())
            })?;

            // Binance kline array layout:
            // [0] open_time_ms, [1] open, [2] high, [3] low, [4] close,
            // [5] volume, [6] close_time_ms, [7] quote_asset_volume,
            // [8] trade_count, ...
            let open_time_ms = row[0]
                .as_i64()
                .ok_or_else(|| ApiError::Internal("kline open_time not i64".into()))?;
            let close_time_ms = row[6]
                .as_i64()
                .ok_or_else(|| ApiError::Internal("kline close_time not i64".into()))?;
            let parse_f64 = |v: &Value, name: &str| -> Result<f64, ApiError> {
                v.as_str()
                    .and_then(|s| s.parse().ok())
                    .ok_or_else(|| ApiError::Internal(format!("kline {name} not parseable")))
            };
            let trade_count = row[8]
                .as_i64()
                .ok_or_else(|| ApiError::Internal("kline trade_count not i64".into()))?
                as i32;

            candles.push(KlineCandle {
                open_time: Utc
                    .timestamp_millis_opt(open_time_ms)
                    .single()
                    .ok_or_else(|| {
                        ApiError::Internal("kline open_time timestamp invalid".into())
                    })?,
                close_time: Utc
                    .timestamp_millis_opt(close_time_ms)
                    .single()
                    .ok_or_else(|| {
                        ApiError::Internal("kline close_time timestamp invalid".into())
                    })?,
                open: parse_f64(&row[1], "open")?,
                high: parse_f64(&row[2], "high")?,
                low: parse_f64(&row[3], "low")?,
                close: parse_f64(&row[4], "close")?,
                volume: parse_f64(&row[5], "volume")?,
                trade_count,
            });
        }

        let fetched = arr.len() as u64;
        if fetched < LIMIT {
            break; // last page
        }

        // Advance start to open_time of the last candle + 1ms to avoid re-fetching it.
        let last_open_ms = arr
            .last()
            .and_then(|r| r.as_array())
            .and_then(|r| r[0].as_i64())
            .unwrap_or(start_ms);
        start_ms = last_open_ms + 1;

        if start_ms >= end_ms {
            break;
        }
    }

    Ok(candles)
}
