use std::sync::Arc;

use arrow::array::{
    ArrayRef, BooleanArray, Float64Array, StringArray, TimestampNanosecondArray, UInt32Array,
    UInt8Array,
};
use arrow::record_batch::RecordBatch;
use async_trait::async_trait;
use bt_kernel::{AssetClass, BtError, Result, TimeRange};
use serde::de::DeserializeOwned;
use serde::Deserialize;

use crate::provider::{
    bars_1m_schema, funding_rates_schema, raw_trades_schema, BarInterval, DataProvider,
    ProviderSymbol, RateLimitInfo,
};

const NANOS_PER_MILLISECOND: i64 = 1_000_000;
const DEFAULT_SPOT_BASE_URL: &str = "https://api.binance.com";
const DEFAULT_FUTURES_BASE_URL: &str = "https://fapi.binance.com";

#[derive(Clone, Debug)]
pub struct BinanceProvider {
    client: reqwest::Client,
    spot_base_url: String,
    futures_base_url: String,
}

impl Default for BinanceProvider {
    fn default() -> Self {
        Self::new()
    }
}

impl BinanceProvider {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
            spot_base_url: DEFAULT_SPOT_BASE_URL.to_string(),
            futures_base_url: DEFAULT_FUTURES_BASE_URL.to_string(),
        }
    }

    pub fn with_base_urls(
        spot_base_url: impl Into<String>,
        futures_base_url: impl Into<String>,
    ) -> Self {
        Self {
            client: reqwest::Client::new(),
            spot_base_url: spot_base_url.into(),
            futures_base_url: futures_base_url.into(),
        }
    }

    #[tracing::instrument(level = "debug", skip_all, fields(url))]
    async fn get_json<T: DeserializeOwned>(
        &self,
        base_url: &str,
        path: &str,
        query: &[(&str, String)],
    ) -> Result<T> {
        let url = format!("{}{}", base_url.trim_end_matches('/'), path);
        tracing::Span::current().record("url", tracing::field::display(&url));
        let response = self
            .client
            .get(&url)
            .query(query)
            .send()
            .await
            .map_err(|err| BtError::Data(format!("binance request failed: {err}")))?;

        let status = response.status();
        tracing::debug!(%status, "received HTTP response");
        let payload = response
            .text()
            .await
            .map_err(|err| BtError::Data(format!("binance response read failed: {err}")))?;

        if !status.is_success() {
            return Err(BtError::Data(format!(
                "binance request returned {status}: {payload}"
            )));
        }

        serde_json::from_str::<T>(&payload)
            .map_err(|err| BtError::Data(format!("binance json decode failed: {err}")))
    }

    fn range_to_millis(range: TimeRange) -> Result<(i64, i64)> {
        let start_ms = range.start.0.div_euclid(NANOS_PER_MILLISECOND);
        let end_ms = range
            .end
            .0
            .div_euclid(NANOS_PER_MILLISECOND)
            .checked_sub(1)
            .ok_or_else(|| {
                BtError::TimeRange("invalid end time for millisecond conversion".to_string())
            })?;
        Ok((start_ms, end_ms))
    }

    fn parse_f64(value: &str, field: &str) -> Result<f64> {
        value.parse::<f64>().map_err(|err| {
            BtError::Data(format!(
                "binance payload contains invalid float for {field}: '{value}' ({err})"
            ))
        })
    }

    fn interval_to_binance(interval: BarInterval) -> Result<String> {
        let interval = match interval {
            BarInterval::Seconds(1) => "1s".to_string(),
            BarInterval::Minutes(1 | 3 | 5 | 15 | 30) => {
                let BarInterval::Minutes(value) = interval else {
                    unreachable!()
                };
                format!("{value}m")
            }
            BarInterval::Hours(1 | 2 | 4 | 6 | 8 | 12) => {
                let BarInterval::Hours(value) = interval else {
                    unreachable!()
                };
                format!("{value}h")
            }
            BarInterval::Days(1) => "1d".to_string(),
            _ => {
                return Err(BtError::Data(format!(
                    "unsupported binance bar interval: {interval:?}"
                )))
            }
        };

        Ok(interval)
    }

    fn agg_trades_to_record_batch(symbol: &str, trades: &[BinanceAggTrade]) -> Result<RecordBatch> {
        if trades.is_empty() {
            return Ok(RecordBatch::new_empty(raw_trades_schema()));
        }

        let row_count = trades.len();
        let mut timestamps = Vec::with_capacity(row_count);
        let mut received_at = Vec::with_capacity(row_count);
        let mut providers = Vec::with_capacity(row_count);
        let mut symbols = Vec::with_capacity(row_count);
        let mut symbol_ids = Vec::with_capacity(row_count);
        let mut prices = Vec::with_capacity(row_count);
        let mut quantities = Vec::with_capacity(row_count);
        let mut sides = Vec::with_capacity(row_count);
        let mut trade_ids = Vec::with_capacity(row_count);
        let mut conditions: Vec<Option<String>> = Vec::with_capacity(row_count);

        for trade in trades {
            timestamps.push(
                trade
                    .trade_time_ms
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("timestamp overflow".to_string()))?,
            );
            received_at.push(
                trade
                    .trade_time_ms
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("received_at overflow".to_string()))?,
            );
            providers.push("binance".to_string());
            symbols.push(symbol.to_string());
            symbol_ids.push(0_u32);
            prices.push(Self::parse_f64(&trade.price, "price")?);
            quantities.push(Self::parse_f64(&trade.quantity, "quantity")?);
            sides.push(if trade.is_buyer_maker { 2_u8 } else { 1_u8 });
            trade_ids.push(trade.aggregate_trade_id.to_string());
            conditions.push(None);
        }

        let arrays: Vec<ArrayRef> = vec![
            Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
            Arc::new(TimestampNanosecondArray::from(received_at).with_timezone("UTC")),
            Arc::new(StringArray::from(providers)),
            Arc::new(StringArray::from(symbols)),
            Arc::new(UInt32Array::from(symbol_ids)),
            Arc::new(Float64Array::from(prices)),
            Arc::new(Float64Array::from(quantities)),
            Arc::new(UInt8Array::from(sides)),
            Arc::new(StringArray::from(trade_ids)),
            Arc::new(StringArray::from(conditions)),
        ];

        Ok(RecordBatch::try_new(raw_trades_schema(), arrays)?)
    }

    fn klines_to_record_batch(klines: &[Vec<serde_json::Value>]) -> Result<RecordBatch> {
        if klines.is_empty() {
            return Ok(RecordBatch::new_empty(bars_1m_schema()));
        }

        let row_count = klines.len();
        let mut timestamps = Vec::with_capacity(row_count);
        let mut symbol_ids = Vec::with_capacity(row_count);
        let mut open = Vec::with_capacity(row_count);
        let mut high = Vec::with_capacity(row_count);
        let mut low = Vec::with_capacity(row_count);
        let mut close = Vec::with_capacity(row_count);
        let mut vwap = Vec::with_capacity(row_count);
        let mut volume = Vec::with_capacity(row_count);
        let mut buy_volume: Vec<Option<f64>> = Vec::with_capacity(row_count);
        let mut sell_volume: Vec<Option<f64>> = Vec::with_capacity(row_count);
        let mut trade_count = Vec::with_capacity(row_count);
        let mut bid: Vec<Option<f64>> = Vec::with_capacity(row_count);
        let mut ask: Vec<Option<f64>> = Vec::with_capacity(row_count);
        let mut spread: Vec<Option<f64>> = Vec::with_capacity(row_count);
        let mut is_gap = Vec::with_capacity(row_count);
        let mut gap_fill_method = Vec::with_capacity(row_count);

        for (idx, kline) in klines.iter().enumerate() {
            if kline.len() < 9 {
                return Err(BtError::Data(format!(
                    "binance kline at index {idx} has {} fields, expected >= 9",
                    kline.len()
                )));
            }

            let open_time_ms = kline[0]
                .as_i64()
                .ok_or_else(|| BtError::Data(format!("invalid kline open_time at index {idx}")))?;
            let open_price = Self::parse_f64(
                kline[1]
                    .as_str()
                    .ok_or_else(|| BtError::Data(format!("invalid kline open at index {idx}")))?,
                "open",
            )?;
            let high_price = Self::parse_f64(
                kline[2]
                    .as_str()
                    .ok_or_else(|| BtError::Data(format!("invalid kline high at index {idx}")))?,
                "high",
            )?;
            let low_price = Self::parse_f64(
                kline[3]
                    .as_str()
                    .ok_or_else(|| BtError::Data(format!("invalid kline low at index {idx}")))?,
                "low",
            )?;
            let close_price = Self::parse_f64(
                kline[4]
                    .as_str()
                    .ok_or_else(|| BtError::Data(format!("invalid kline close at index {idx}")))?,
                "close",
            )?;
            let traded_volume = Self::parse_f64(
                kline[5]
                    .as_str()
                    .ok_or_else(|| BtError::Data(format!("invalid kline volume at index {idx}")))?,
                "volume",
            )?;
            let quote_volume = Self::parse_f64(
                kline[7].as_str().ok_or_else(|| {
                    BtError::Data(format!("invalid kline quote_volume at index {idx}"))
                })?,
                "quote_volume",
            )?;
            let trades = kline[8].as_u64().ok_or_else(|| {
                BtError::Data(format!("invalid kline trade_count at index {idx}"))
            })?;

            timestamps.push(
                open_time_ms
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("kline timestamp overflow".to_string()))?,
            );
            symbol_ids.push(0_u32);
            open.push(open_price);
            high.push(high_price);
            low.push(low_price);
            close.push(close_price);
            volume.push(traded_volume);
            vwap.push(if traded_volume > 0.0 {
                quote_volume / traded_volume
            } else {
                close_price
            });
            buy_volume.push(None);
            sell_volume.push(None);
            trade_count.push(u32::try_from(trades).map_err(|_| {
                BtError::Data(format!(
                    "kline trade_count too large at index {idx}: {trades}"
                ))
            })?);
            bid.push(None);
            ask.push(None);
            spread.push(None);
            is_gap.push(false);
            gap_fill_method.push(0_u8);
        }

        let arrays: Vec<ArrayRef> = vec![
            Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
            Arc::new(UInt32Array::from(symbol_ids)),
            Arc::new(Float64Array::from(open)),
            Arc::new(Float64Array::from(high)),
            Arc::new(Float64Array::from(low)),
            Arc::new(Float64Array::from(close)),
            Arc::new(Float64Array::from(vwap)),
            Arc::new(Float64Array::from(volume)),
            Arc::new(Float64Array::from(buy_volume)),
            Arc::new(Float64Array::from(sell_volume)),
            Arc::new(UInt32Array::from(trade_count)),
            Arc::new(Float64Array::from(bid)),
            Arc::new(Float64Array::from(ask)),
            Arc::new(Float64Array::from(spread)),
            Arc::new(BooleanArray::from(is_gap)),
            Arc::new(UInt8Array::from(gap_fill_method)),
        ];

        Ok(RecordBatch::try_new(bars_1m_schema(), arrays)?)
    }

    fn funding_rates_to_record_batch(
        symbol: &str,
        funding_rates: &[BinanceFundingRate],
    ) -> Result<RecordBatch> {
        if funding_rates.is_empty() {
            return Ok(RecordBatch::new_empty(funding_rates_schema()));
        }

        let row_count = funding_rates.len();
        let mut timestamps = Vec::with_capacity(row_count);
        let mut symbols = Vec::with_capacity(row_count);
        let mut rates = Vec::with_capacity(row_count);
        let mut mark_prices: Vec<Option<f64>> = Vec::with_capacity(row_count);

        for record in funding_rates {
            timestamps.push(
                record
                    .funding_time_ms
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("funding timestamp overflow".to_string()))?,
            );
            symbols.push(symbol.to_string());
            rates.push(Self::parse_f64(&record.funding_rate, "funding_rate")?);
            mark_prices.push(match &record.mark_price {
                Some(value) => Some(Self::parse_f64(value, "mark_price")?),
                None => None,
            });
        }

        let arrays: Vec<ArrayRef> = vec![
            Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
            Arc::new(StringArray::from(symbols)),
            Arc::new(Float64Array::from(rates)),
            Arc::new(Float64Array::from(mark_prices)),
        ];

        Ok(RecordBatch::try_new(funding_rates_schema(), arrays)?)
    }
}

#[async_trait]
impl DataProvider for BinanceProvider {
    fn name(&self) -> &str {
        "binance"
    }

    fn asset_classes(&self) -> &[AssetClass] {
        static ASSET_CLASSES: [AssetClass; 2] =
            [AssetClass::CryptoSpot, AssetClass::CryptoPerpetual];
        &ASSET_CLASSES
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn fetch_trades(&self, symbol: &str, range: TimeRange) -> Result<RecordBatch> {
        let (start_ms, end_ms) = Self::range_to_millis(range)?;
        let payload = self
            .get_json::<Vec<BinanceAggTrade>>(
                &self.spot_base_url,
                "/api/v3/aggTrades",
                &[
                    ("symbol", symbol.to_string()),
                    ("startTime", start_ms.to_string()),
                    ("endTime", end_ms.to_string()),
                    ("limit", "1000".to_string()),
                ],
            )
            .await?;

        let batch = Self::agg_trades_to_record_batch(symbol, &payload)?;
        tracing::debug!(rows = batch.num_rows(), "fetched trades");
        Ok(batch)
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn fetch_bars(
        &self,
        symbol: &str,
        range: TimeRange,
        interval: BarInterval,
    ) -> Result<Option<RecordBatch>> {
        let (start_ms, end_ms) = Self::range_to_millis(range)?;
        let interval = Self::interval_to_binance(interval)?;
        let payload = self
            .get_json::<Vec<Vec<serde_json::Value>>>(
                &self.spot_base_url,
                "/api/v3/klines",
                &[
                    ("symbol", symbol.to_string()),
                    ("interval", interval),
                    ("startTime", start_ms.to_string()),
                    ("endTime", end_ms.to_string()),
                    ("limit", "1000".to_string()),
                ],
            )
            .await?;

        let batch = Self::klines_to_record_batch(&payload)?;
        tracing::debug!(rows = batch.num_rows(), "fetched bars");
        Ok(Some(batch))
    }

    async fn fetch_funding_rates(
        &self,
        symbol: &str,
        range: TimeRange,
    ) -> Result<Option<RecordBatch>> {
        let (start_ms, end_ms) = Self::range_to_millis(range)?;
        let payload = self
            .get_json::<Vec<BinanceFundingRate>>(
                &self.futures_base_url,
                "/fapi/v1/fundingRate",
                &[
                    ("symbol", symbol.to_string()),
                    ("startTime", start_ms.to_string()),
                    ("endTime", end_ms.to_string()),
                    ("limit", "1000".to_string()),
                ],
            )
            .await?;

        Ok(Some(Self::funding_rates_to_record_batch(symbol, &payload)?))
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn list_symbols(&self) -> Result<Vec<ProviderSymbol>> {
        let payload = self
            .get_json::<BinanceExchangeInfo>(&self.spot_base_url, "/api/v3/exchangeInfo", &[])
            .await?;

        let symbols = payload
            .symbols
            .into_iter()
            .map(|symbol| ProviderSymbol {
                symbol: symbol.symbol,
                base_asset: symbol.base_asset,
                quote_asset: symbol.quote_asset,
                status: symbol.status,
                asset_class: AssetClass::CryptoSpot,
            })
            .collect::<Vec<_>>();

        tracing::debug!(count = symbols.len(), "fetched symbols");
        Ok(symbols)
    }

    fn rate_limits(&self) -> RateLimitInfo {
        RateLimitInfo {
            requests_per_minute: Some(1200),
            burst: Some(50),
        }
    }
}

#[derive(Debug, Deserialize)]
struct BinanceAggTrade {
    #[serde(rename = "a")]
    aggregate_trade_id: u64,
    #[serde(rename = "p")]
    price: String,
    #[serde(rename = "q")]
    quantity: String,
    #[serde(rename = "T")]
    trade_time_ms: i64,
    #[serde(rename = "m")]
    is_buyer_maker: bool,
}

#[derive(Debug, Deserialize)]
struct BinanceFundingRate {
    #[serde(rename = "fundingRate")]
    funding_rate: String,
    #[serde(rename = "fundingTime")]
    funding_time_ms: i64,
    #[serde(rename = "markPrice")]
    mark_price: Option<String>,
}

#[derive(Debug, Deserialize)]
struct BinanceExchangeInfo {
    symbols: Vec<BinanceExchangeSymbol>,
}

#[derive(Debug, Deserialize)]
struct BinanceExchangeSymbol {
    symbol: String,
    status: String,
    #[serde(rename = "baseAsset")]
    base_asset: String,
    #[serde(rename = "quoteAsset")]
    quote_asset: String,
}

#[cfg(test)]
mod tests {
    use arrow::array::{Float64Array, UInt8Array};

    use super::*;

    #[test]
    fn agg_trades_to_record_batch_maps_side_and_price() {
        let trades = vec![
            BinanceAggTrade {
                aggregate_trade_id: 10,
                price: "101.5".to_string(),
                quantity: "2.5".to_string(),
                trade_time_ms: 1_704_067_200_000,
                is_buyer_maker: false,
            },
            BinanceAggTrade {
                aggregate_trade_id: 11,
                price: "102.0".to_string(),
                quantity: "1.0".to_string(),
                trade_time_ms: 1_704_067_201_000,
                is_buyer_maker: true,
            },
        ];

        let batch = BinanceProvider::agg_trades_to_record_batch("BTCUSDT", &trades)
            .expect("convert agg trades");
        assert_eq!(batch.num_rows(), 2);

        let prices = batch
            .column_by_name("price")
            .expect("price column")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("float64 price");
        assert_eq!(prices.value(0), 101.5);
        assert_eq!(prices.value(1), 102.0);

        let sides = batch
            .column_by_name("side")
            .expect("side column")
            .as_any()
            .downcast_ref::<UInt8Array>()
            .expect("u8 side");
        assert_eq!(sides.value(0), 1);
        assert_eq!(sides.value(1), 2);
    }

    #[test]
    fn klines_to_record_batch_computes_vwap() {
        let klines = vec![vec![
            serde_json::json!(1_704_067_200_000_i64),
            serde_json::json!("100.0"),
            serde_json::json!("102.0"),
            serde_json::json!("99.0"),
            serde_json::json!("101.0"),
            serde_json::json!("10.0"),
            serde_json::json!(1_704_067_260_000_i64),
            serde_json::json!("1005.0"),
            serde_json::json!(8_u64),
        ]];

        let batch = BinanceProvider::klines_to_record_batch(&klines).expect("kline conversion");
        assert_eq!(batch.num_rows(), 1);

        let vwap = batch
            .column_by_name("vwap")
            .expect("vwap column")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("vwap array");
        assert_eq!(vwap.value(0), 100.5);
    }

    #[test]
    fn interval_to_binance_handles_supported_and_rejects_unknown() {
        assert_eq!(
            BinanceProvider::interval_to_binance(BarInterval::Seconds(1)).expect("1s"),
            "1s"
        );
        assert_eq!(
            BinanceProvider::interval_to_binance(BarInterval::Minutes(5)).expect("5m"),
            "5m"
        );
        assert!(BinanceProvider::interval_to_binance(BarInterval::Seconds(2)).is_err());
    }
}
