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 FIXED_PRICE_SCALE: f64 = 1_000_000_000.0;
const DEFAULT_BASE_URL: &str = "https://hist.databento.com/v0";

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

impl DatabentoProvider {
    /// Create a new provider for a specific dataset (e.g. `"GLBX.MDP3"` for CME futures).
    /// Requires a Pro license (`data_connectors` feature).
    pub fn new(api_key: impl Into<String>, dataset: 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(),
            dataset: dataset.into(),
        })
    }

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

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

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

        let response = self
            .client
            .get(&url)
            .basic_auth(&self.api_key, Option::<&str>::None)
            .query(query)
            .send()
            .await
            .map_err(|err| BtError::Data(format!("databento request failed: {err}")))?;

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

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

        // Databento returns newline-delimited JSON for timeseries data
        let mut records = Vec::new();
        for line in payload.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let record: T = serde_json::from_str(line)
                .map_err(|err| BtError::Data(format!("databento json decode failed: {err}")))?;
            records.push(record);
        }
        Ok(records)
    }

    fn range_to_iso(range: TimeRange) -> (String, String) {
        // Databento accepts Unix nanos as strings
        (range.start.0.to_string(), range.end.0.to_string())
    }

    fn interval_to_schema(interval: BarInterval) -> Result<String> {
        let schema = match interval {
            BarInterval::Seconds(1) => "ohlcv-1s",
            BarInterval::Minutes(1) => "ohlcv-1m",
            BarInterval::Hours(1) => "ohlcv-1h",
            BarInterval::Days(1) => "ohlcv-1d",
            _ => {
                return Err(BtError::Data(format!(
                    "unsupported databento interval: {interval:?}. Supported: 1s, 1m, 1h, 1d"
                )))
            }
        };
        Ok(schema.to_string())
    }

    fn ohlcv_to_record_batch(records: &[DbOhlcv]) -> Result<RecordBatch> {
        if records.is_empty() {
            return Ok(RecordBatch::new_empty(bars_1m_schema()));
        }

        let row_count = records.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 record in records {
            let ts = record.hd.as_ref().map(|h| h.ts_event).unwrap_or(0);

            let o = Self::parse_fixed_price(&record.open)?;
            let h = Self::parse_fixed_price(&record.high)?;
            let l = Self::parse_fixed_price(&record.low)?;
            let c = Self::parse_fixed_price(&record.close)?;
            let v = record.volume.parse::<f64>().map_err(|e| {
                BtError::Data(format!("databento invalid volume '{}': {e}", record.volume))
            })?;

            timestamps.push(ts);
            symbol_ids.push(record.hd.as_ref().map(|h| h.instrument_id).unwrap_or(0));
            open.push(o);
            high.push(h);
            low.push(l);
            close.push(c);
            volume.push(v);
            vwap.push((h + l + c) / 3.0);
            buy_volume.push(None);
            sell_volume.push(None);
            trade_count.push(0_u32);
            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, records: &[DbTrade]) -> Result<RecordBatch> {
        if records.is_empty() {
            return Ok(RecordBatch::new_empty(raw_trades_schema()));
        }

        let row_count = records.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 records {
            let ts = trade.hd.as_ref().map(|h| h.ts_event).unwrap_or(0);

            timestamps.push(ts);
            received_at.push(trade.ts_recv.unwrap_or(ts));
            providers.push("databento".to_string());
            symbols.push(symbol.to_string());
            symbol_ids.push(trade.hd.as_ref().map(|h| h.instrument_id).unwrap_or(0));
            prices.push(Self::parse_fixed_price(&trade.price)?);
            quantities.push(trade.size as f64);
            sides.push(match trade.side.as_deref() {
                Some("B") => 1_u8,
                Some("A") => 2_u8,
                _ => 0_u8,
            });
            trade_ids.push(trade.sequence.map(|s| s.to_string()).unwrap_or_default());
            conditions.push(trade.action.clone());
        }

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

    /// Parse a Databento fixed-point price string (1 unit = 1e-9).
    fn parse_fixed_price(value: &str) -> Result<f64> {
        let raw = value
            .parse::<i64>()
            .map_err(|e| BtError::Data(format!("databento invalid price '{value}': {e}")))?;
        Ok(raw as f64 / FIXED_PRICE_SCALE)
    }
}

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

    fn asset_classes(&self) -> &[AssetClass] {
        // Depends on dataset; GLBX.MDP3 = futures, XNAS.ITCH = equities, etc.
        static ASSET_CLASSES: [AssetClass; 4] = [
            AssetClass::Equity,
            AssetClass::Future,
            AssetClass::EquityOption,
            AssetClass::Index,
        ];
        &ASSET_CLASSES
    }

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name(), dataset = %self.dataset))]
    async fn fetch_trades(&self, symbol: &str, range: TimeRange) -> Result<RecordBatch> {
        let (start, end) = Self::range_to_iso(range);
        let records = self
            .get_json_lines::<DbTrade>(
                "/timeseries.get_range",
                &[
                    ("dataset", self.dataset.clone()),
                    ("symbols", symbol.to_string()),
                    ("schema", "trades".to_string()),
                    ("start", start),
                    ("end", end),
                    ("encoding", "json".to_string()),
                ],
            )
            .await?;

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

    #[tracing::instrument(level = "debug", skip(self), fields(provider = self.name(), dataset = %self.dataset))]
    async fn fetch_bars(
        &self,
        symbol: &str,
        range: TimeRange,
        interval: BarInterval,
    ) -> Result<Option<RecordBatch>> {
        let schema = Self::interval_to_schema(interval)?;
        let (start, end) = Self::range_to_iso(range);
        let records = self
            .get_json_lines::<DbOhlcv>(
                "/timeseries.get_range",
                &[
                    ("dataset", self.dataset.clone()),
                    ("symbols", symbol.to_string()),
                    ("schema", schema),
                    ("start", start),
                    ("end", end),
                    ("encoding", "json".to_string()),
                ],
            )
            .await?;

        let batch = Self::ohlcv_to_record_batch(&records)?;
        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>> {
        // Databento doesn't have a symbol list per se — return empty.
        // Users should know their dataset symbols (e.g. "ESH4", "NQH4").
        Ok(Vec::new())
    }

    fn rate_limits(&self) -> RateLimitInfo {
        // Databento has no documented rate limits for the historical API
        RateLimitInfo {
            requests_per_minute: None,
            burst: None,
        }
    }
}

// ── Databento API response types ────────────────────────────────────────────

#[derive(Debug, Deserialize)]
struct DbHeader {
    /// Timestamp in nanoseconds
    #[serde(deserialize_with = "deserialize_i64_from_string")]
    ts_event: i64,
    #[serde(default)]
    instrument_id: u32,
}

#[derive(Debug, Deserialize)]
struct DbOhlcv {
    #[serde(default)]
    hd: Option<DbHeader>,
    /// Fixed-point price string (1 unit = 1e-9)
    open: String,
    high: String,
    low: String,
    close: String,
    volume: String,
}

#[derive(Debug, Deserialize)]
struct DbTrade {
    #[serde(default)]
    hd: Option<DbHeader>,
    /// Fixed-point price string
    price: String,
    /// Quantity
    #[serde(default)]
    size: u32,
    /// "B" (bid/buy), "A" (ask/sell), "N" (none)
    side: Option<String>,
    /// "T" (trade), "A" (add), etc.
    action: Option<String>,
    /// Receive timestamp (nanos)
    ts_recv: Option<i64>,
    /// Sequence number
    sequence: Option<u64>,
}

/// Databento sends i64 fields as quoted strings in JSON encoding.
fn deserialize_i64_from_string<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    s.parse::<i64>().map_err(serde::de::Error::custom)
}

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

    use super::*;

    #[test]
    fn parse_fixed_price_scales_correctly() {
        // 472600000000 / 1e9 = 472.6
        let price = DatabentoProvider::parse_fixed_price("472600000000").expect("parse");
        assert!((price - 472.6).abs() < 0.0001);
    }

    #[test]
    fn ohlcv_to_record_batch_maps_fields() {
        let records = vec![DbOhlcv {
            hd: Some(DbHeader {
                ts_event: 1_702_987_922_000_000_000,
                instrument_id: 15144,
            }),
            open: "472600000000".to_string(),
            high: "473000000000".to_string(),
            low: "472000000000".to_string(),
            close: "472800000000".to_string(),
            volume: "300".to_string(),
        }];

        let batch = DatabentoProvider::ohlcv_to_record_batch(&records).expect("convert");
        assert_eq!(batch.num_rows(), 1);

        let close = batch
            .column_by_name("close")
            .expect("close")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("f64");
        assert!((close.value(0) - 472.8).abs() < 0.0001);
    }

    #[test]
    fn trades_to_record_batch_maps_side() {
        let records = vec![
            DbTrade {
                hd: Some(DbHeader {
                    ts_event: 1_702_987_922_000_000_000,
                    instrument_id: 15144,
                }),
                price: "472600000000".to_string(),
                size: 1,
                side: Some("B".to_string()),
                action: Some("T".to_string()),
                ts_recv: Some(1_702_987_922_000_100_000),
                sequence: Some(123456),
            },
            DbTrade {
                hd: Some(DbHeader {
                    ts_event: 1_702_987_923_000_000_000,
                    instrument_id: 15144,
                }),
                price: "472700000000".to_string(),
                size: 2,
                side: Some("A".to_string()),
                action: Some("T".to_string()),
                ts_recv: None,
                sequence: None,
            },
        ];

        let batch = DatabentoProvider::trades_to_record_batch("ESH4", &records).expect("convert");
        assert_eq!(batch.num_rows(), 2);

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

    #[test]
    fn interval_conversion_handles_supported_and_rejects_unsupported() {
        assert_eq!(
            DatabentoProvider::interval_to_schema(BarInterval::Minutes(1)).expect("1m"),
            "ohlcv-1m"
        );
        assert_eq!(
            DatabentoProvider::interval_to_schema(BarInterval::Days(1)).expect("1d"),
            "ohlcv-1d"
        );
        assert!(DatabentoProvider::interval_to_schema(BarInterval::Minutes(5)).is_err());
    }

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

    #[test]
    fn new_requires_pro_license() {
        // Without a Pro license activated, new() should fail with Unsupported.
        let result = DatabentoProvider::new("fake_key", "GLBX.MDP3");
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("data_connectors"),
            "expected license gate error, got: {err}"
        );
    }
}
