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::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_BASE_URL: &str = "https://api.hyperliquid.xyz";

#[derive(Clone, Debug)]
pub struct HyperliquidProvider {
    client: reqwest::Client,
    base_url: String,
}

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

impl HyperliquidProvider {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
            base_url: DEFAULT_BASE_URL.to_string(),
        }
    }

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

    #[tracing::instrument(level = "debug", skip_all)]
    async fn post_info<T: serde::de::DeserializeOwned>(
        &self,
        body: serde_json::Value,
    ) -> Result<T> {
        let url = format!("{}/info", self.base_url.trim_end_matches('/'));
        let response = self
            .client
            .post(&url)
            .json(&body)
            .send()
            .await
            .map_err(|err| BtError::Data(format!("hyperliquid request failed: {err}")))?;

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

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

        serde_json::from_str::<T>(&payload)
            .map_err(|err| BtError::Data(format!("hyperliquid 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 interval_to_hyperliquid(interval: BarInterval) -> Result<String> {
        let s = match interval {
            BarInterval::Minutes(1) => "1m",
            BarInterval::Minutes(5) => "5m",
            BarInterval::Minutes(15) => "15m",
            BarInterval::Minutes(30) => "30m",
            BarInterval::Hours(1) => "1h",
            BarInterval::Hours(4) => "4h",
            BarInterval::Days(1) => "1d",
            _ => {
                return Err(BtError::Data(format!(
                    "unsupported hyperliquid bar interval: {interval:?}"
                )))
            }
        };
        Ok(s.to_string())
    }

    /// Strip the perpetual suffix used by the engine (e.g. "BTC-PERP" → "BTC").
    /// Hyperliquid uses bare coin names like "BTC", "ETH".
    fn normalize_coin(symbol: &str) -> &str {
        symbol
            .strip_suffix("-PERP")
            .or_else(|| symbol.strip_suffix("-USD"))
            .unwrap_or(symbol)
    }

    fn trades_to_record_batch(symbol: &str, trades: &[HlTrade]) -> 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 {
            let ts_nanos = trade
                .time
                .checked_mul(NANOS_PER_MILLISECOND)
                .ok_or_else(|| BtError::Data("timestamp overflow".to_string()))?;
            timestamps.push(ts_nanos);
            received_at.push(ts_nanos);
            providers.push("hyperliquid".to_string());
            symbols.push(symbol.to_string());
            symbol_ids.push(0_u32);
            prices.push(
                trade
                    .px
                    .parse::<f64>()
                    .map_err(|e| BtError::Data(format!("invalid price '{}': {e}", trade.px)))?,
            );
            quantities.push(
                trade
                    .sz
                    .parse::<f64>()
                    .map_err(|e| BtError::Data(format!("invalid size '{}': {e}", trade.sz)))?,
            );
            sides.push(if trade.side == "B" { 1_u8 } else { 2_u8 });
            trade_ids.push(trade.tid.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 candles_to_record_batch(candles: &[HlCandle]) -> Result<RecordBatch> {
        if candles.is_empty() {
            return Ok(RecordBatch::new_empty(bars_1m_schema()));
        }

        let row_count = candles.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, candle) in candles.iter().enumerate() {
            let parse = |s: &str, field: &str| -> Result<f64> {
                s.parse::<f64>().map_err(|e| {
                    BtError::Data(format!(
                        "hyperliquid candle invalid {field} at index {idx}: '{s}' ({e})"
                    ))
                })
            };

            let o = parse(&candle.o, "open")?;
            let h = parse(&candle.h, "high")?;
            let l = parse(&candle.l, "low")?;
            let c = parse(&candle.c, "close")?;
            let v = parse(&candle.v, "volume")?;

            timestamps.push(
                candle
                    .t
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("candle timestamp overflow".to_string()))?,
            );
            symbol_ids.push(0_u32);
            open.push(o);
            high.push(h);
            low.push(l);
            close.push(c);
            volume.push(v);
            // Hyperliquid doesn't provide quote volume, approximate VWAP as midpoint
            vwap.push((h + l) / 2.0);
            buy_volume.push(None);
            sell_volume.push(None);
            trade_count.push(candle.n);
            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_to_record_batch(coin: &str, records: &[HlFundingEntry]) -> Result<RecordBatch> {
        if records.is_empty() {
            return Ok(RecordBatch::new_empty(funding_rates_schema()));
        }

        let row_count = records.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 records {
            timestamps.push(
                record
                    .time
                    .checked_mul(NANOS_PER_MILLISECOND)
                    .ok_or_else(|| BtError::Data("funding timestamp overflow".to_string()))?,
            );
            symbols.push(coin.to_string());
            rates.push(record.funding_rate.parse::<f64>().map_err(|e| {
                BtError::Data(format!(
                    "invalid funding_rate '{}': {e}",
                    record.funding_rate
                ))
            })?);
            mark_prices.push(match &record.premium {
                Some(p) => Some(
                    p.parse::<f64>()
                        .map_err(|e| BtError::Data(format!("invalid premium '{p}': {e}")))?,
                ),
                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 HyperliquidProvider {
    fn name(&self) -> &str {
        "hyperliquid"
    }

    fn asset_classes(&self) -> &[AssetClass] {
        static ASSET_CLASSES: [AssetClass; 1] = [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 coin = Self::normalize_coin(symbol);
        let payload = self
            .post_info::<Vec<HlTrade>>(serde_json::json!({
                "type": "recentTrades",
                "coin": coin,
                "startTime": start_ms,
            }))
            .await?;

        let batch = Self::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 coin = Self::normalize_coin(symbol);
        let hl_interval = Self::interval_to_hyperliquid(interval)?;
        let payload = self
            .post_info::<Vec<HlCandle>>(serde_json::json!({
                "type": "candleSnapshot",
                "req": {
                    "coin": coin,
                    "interval": hl_interval,
                    "startTime": start_ms,
                    "endTime": end_ms,
                }
            }))
            .await?;

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

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn fetch_funding_rates(
        &self,
        symbol: &str,
        range: TimeRange,
    ) -> Result<Option<RecordBatch>> {
        let (start_ms, end_ms) = Self::range_to_millis(range)?;
        let coin = Self::normalize_coin(symbol);
        let payload = self
            .post_info::<Vec<HlFundingEntry>>(serde_json::json!({
                "type": "fundingHistory",
                "coin": coin,
                "startTime": start_ms,
                "endTime": end_ms,
            }))
            .await?;

        Ok(Some(Self::funding_to_record_batch(coin, &payload)?))
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn list_symbols(&self) -> Result<Vec<ProviderSymbol>> {
        let meta = self
            .post_info::<HlMeta>(serde_json::json!({ "type": "meta" }))
            .await?;

        let symbols = meta
            .universe
            .into_iter()
            .map(|asset| ProviderSymbol {
                symbol: format!("{}-PERP", asset.name),
                base_asset: asset.name,
                quote_asset: "USD".to_string(),
                status: "active".to_string(),
                asset_class: AssetClass::CryptoPerpetual,
            })
            .collect::<Vec<_>>();

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

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

// ── Hyperliquid API response types ──────────────────────────────────────────

#[derive(Debug, Deserialize)]
struct HlTrade {
    #[serde(rename = "tid")]
    tid: u64,
    px: String,
    sz: String,
    side: String,
    time: i64,
}

#[derive(Debug, Deserialize)]
struct HlCandle {
    /// Open time in milliseconds
    #[serde(rename = "t")]
    t: i64,
    /// Open price
    #[serde(rename = "o")]
    o: String,
    /// High price
    #[serde(rename = "h")]
    h: String,
    /// Low price
    #[serde(rename = "l")]
    l: String,
    /// Close price
    #[serde(rename = "c")]
    c: String,
    /// Volume
    #[serde(rename = "v")]
    v: String,
    /// Number of trades
    n: u32,
}

#[derive(Debug, Deserialize)]
struct HlFundingEntry {
    time: i64,
    #[serde(rename = "fundingRate")]
    funding_rate: String,
    premium: Option<String>,
}

#[derive(Debug, Deserialize)]
struct HlMeta {
    universe: Vec<HlAsset>,
}

#[derive(Debug, Deserialize)]
struct HlAsset {
    name: String,
}

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

    use super::*;

    #[test]
    fn trades_to_record_batch_maps_side_and_price() {
        let trades = vec![
            HlTrade {
                tid: 1,
                px: "42000.5".to_string(),
                sz: "0.1".to_string(),
                side: "B".to_string(),
                time: 1_704_067_200_000,
            },
            HlTrade {
                tid: 2,
                px: "42001.0".to_string(),
                sz: "0.2".to_string(),
                side: "A".to_string(),
                time: 1_704_067_201_000,
            },
        ];

        let batch = HyperliquidProvider::trades_to_record_batch("BTC-PERP", &trades)
            .expect("convert 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), 42000.5);
        assert_eq!(prices.value(1), 42001.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); // B = buy
        assert_eq!(sides.value(1), 2); // A = sell
    }

    #[test]
    fn candles_to_record_batch_computes_vwap() {
        let candles = vec![HlCandle {
            t: 1_704_067_200_000,
            o: "100.0".to_string(),
            h: "102.0".to_string(),
            l: "98.0".to_string(),
            c: "101.0".to_string(),
            v: "500.0".to_string(),
            n: 42,
        }];

        let batch =
            HyperliquidProvider::candles_to_record_batch(&candles).expect("candle 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.0); // (102 + 98) / 2
    }

    #[test]
    fn interval_conversion_handles_supported_and_rejects_unknown() {
        assert_eq!(
            HyperliquidProvider::interval_to_hyperliquid(BarInterval::Minutes(1)).expect("1m"),
            "1m"
        );
        assert_eq!(
            HyperliquidProvider::interval_to_hyperliquid(BarInterval::Hours(4)).expect("4h"),
            "4h"
        );
        assert!(HyperliquidProvider::interval_to_hyperliquid(BarInterval::Seconds(1)).is_err());
    }

    #[test]
    fn normalize_coin_strips_perp_suffix() {
        assert_eq!(HyperliquidProvider::normalize_coin("BTC-PERP"), "BTC");
        assert_eq!(HyperliquidProvider::normalize_coin("ETH-USD"), "ETH");
        assert_eq!(HyperliquidProvider::normalize_coin("SOL"), "SOL");
    }
}
