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, raw_trades_schema, BarInterval, DataProvider, ProviderSymbol, RateLimitInfo,
};

const NANOS_PER_MILLISECOND: i64 = 1_000_000;
const DEFAULT_BASE_URL: &str = "https://api.massive.com";

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

impl MassiveProvider {
    /// Create a new Massive provider. Requires a Pro license (`data_connectors` feature).
    pub fn new(api_key: impl Into<String>) -> Result<Self> {
        bt_license::check_feature("data_connectors")
            .map_err(|e| BtError::Unsupported(e.to_string()))?;
        Ok(Self {
            client: reqwest::Client::new(),
            base_url: DEFAULT_BASE_URL.to_string(),
            api_key: api_key.into(),
        })
    }

    /// Test/internal constructor — bypasses license check.
    #[cfg(test)]
    pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
        Self {
            client: reqwest::Client::new(),
            base_url: base_url.into(),
            api_key: api_key.into(),
        }
    }

    pub fn from_env() -> Result<Self> {
        bt_license::check_feature("data_connectors")
            .map_err(|e| BtError::Unsupported(e.to_string()))?;
        let api_key = std::env::var("MASSIVE_API_KEY").map_err(|_| {
            BtError::Data("MASSIVE_API_KEY environment variable not set".to_string())
        })?;
        Self::new(api_key)
    }

    #[tracing::instrument(level = "debug", skip_all, fields(url))]
    async fn get_json<T: serde::de::DeserializeOwned>(
        &self,
        path: &str,
        query: &[(&str, String)],
    ) -> Result<T> {
        let url = format!("{}{}", self.base_url.trim_end_matches('/'), path);
        tracing::Span::current().record("url", tracing::field::display(&url));

        let mut all_query = vec![("apiKey", self.api_key.clone())];
        all_query.extend_from_slice(query);

        let response = self
            .client
            .get(&url)
            .query(&all_query)
            .send()
            .await
            .map_err(|err| BtError::Data(format!("massive request failed: {err}")))?;

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

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

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

    fn range_to_dates(range: TimeRange) -> (String, String) {
        let start_ms = range.start.0.div_euclid(NANOS_PER_MILLISECOND);
        let end_ms = range.end.0.div_euclid(NANOS_PER_MILLISECOND);
        (start_ms.to_string(), end_ms.to_string())
    }

    fn interval_to_massive(interval: BarInterval) -> Result<(String, String)> {
        let (multiplier, timespan) = match interval {
            BarInterval::Seconds(n) => (n.to_string(), "second"),
            BarInterval::Minutes(n) => (n.to_string(), "minute"),
            BarInterval::Hours(n) => (n.to_string(), "hour"),
            BarInterval::Days(n) => (n.to_string(), "day"),
        };
        Ok((multiplier, timespan.to_string()))
    }

    fn aggs_to_record_batch(aggs: &[MassiveAgg]) -> Result<RecordBatch> {
        if aggs.is_empty() {
            return Ok(RecordBatch::new_empty(bars_1m_schema()));
        }

        let row_count = aggs.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, agg) in aggs.iter().enumerate() {
            timestamps.push(agg.t.checked_mul(NANOS_PER_MILLISECOND).ok_or_else(|| {
                BtError::Data(format!("massive agg timestamp overflow at index {idx}"))
            })?);
            symbol_ids.push(0_u32);
            open.push(agg.o);
            high.push(agg.h);
            low.push(agg.l);
            close.push(agg.c);
            volume.push(agg.v);
            vwap.push(agg.vw.unwrap_or((agg.h + agg.l + agg.c) / 3.0));
            buy_volume.push(None);
            sell_volume.push(None);
            trade_count.push(agg.n.unwrap_or(0));
            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 trades_to_record_batch(symbol: &str, trades: &[MassiveTrade]) -> 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.sip_timestamp);
            received_at.push(trade.sip_timestamp);
            providers.push("massive".to_string());
            symbols.push(symbol.to_string());
            symbol_ids.push(0_u32);
            prices.push(trade.price);
            quantities.push(trade.size);
            sides.push(0_u8);
            trade_ids.push(trade.id.clone().unwrap_or_default());
            conditions.push(trade.conditions.as_ref().map(|c| {
                c.iter()
                    .map(|v| v.to_string())
                    .collect::<Vec<_>>()
                    .join(",")
            }));
        }

        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)?)
    }
}

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

    fn asset_classes(&self) -> &[AssetClass] {
        static ASSET_CLASSES: [AssetClass; 6] = [
            AssetClass::Equity,
            AssetClass::Index,
            AssetClass::Future,
            AssetClass::EquityOption,
            AssetClass::Forex,
            AssetClass::CryptoSpot,
        ];
        &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_ns, end_ns) = (range.start.0.to_string(), range.end.0.to_string());
        let payload = self
            .get_json::<MassiveTradesResponse>(
                &format!("/v3/trades/{symbol}"),
                &[
                    ("timestamp.gte", start_ns),
                    ("timestamp.lte", end_ns),
                    ("limit", "50000".to_string()),
                    ("order", "asc".to_string()),
                ],
            )
            .await?;

        let batch = Self::trades_to_record_batch(symbol, &payload.results.unwrap_or_default())?;
        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 (multiplier, timespan) = Self::interval_to_massive(interval)?;
        let (from, to) = Self::range_to_dates(range);
        let path = format!("/v2/aggs/ticker/{symbol}/range/{multiplier}/{timespan}/{from}/{to}");
        let payload = self
            .get_json::<MassiveAggsResponse>(
                &path,
                &[
                    ("adjusted", "true".to_string()),
                    ("sort", "asc".to_string()),
                    ("limit", "50000".to_string()),
                ],
            )
            .await?;

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

    async fn fetch_funding_rates(
        &self,
        _symbol: &str,
        _range: TimeRange,
    ) -> Result<Option<RecordBatch>> {
        Ok(None)
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name()))]
    async fn list_symbols(&self) -> Result<Vec<ProviderSymbol>> {
        let payload = self
            .get_json::<MassiveTickersResponse>(
                "/v3/reference/tickers",
                &[
                    ("active", "true".to_string()),
                    ("limit", "1000".to_string()),
                ],
            )
            .await?;

        let symbols = payload
            .results
            .unwrap_or_default()
            .into_iter()
            .map(|t| {
                let asset_class = match t.market.as_deref() {
                    Some("stocks") => AssetClass::Equity,
                    Some("crypto") => AssetClass::CryptoSpot,
                    Some("fx") | Some("forex") => AssetClass::Forex,
                    Some("options") => AssetClass::EquityOption,
                    Some("futures") => AssetClass::Future,
                    Some("indices") => AssetClass::Index,
                    Some("otc") => AssetClass::Equity,
                    _ => AssetClass::Custom,
                };
                ProviderSymbol {
                    symbol: t.ticker,
                    base_asset: t.name.unwrap_or_default(),
                    quote_asset: t.currency_name.unwrap_or_else(|| "USD".to_string()),
                    status: if t.active.unwrap_or(true) {
                        "active".to_string()
                    } else {
                        "inactive".to_string()
                    },
                    asset_class,
                }
            })
            .collect::<Vec<_>>();

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

    fn rate_limits(&self) -> RateLimitInfo {
        RateLimitInfo {
            requests_per_minute: None,
            burst: None,
        }
    }
}

// ── Massive API response types (Polygon-compatible) ─────────────────────────

#[derive(Debug, Deserialize)]
struct MassiveAggsResponse {
    #[serde(default)]
    results: Option<Vec<MassiveAgg>>,
}

#[derive(Debug, Deserialize)]
struct MassiveAgg {
    /// Open
    o: f64,
    /// High
    h: f64,
    /// Low
    l: f64,
    /// Close
    c: f64,
    /// Volume
    v: f64,
    /// Volume-weighted average price
    vw: Option<f64>,
    /// Timestamp (Unix ms)
    t: i64,
    /// Number of transactions
    n: Option<u32>,
}

#[derive(Debug, Deserialize)]
struct MassiveTradesResponse {
    #[serde(default)]
    results: Option<Vec<MassiveTrade>>,
}

#[derive(Debug, Deserialize)]
struct MassiveTrade {
    /// Trade price
    price: f64,
    /// Trade size
    size: f64,
    /// SIP timestamp (nanoseconds)
    sip_timestamp: i64,
    /// Trade ID
    id: Option<String>,
    /// Condition codes
    conditions: Option<Vec<i32>>,
}

#[derive(Debug, Deserialize)]
struct MassiveTickersResponse {
    #[serde(default)]
    results: Option<Vec<MassiveTicker>>,
}

#[derive(Debug, Deserialize)]
struct MassiveTicker {
    ticker: String,
    name: Option<String>,
    market: Option<String>,
    active: Option<bool>,
    currency_name: Option<String>,
}

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

    use super::*;

    #[test]
    fn aggs_to_record_batch_maps_ohlcv_and_vwap() {
        let aggs = vec![MassiveAgg {
            o: 130.28,
            h: 131.0,
            l: 129.5,
            c: 130.75,
            v: 98453126.0,
            vw: Some(130.62),
            t: 1_626_912_000_000,
            n: Some(125329),
        }];

        let batch = MassiveProvider::aggs_to_record_batch(&aggs).expect("convert aggs");
        assert_eq!(batch.num_rows(), 1);

        let vwap = batch
            .column_by_name("vwap")
            .expect("vwap")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("f64");
        assert_eq!(vwap.value(0), 130.62);

        let close = batch
            .column_by_name("close")
            .expect("close")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("f64");
        assert_eq!(close.value(0), 130.75);
    }

    #[test]
    fn aggs_to_record_batch_fallback_vwap() {
        let aggs = vec![MassiveAgg {
            o: 100.0,
            h: 105.0,
            l: 95.0,
            c: 102.0,
            v: 1000.0,
            vw: None,
            t: 1_626_912_000_000,
            n: None,
        }];

        let batch = MassiveProvider::aggs_to_record_batch(&aggs).expect("convert");
        let vwap = batch
            .column_by_name("vwap")
            .expect("vwap")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("f64");
        assert!((vwap.value(0) - 100.666_666_666_666_67).abs() < 0.001);
    }

    #[test]
    fn interval_conversion_accepts_all_variants() {
        let (m, t) = MassiveProvider::interval_to_massive(BarInterval::Minutes(5)).expect("5m");
        assert_eq!(m, "5");
        assert_eq!(t, "minute");

        let (m, t) = MassiveProvider::interval_to_massive(BarInterval::Days(1)).expect("1d");
        assert_eq!(m, "1");
        assert_eq!(t, "day");
    }

    #[test]
    fn trades_to_record_batch_maps_fields() {
        let trades = vec![MassiveTrade {
            price: 171.55,
            size: 100.0,
            sip_timestamp: 1_517_562_000_016_036_600,
            id: Some("52983525029461".to_string()),
            conditions: Some(vec![12, 41]),
        }];

        let batch =
            MassiveProvider::trades_to_record_batch("AAPL", &trades).expect("convert trades");
        assert_eq!(batch.num_rows(), 1);

        let prices = batch
            .column_by_name("price")
            .expect("price")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("f64");
        assert_eq!(prices.value(0), 171.55);
    }

    #[test]
    fn empty_aggs_returns_empty_batch() {
        let batch = MassiveProvider::aggs_to_record_batch(&[]).expect("empty");
        assert_eq!(batch.num_rows(), 0);
    }

    #[test]
    fn new_requires_pro_license() {
        let result = MassiveProvider::new("fake_key");
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("data_connectors"),
            "expected license gate error, got: {err}"
        );
    }
}
