//! Historical data fetching for Polymarket and Kalshi.
//!
//! Uses reqwest + tokio (one-shot runtime) matching existing exchange client patterns.

use crate::orderbook::OrderbookSnapshot;
use pyo3::prelude::*;
use std::time::Duration;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// A historical trade record.
#[pyclass]
#[derive(Debug, Clone)]
pub struct HistoricalTrade {
    #[pyo3(get)]
    pub timestamp: f64,
    #[pyo3(get)]
    pub price: f64,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub side: String,
    #[pyo3(get)]
    pub market_id: String,
}

#[pymethods]
impl HistoricalTrade {
    fn __repr__(&self) -> String {
        format!(
            "HistoricalTrade(ts={:.0}, price={:.4}, size={:.1}, side='{}')",
            self.timestamp, self.price, self.size, self.side
        )
    }
}

/// An OHLC bar.
#[pyclass]
#[derive(Debug, Clone)]
pub struct OHLCBar {
    #[pyo3(get)]
    pub timestamp: f64,
    #[pyo3(get)]
    pub open: f64,
    #[pyo3(get)]
    pub high: f64,
    #[pyo3(get)]
    pub low: f64,
    #[pyo3(get)]
    pub close: f64,
    #[pyo3(get)]
    pub volume: f64,
}

#[pymethods]
impl OHLCBar {
    fn __repr__(&self) -> String {
        format!(
            "OHLCBar(ts={:.0}, o={:.4}, h={:.4}, l={:.4}, c={:.4}, v={:.0})",
            self.timestamp, self.open, self.high, self.low, self.close, self.volume
        )
    }
}

// ---------------------------------------------------------------------------
// Internal async helpers
// ---------------------------------------------------------------------------

/// Create a one-shot tokio runtime for blocking data fetches.
fn oneshot_runtime() -> PyResult<tokio::runtime::Runtime> {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!(
                "failed to create runtime: {}",
                e
            ))
        })
}

fn http_client() -> Result<reqwest::Client, String> {
    reqwest::Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .map_err(|e| format!("failed to build HTTP client: {}", e))
}

/// Validate that a user-supplied ID contains only safe URL characters.
fn validate_id(id: &str, label: &str) -> Result<(), String> {
    if id.is_empty() {
        return Err(format!("{} must not be empty", label));
    }
    // Allow alphanumeric, hyphens, underscores, dots (safe for URL path/query values)
    if !id
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
    {
        return Err(format!(
            "{} contains invalid characters (only alphanumeric, -, _, . allowed)",
            label
        ));
    }
    Ok(())
}

async fn fetch_polymarket_prices_async(
    token_id: &str,
    start_ts: f64,
    end_ts: f64,
    fidelity: u64,
) -> Result<Vec<OHLCBar>, String> {
    validate_id(token_id, "token_id")?;

    let url = format!(
        "https://clob.polymarket.com/prices-history?market={}&startTs={}&endTs={}&fidelity={}",
        token_id,
        start_ts as u64,
        end_ts as u64,
        fidelity
    );

    let client = http_client()?;
    let resp = client
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|e| format!("request failed: {}", e))?;

    if !resp.status().is_success() {
        return Err(format!("HTTP {} for prices-history", resp.status()));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("JSON parse failed: {}", e))?;

    // Polymarket returns {"history": [{"t": timestamp, "p": price}, ...]}
    let history = body
        .get("history")
        .and_then(|h| h.as_array())
        .ok_or_else(|| "missing 'history' array in response".to_string())?;

    let mut bars = Vec::with_capacity(history.len());
    for entry in history {
        let t = entry
            .get("t")
            .and_then(|v| v.as_f64())
            .unwrap_or(0.0);
        let p = entry
            .get("p")
            .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
            .unwrap_or(0.0);

        bars.push(OHLCBar {
            timestamp: t,
            open: p,
            high: p,
            low: p,
            close: p,
            volume: 0.0,
        });
    }

    Ok(bars)
}

async fn fetch_polymarket_trades_async(
    token_id: &str,
    limit: u32,
) -> Result<Vec<HistoricalTrade>, String> {
    validate_id(token_id, "token_id")?;

    let url = format!(
        "https://clob.polymarket.com/trades?asset_id={}&limit={}",
        token_id,
        limit.min(500) // API caps per request
    );

    let client = http_client()?;
    let mut all_trades = Vec::new();
    let mut next_cursor: Option<String> = None;
    let max_pages = (limit as usize + 499) / 500;

    for _ in 0..max_pages {
        let req_url = match &next_cursor {
            Some(cursor) => {
                // Validate cursor to prevent URL injection
                let safe_cursor: String = cursor
                    .chars()
                    .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '=' || *c == '.')
                    .collect();
                format!("{}&cursor={}", url, safe_cursor)
            }
            None => url.clone(),
        };

        let resp = client
            .get(&req_url)
            .header("Accept", "application/json")
            .send()
            .await
            .map_err(|e| format!("request failed: {}", e))?;

        if !resp.status().is_success() {
            return Err(format!("HTTP {}", resp.status()));
        }

        let body: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| format!("JSON parse failed: {}", e))?;

        if let Some(trades) = body.as_array() {
            for trade in trades {
                let ts_str = trade.get("match_time").and_then(|v| v.as_str()).unwrap_or("0");
                let timestamp = ts_str.parse::<f64>().unwrap_or(0.0);
                let price = trade
                    .get("price")
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                    .unwrap_or(0.0);
                let size = trade
                    .get("size")
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                    .unwrap_or(0.0);
                let side = trade
                    .get("side")
                    .and_then(|v| v.as_str())
                    .unwrap_or("buy")
                    .to_string();

                all_trades.push(HistoricalTrade {
                    timestamp,
                    price,
                    size,
                    side,
                    market_id: token_id.to_string(),
                });
            }

            if trades.len() < 500 {
                break; // No more pages
            }
        } else {
            // Response might be {"data": [...], "next_cursor": "..."}
            if let Some(data) = body.get("data").and_then(|v| v.as_array()) {
                for trade in data {
                    let timestamp = trade.get("match_time")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                        .unwrap_or(0.0);
                    let price = trade
                        .get("price")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                        .unwrap_or(0.0);
                    let size = trade
                        .get("size")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                        .unwrap_or(0.0);
                    let side = trade
                        .get("side")
                        .and_then(|v| v.as_str())
                        .unwrap_or("buy")
                        .to_string();

                    all_trades.push(HistoricalTrade {
                        timestamp,
                        price,
                        size,
                        side,
                        market_id: token_id.to_string(),
                    });
                }

                next_cursor = body.get("next_cursor").and_then(|v| v.as_str()).map(String::from);
                if next_cursor.is_none() || data.len() < 500 {
                    break;
                }
            } else {
                break;
            }
        }

        if all_trades.len() >= limit as usize {
            break;
        }
    }

    all_trades.truncate(limit as usize);
    Ok(all_trades)
}

async fn fetch_polymarket_book_async(token_id: &str) -> Result<OrderbookSnapshot, String> {
    validate_id(token_id, "token_id")?;

    let url = format!(
        "https://clob.polymarket.com/book?token_id={}",
        token_id
    );

    let client = http_client()?;
    let resp = client
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|e| format!("request failed: {}", e))?;

    if !resp.status().is_success() {
        return Err(format!("HTTP {} for polymarket book", resp.status()));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("JSON parse failed: {}", e))?;

    let parse_levels = |key: &str| -> Vec<(f64, f64)> {
        body.get(key)
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|level| {
                        let price = level
                            .get("price")
                            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))?;
                        let size = level
                            .get("size")
                            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))?;
                        Some((price, size))
                    })
                    .collect()
            })
            .unwrap_or_default()
    };

    let bids = parse_levels("bids");
    let asks = parse_levels("asks");

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64();

    Ok(OrderbookSnapshot::from_levels(
        bids,
        asks,
        now,
        "polymarket".to_string(),
    ))
}

async fn fetch_kalshi_market_async(ticker: &str) -> Result<serde_json::Value, String> {
    validate_id(ticker, "ticker")?;

    let url = format!(
        "https://api.elections.kalshi.com/trade-api/v2/markets/{}",
        ticker
    );

    let client = http_client()?;
    let resp = client
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|e| format!("request failed: {}", e))?;

    if !resp.status().is_success() {
        return Err(format!("HTTP {} for kalshi market", resp.status()));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("JSON parse failed: {}", e))?;

    // API returns {"market": {...}} or just {...}
    Ok(body.get("market").cloned().unwrap_or(body))
}

async fn fetch_kalshi_book_async(ticker: &str) -> Result<OrderbookSnapshot, String> {
    validate_id(ticker, "ticker")?;

    let url = format!(
        "https://api.elections.kalshi.com/trade-api/v2/markets/{}/orderbook",
        ticker
    );

    let client = http_client()?;
    let resp = client
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|e| format!("request failed: {}", e))?;

    if !resp.status().is_success() {
        return Err(format!("HTTP {} for orderbook", resp.status()));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("JSON parse failed: {}", e))?;

    let orderbook = body
        .get("orderbook")
        .ok_or_else(|| "missing 'orderbook' in response".to_string())?;

    let parse_levels = |key: &str| -> Vec<(f64, f64)> {
        orderbook
            .get(key)
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|level| {
                        let price = level.get(0).and_then(|v| v.as_f64())?;
                        let size = level.get(1).and_then(|v| v.as_f64())?;
                        // Kalshi prices are in cents (1-99), normalize to 0-1
                        Some((price / 100.0, size))
                    })
                    .collect()
            })
            .unwrap_or_default()
    };

    let mut bids = parse_levels("yes");
    let asks = parse_levels("no");

    // Bids should be sorted descending
    bids.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
    // Asks should be sorted ascending (no prices, so 1-p ascending means p descending)

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64();

    Ok(OrderbookSnapshot::from_levels(
        bids,
        asks,
        now,
        "kalshi".to_string(),
    ))
}

// ---------------------------------------------------------------------------
// Python-facing functions
// ---------------------------------------------------------------------------

/// Fetch Polymarket OHLC price history.
///
/// GET https://clob.polymarket.com/prices-history?market={token_id}&startTs={start}&endTs={end}&fidelity={fidelity}
#[pyfunction]
#[pyo3(signature = (token_id, start_ts, end_ts, fidelity=60))]
fn fetch_polymarket_prices(
    token_id: String,
    start_ts: f64,
    end_ts: f64,
    fidelity: u64,
) -> PyResult<Vec<OHLCBar>> {
    let rt = oneshot_runtime()?;
    rt.block_on(fetch_polymarket_prices_async(&token_id, start_ts, end_ts, fidelity))
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}

/// Fetch Polymarket trade history.
///
/// GET https://clob.polymarket.com/trades?asset_id={token_id}
#[pyfunction]
#[pyo3(signature = (token_id, limit=1000))]
fn fetch_polymarket_trades(token_id: String, limit: u32) -> PyResult<Vec<HistoricalTrade>> {
    let rt = oneshot_runtime()?;
    rt.block_on(fetch_polymarket_trades_async(&token_id, limit))
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}

/// Fetch a single Kalshi orderbook snapshot.
///
/// GET https://trading-api.kalshi.com/trade-api/v2/markets/{ticker}/orderbook
#[pyfunction]
fn fetch_kalshi_book(ticker: String) -> PyResult<OrderbookSnapshot> {
    let rt = oneshot_runtime()?;
    rt.block_on(fetch_kalshi_book_async(&ticker))
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}

/// Fetch Polymarket orderbook snapshot.
///
/// GET https://clob.polymarket.com/book?token_id={token_id}
#[pyfunction]
fn fetch_polymarket_book(token_id: String) -> PyResult<OrderbookSnapshot> {
    let rt = oneshot_runtime()?;
    rt.block_on(fetch_polymarket_book_async(&token_id))
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}

/// Fetch a single Kalshi market metadata.
///
/// GET https://trading-api.kalshi.com/trade-api/v2/markets/{ticker}
///
/// Returns raw JSON as a string (parse with json.loads in Python).
#[pyfunction]
fn fetch_kalshi_market(ticker: String) -> PyResult<String> {
    let rt = oneshot_runtime()?;
    let val = rt
        .block_on(fetch_kalshi_market_async(&ticker))
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))?;

    serde_json::to_string(&val)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("JSON serialize failed: {}", e)))
}

/// Register data fetcher types and functions in the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<HistoricalTrade>()?;
    m.add_class::<OHLCBar>()?;
    m.add_function(wrap_pyfunction!(fetch_polymarket_prices, m)?)?;
    m.add_function(wrap_pyfunction!(fetch_polymarket_trades, m)?)?;
    m.add_function(wrap_pyfunction!(fetch_kalshi_book, m)?)?;
    m.add_function(wrap_pyfunction!(fetch_polymarket_book, m)?)?;
    m.add_function(wrap_pyfunction!(fetch_kalshi_market, m)?)?;
    Ok(())
}
