//! Robinhood Crypto REST exchange client.
//!
//! Implements the Exchange trait for Robinhood's Crypto Trading API.
//! Supports crypto trading via Bearer token auth.
//! Includes fill polling and order management.

use crate::error::HorizonError;
use crate::exchanges::Exchange;
use crate::types::{Fill, OrderRequest, OrderSide, Position, Side};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, warn};
use zeroize::Zeroizing;

const API_URL: &str = "https://trading.robinhood.com/api/v1";

/// Validate that a URL path parameter contains only safe characters.
fn validate_url_param(param: &str, name: &str) -> Result<(), HorizonError> {
    if param.is_empty() {
        return Err(HorizonError::Exchange(format!(
            "robinhood: {} is empty",
            name
        )));
    }
    if !param
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    {
        return Err(HorizonError::Exchange(format!(
            "robinhood: invalid characters in {}",
            name
        )));
    }
    Ok(())
}

/// Parse an ISO 8601 timestamp string to Unix seconds.
/// Handles formats like "2024-01-15T12:00:00Z" and "2024-01-15T12:00:00.123Z".
/// Also handles timezone offsets like "+00:00" by stripping them (assumes UTC).
fn parse_iso_timestamp(s: &str) -> f64 {
    // Try numeric first
    if let Ok(v) = s.parse::<f64>() {
        return v;
    }
    let s = s.trim();
    let s = if s.ends_with('Z') {
        &s[..s.len() - 1]
    } else if s.len() > 6 {
        let tail = &s[s.len() - 6..];
        if (tail.starts_with('+') || tail.starts_with('-'))
            && tail.as_bytes()[3] == b':'
        {
            &s[..s.len() - 6]
        } else {
            s
        }
    } else {
        s
    };

    let (date_part, time_part) = match s.split_once('T') {
        Some(parts) => parts,
        None => return 0.0,
    };
    let mut date_iter = date_part.split('-');
    let year: i64 = match date_iter.next().and_then(|v| v.parse().ok()) {
        Some(v) => v,
        None => return 0.0,
    };
    let month: i64 = match date_iter.next().and_then(|v| v.parse().ok()) {
        Some(v) => v,
        None => return 0.0,
    };
    let day: i64 = match date_iter.next().and_then(|v| v.parse().ok()) {
        Some(v) => v,
        None => return 0.0,
    };

    let (time_whole, frac) = if let Some((w, f)) = time_part.split_once('.') {
        let frac_secs: f64 = format!("0.{}", f).parse().unwrap_or(0.0);
        (w, frac_secs)
    } else {
        (time_part, 0.0)
    };

    let mut time_iter = time_whole.split(':');
    let hour: i64 = time_iter.next().and_then(|v| v.parse().ok()).unwrap_or(0);
    let min: i64 = time_iter.next().and_then(|v| v.parse().ok()).unwrap_or(0);
    let sec: i64 = time_iter.next().and_then(|v| v.parse().ok()).unwrap_or(0);

    if month < 1 || month > 12 || day < 1 || day > 31 {
        return 0.0;
    }
    if hour > 23 || min > 59 || sec > 59 {
        return 0.0;
    }

    let is_leap = |y: i64| (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
    let mut days: i64 = 0;
    for y in 1970..year {
        days += if is_leap(y) { 366 } else { 365 };
    }
    let month_days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    for m in 1..month {
        days += month_days[m as usize];
        if m == 2 && is_leap(year) {
            days += 1;
        }
    }
    days += day - 1;

    let total_secs = days * 86400 + hour * 3600 + min * 60 + sec;
    total_secs as f64 + frac
}

pub struct RobinhoodExchange {
    api_url: String,
    api_key: Zeroizing<String>,
    client: reqwest::Client,
    runtime: Arc<tokio::runtime::Runtime>,
    fills: Mutex<Vec<Fill>>,
    next_fill_id: Mutex<u64>,
    last_fill_cursor: Mutex<Option<String>>,
    /// Tracks open order IDs per market (symbol) for per-market cancellation.
    open_orders: Mutex<HashMap<String, Vec<String>>>,
    /// Seen fill IDs for deduplication.
    seen_fill_ids: Mutex<HashSet<String>>,
    /// FIFO order for seen fill IDs (oldest first for eviction).
    seen_fill_order: Mutex<VecDeque<String>>,
    /// Maximum fill timestamp seen so far (for filtering already-processed fills).
    last_fill_ts: Mutex<f64>,
}

impl RobinhoodExchange {
    pub fn new(
        api_key: String,
        api_url: Option<String>,
        runtime: Arc<tokio::runtime::Runtime>,
    ) -> Self {
        Self {
            api_url: api_url.unwrap_or_else(|| API_URL.to_string()),
            api_key: Zeroizing::new(api_key),
            client: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(10))
                .connect_timeout(std::time::Duration::from_secs(5))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            runtime,
            fills: Mutex::new(Vec::new()),
            next_fill_id: Mutex::new(1),
            last_fill_cursor: Mutex::new(None),
            open_orders: Mutex::new(HashMap::new()),
            seen_fill_ids: Mutex::new(HashSet::new()),
            seen_fill_order: Mutex::new(VecDeque::new()),
            last_fill_ts: Mutex::new(0.0),
        }
    }

    /// Add Robinhood auth headers to a request builder.
    fn auth_headers(
        &self,
        builder: reqwest::RequestBuilder,
    ) -> reqwest::RequestBuilder {
        builder
            .header("Authorization", format!("Bearer {}", &*self.api_key))
            .header("Content-Type", "application/json")
    }

    /// Submit order to Robinhood with retry on 429.
    async fn submit_order_async(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let url = format!("{}/crypto/trading/orders/", self.api_url);

        if req.size <= 0.0 || req.size.is_nan() || req.size.is_infinite() {
            return Err(HorizonError::Exchange(format!(
                "invalid size for Robinhood: {}",
                req.size
            )));
        }
        if req.price <= 0.0 || req.price.is_nan() || req.price.is_infinite() {
            return Err(HorizonError::Exchange(format!(
                "invalid price for Robinhood: {}",
                req.price
            )));
        }

        let side_str = match req.order_side {
            OrderSide::Buy => "buy",
            OrderSide::Sell => "sell",
        };

        let client_order_id = uuid::Uuid::new_v4().to_string();

        let body = serde_json::json!({
            "client_order_id": client_order_id,
            "side": side_str,
            "symbol": req.market_id,
            "type": "limit",
            "limit_price": format!("{:.8}", req.price),
            "quantity": format!("{}", req.size),
            "time_in_force": "gtc",
        });

        debug!(
            symbol = %req.market_id,
            side = side_str,
            price = req.price,
            size = req.size,
            "robinhood: submitting order"
        );

        let mut backoff_ms: u64 = 1000;

        for attempt in 0..5u32 {
            let builder = self.client.post(&url).json(&body);
            let resp = self
                .auth_headers(builder)
                .send()
                .await
                .map_err(|e| HorizonError::Exchange(format!("robinhood order failed: {}", e)))?;

            let status_code = resp.status().as_u16();

            if status_code == 429 && attempt < 4 {
                warn!(
                    attempt = attempt + 1,
                    backoff_ms = backoff_ms,
                    "robinhood: rate limited (429), retrying"
                );
                tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)).await;
                backoff_ms = (backoff_ms * 2).min(30_000);
                continue;
            }

            if status_code == 401 || status_code == 403 {
                let body_text = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "robinhood: forbidden ({}) — check API key: {}",
                    status_code, body_text
                )));
            }

            if !resp.status().is_success() {
                let status = resp.status();
                let body_text = resp.text().await.unwrap_or_default();
                error!(status = %status, body = %body_text, "robinhood: order rejected");
                return Err(HorizonError::Exchange(format!(
                    "robinhood order rejected ({}): {}",
                    status, body_text
                )));
            }

            let json: serde_json::Value = resp
                .json()
                .await
                .map_err(|e| HorizonError::Exchange(format!("robinhood parse error: {}", e)))?;

            let order_id = json
                .get("id")
                .and_then(|v| v.as_str())
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string())
                .ok_or_else(|| {
                    HorizonError::Exchange(format!(
                        "robinhood: response missing order id: {}",
                        json
                    ))
                })?;

            info!(order_id = %order_id, "robinhood: order accepted");
            return Ok(order_id);
        }

        Err(HorizonError::Exchange(
            "robinhood: retry attempts exhausted".into(),
        ))
    }

    /// Cancel order on Robinhood.
    async fn cancel_order_async(&self, order_id: &str) -> Result<(), HorizonError> {
        validate_url_param(order_id, "order_id")?;
        let url = format!(
            "{}/crypto/trading/orders/{}/cancel/",
            self.api_url, order_id
        );

        let builder = self.client.post(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("robinhood cancel failed: {}", e)))?;

        if resp.status().as_u16() == 404 {
            // Order already filled or canceled
            debug!(order_id = %order_id, "robinhood: order not found (may be filled)");
            return Ok(());
        }

        if !resp.status().is_success() && resp.status().as_u16() != 204 {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(HorizonError::Exchange(format!(
                "robinhood cancel failed ({}): {}",
                status, body
            )));
        }

        debug!(order_id = %order_id, "robinhood: order canceled");
        Ok(())
    }

    /// Cancel all orders on Robinhood by listing open orders and canceling each.
    async fn cancel_all_async(&self) -> Result<usize, HorizonError> {
        let url = format!(
            "{}/crypto/trading/orders/?status=open",
            self.api_url
        );

        let builder = self.client.get(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("robinhood cancel-all list failed: {}", e))
            })?;

        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(HorizonError::Exchange(format!(
                "robinhood cancel-all list failed ({}): {}",
                status, body
            )));
        }

        let json: serde_json::Value =
            resp.json().await.unwrap_or_else(|_| serde_json::Value::Null);

        let order_ids: Vec<String> = json
            .get("results")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|o| o.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        let mut canceled = 0;
        for oid in &order_ids {
            match self.cancel_order_async(oid).await {
                Ok(()) => canceled += 1,
                Err(e) => {
                    debug!(
                        order_id = %oid,
                        error = %e,
                        "robinhood: cancel-all: individual cancel failed"
                    );
                }
            }
        }

        info!(count = canceled, "robinhood: canceled all orders");
        Ok(canceled)
    }

    /// Poll for filled orders from Robinhood.
    async fn poll_fills_async(&self) -> Result<Vec<Fill>, HorizonError> {
        let url = format!(
            "{}/crypto/trading/orders/?status=filled",
            self.api_url
        );

        let builder = self.client.get(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("robinhood: fill poll failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            debug!(status = %status, "robinhood: fill poll returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("robinhood: fill parse error: {}", e))
        })?;

        let mut new_fills = Vec::new();

        let last_ts = *self.last_fill_ts.lock().unwrap_or_else(|e| e.into_inner());
        let mut max_ts = last_ts;

        let results = json
            .get("results")
            .and_then(|v| v.as_array());

        if let Some(orders_arr) = results {
            let mut next_id = self.next_fill_id.lock().unwrap_or_else(|e| e.into_inner());

            for order_json in orders_arr {
                let order_id = order_json
                    .get("id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let client_order_id = order_json
                    .get("client_order_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let symbol = order_json
                    .get("symbol")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let filled_qty = order_json
                    .get("filled_quantity")
                    .and_then(|v| {
                        v.as_str()
                            .and_then(|s| s.parse::<f64>().ok())
                            .or_else(|| v.as_f64())
                    })
                    .unwrap_or(0.0);

                let filled_avg_price = order_json
                    .get("filled_avg_price")
                    .and_then(|v| {
                        v.as_str()
                            .and_then(|s| s.parse::<f64>().ok())
                            .or_else(|| v.as_f64())
                    })
                    .unwrap_or(0.0);

                let side_str = order_json
                    .get("side")
                    .and_then(|v| v.as_str())
                    .unwrap_or("buy");

                let order_side = if side_str.eq_ignore_ascii_case("buy") {
                    OrderSide::Buy
                } else {
                    OrderSide::Sell
                };

                let ts = order_json
                    .get("created_at")
                    .and_then(|v| v.as_str())
                    .map(parse_iso_timestamp)
                    .unwrap_or(0.0);

                // Validate: skip entries with non-positive or NaN/Inf filled quantity
                if filled_qty <= 0.0 || !filled_qty.is_finite() {
                    continue;
                }
                if !filled_avg_price.is_finite() || filled_avg_price <= 0.0 {
                    continue;
                }

                // Skip fills already seen based on timestamp (supplements dedup set)
                if ts > 0.0 && ts <= last_ts {
                    continue;
                }

                let fill_id = if order_id.is_empty() {
                    let id = format!("robinhood_fill_{}", *next_id);
                    *next_id += 1;
                    id
                } else {
                    order_id.clone()
                };

                // Dedup by fill ID
                {
                    let mut seen = self
                        .seen_fill_ids
                        .lock()
                        .unwrap_or_else(|e| e.into_inner());
                    if seen.contains(&fill_id) {
                        continue;
                    }
                    seen.insert(fill_id.clone());
                    if let Ok(mut order) = self.seen_fill_order.lock() {
                        order.push_back(fill_id.clone());
                        while seen.len() > 10_000 {
                            if let Some(oldest) = order.pop_front() {
                                seen.remove(&oldest);
                            } else {
                                break;
                            }
                        }
                    }
                }

                // Update cursor to latest order ID
                if let Ok(mut cursor) = self.last_fill_cursor.lock() {
                    let _ = client_order_id; // available for future cursor use
                    *cursor = Some(order_id.clone());
                }

                // Track max timestamp for next poll cycle
                if ts > max_ts {
                    max_ts = ts;
                }

                new_fills.push(Fill {
                    fill_id,
                    order_id,
                    market_id: symbol,
                    side: Side::Yes, // Crypto doesn't have Yes/No sides
                    order_side,
                    price: filled_avg_price,
                    size: filled_qty,
                    fee: 0.0,
                    timestamp: ts,
                    token_id: None,
                    exchange: "robinhood".to_string(),
                    is_maker: false,
                });
            }
        }

        // Persist max fill timestamp for next poll cycle
        if max_ts > last_ts {
            if let Ok(mut ts_guard) = self.last_fill_ts.lock() {
                *ts_guard = max_ts;
            }
        }

        if !new_fills.is_empty() {
            info!(count = new_fills.len(), "robinhood: polled new fills");
        }

        Ok(new_fills)
    }

    /// Fetch positions (holdings) from Robinhood for reconciliation.
    async fn fetch_positions_async(&self) -> Result<Vec<Position>, HorizonError> {
        let url = format!("{}/crypto/trading/holdings/", self.api_url);

        let builder = self.client.get(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("robinhood: position fetch failed: {}", e))
            })?;

        if !resp.status().is_success() {
            let status = resp.status();
            debug!(status = %status, "robinhood: position fetch returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("robinhood: position parse error: {}", e))
        })?;

        let mut positions = Vec::new();

        if let Some(arr) = json.get("results").and_then(|v| v.as_array()) {
            for p in arr {
                let asset_code = p
                    .get("asset_code")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let qty = p
                    .get("total_quantity")
                    .and_then(|v| {
                        v.as_str()
                            .and_then(|s| s.parse::<f64>().ok())
                            .or_else(|| v.as_f64())
                    })
                    .unwrap_or(0.0);
                let avg_cost = p
                    .get("average_cost")
                    .and_then(|v| {
                        v.as_str()
                            .and_then(|s| s.parse::<f64>().ok())
                            .or_else(|| v.as_f64())
                    })
                    .unwrap_or(0.0);

                if qty.abs() > 0.0 && qty.is_finite() {
                    positions.push(Position {
                        market_id: asset_code,
                        side: Side::Yes,
                        size: qty,
                        avg_entry_price: avg_cost,
                        realized_pnl: 0.0,
                        unrealized_pnl: 0.0,
                        token_id: None,
                        exchange: "robinhood".to_string(),
                    });
                }
            }
        }

        if !positions.is_empty() {
            info!(
                count = positions.len(),
                "robinhood: fetched positions for reconciliation"
            );
        }

        Ok(positions)
    }
}

impl Exchange for RobinhoodExchange {
    fn submit_order(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let req_clone = req.clone();
        let order_id = self
            .runtime
            .block_on(self.submit_order_async(&req_clone))?;

        // Track order_id per market for per-market cancellation
        if let Ok(mut map) = self.open_orders.lock() {
            map.entry(req.market_id.clone())
                .or_default()
                .push(order_id.clone());
        }

        Ok(order_id)
    }

    fn cancel_order(&self, order_id: &str) -> Result<(), HorizonError> {
        let id = order_id.to_string();
        self.runtime.block_on(self.cancel_order_async(&id))?;

        // Remove from open_orders tracking
        if let Ok(mut map) = self.open_orders.lock() {
            for ids in map.values_mut() {
                ids.retain(|oid| oid != order_id);
            }
        }

        Ok(())
    }

    fn cancel_all(&self) -> Result<usize, HorizonError> {
        let count = self.runtime.block_on(self.cancel_all_async())?;

        if let Ok(mut map) = self.open_orders.lock() {
            map.clear();
        }

        Ok(count)
    }

    fn cancel_for_market(&self, market_id: &str) -> Result<usize, HorizonError> {
        let order_ids: Vec<String> = {
            let map = self
                .open_orders
                .lock()
                .unwrap_or_else(|e| e.into_inner());
            map.get(market_id).cloned().unwrap_or_default()
        };

        if order_ids.is_empty() {
            return Ok(0);
        }

        let mut successfully_canceled = Vec::new();
        for oid in &order_ids {
            match self.runtime.block_on(self.cancel_order_async(oid)) {
                Ok(()) => successfully_canceled.push(oid.clone()),
                Err(e) => {
                    debug!(
                        order_id = %oid,
                        error = %e,
                        "robinhood: cancel_for_market: order cancel failed (may already be filled)"
                    );
                }
            }
        }

        // Only remove successfully canceled IDs from tracking
        if let Ok(mut map) = self.open_orders.lock() {
            if let Some(ids) = map.get_mut(market_id) {
                ids.retain(|oid| !successfully_canceled.contains(oid));
                if ids.is_empty() {
                    map.remove(market_id);
                }
            }
        }

        let canceled = successfully_canceled.len();
        info!(
            market_id = %market_id,
            canceled = canceled,
            total = order_ids.len(),
            "robinhood: canceled orders for market"
        );
        Ok(canceled)
    }

    fn drain_fills(&self) -> Vec<Fill> {
        // Poll for new fills from the API
        match self.runtime.block_on(self.poll_fills_async()) {
            Ok(new_fills) => {
                if !new_fills.is_empty() {
                    // Remove filled order IDs from open_orders tracking
                    if let Ok(mut map) = self.open_orders.lock() {
                        for fill in &new_fills {
                            for ids in map.values_mut() {
                                ids.retain(|oid| oid != &fill.order_id);
                            }
                        }
                        map.retain(|_, ids| !ids.is_empty());
                    }

                    if let Ok(mut fills) = self.fills.lock() {
                        fills.extend(new_fills);
                    }
                }
            }
            Err(e) => {
                warn!(error = %e, "robinhood: fill polling error");
            }
        }

        match self.fills.lock() {
            Ok(mut fills) => std::mem::take(&mut *fills),
            Err(_) => {
                error!("robinhood: fills lock poisoned");
                Vec::new()
            }
        }
    }

    fn name(&self) -> &str {
        "robinhood"
    }

    fn fetch_positions(&self) -> Result<Vec<Position>, HorizonError> {
        self.runtime.block_on(self.fetch_positions_async())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_url_param_valid() {
        assert!(validate_url_param("abc-123_def", "test").is_ok());
    }

    #[test]
    fn test_validate_url_param_empty() {
        assert!(validate_url_param("", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_special_chars() {
        assert!(validate_url_param("abc/../etc", "test").is_err());
        assert!(validate_url_param("abc def", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_slash() {
        assert!(validate_url_param("abc/def", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_uuid() {
        assert!(validate_url_param("550e8400-e29b-41d4-a716-446655440000", "test").is_ok());
    }

    #[test]
    fn test_validate_url_param_alphanumeric() {
        assert!(validate_url_param("abc123", "test").is_ok());
        assert!(validate_url_param("ABC", "test").is_ok());
    }
}
