//! Coinbase Advanced Trade REST exchange client.
//!
//! Implements the Exchange trait for Coinbase's Advanced Trade API.
//! Uses HMAC-SHA256 header-based auth (CB-ACCESS-KEY, CB-ACCESS-SIGN,
//! CB-ACCESS-TIMESTAMP). Includes fill polling, order management, and
//! position fetching via accounts.

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://api.coinbase.com/api/v3/brokerage";

/// 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!(
            "coinbase: {} is empty",
            name
        )));
    }
    if !param
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    {
        return Err(HorizonError::Exchange(format!(
            "coinbase: invalid characters in {}",
            name
        )));
    }
    Ok(())
}

pub struct CoinbaseExchange {
    api_url: String,
    api_key: Zeroizing<String>,
    api_secret: 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 (product_id) 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>>,
}

impl CoinbaseExchange {
    pub fn new(
        api_key: String,
        api_secret: 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),
            api_secret: Zeroizing::new(api_secret),
            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()),
        }
    }

    /// Generate HMAC-SHA256 signature for Coinbase Advanced Trade API.
    /// Returns (timestamp, signature, api_key).
    fn sign_request(&self, method: &str, path: &str, body: &str) -> (String, String, String) {
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs()
            .to_string();
        let message = format!("{}{}{}{}", timestamp, method, path, body);
        use hmac::{Hmac, Mac};
        use sha2::Sha256;
        type HmacSha256 = Hmac<Sha256>;
        let mut mac =
            HmacSha256::new_from_slice(self.api_secret.as_bytes()).expect("HMAC key length");
        mac.update(message.as_bytes());
        let signature = hex::encode(mac.finalize().into_bytes());
        (timestamp, signature, self.api_key.to_string())
    }

    /// Add Coinbase auth headers to a request builder.
    fn auth_headers(
        &self,
        builder: reqwest::RequestBuilder,
        method: &str,
        path: &str,
        body: &str,
    ) -> reqwest::RequestBuilder {
        let (timestamp, signature, key) = self.sign_request(method, path, body);
        builder
            .header("CB-ACCESS-KEY", key)
            .header("CB-ACCESS-SIGN", signature)
            .header("CB-ACCESS-TIMESTAMP", timestamp)
            .header("Content-Type", "application/json")
    }

    /// Submit order to Coinbase with retry on 429.
    async fn submit_order_async(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let path = "/api/v3/brokerage/orders";
        let url = format!("{}/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 Coinbase: {}",
                req.size
            )));
        }
        if req.price <= 0.0 || req.price.is_nan() || req.price.is_infinite() {
            return Err(HorizonError::Exchange(format!(
                "invalid price for Coinbase: {}",
                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,
            "product_id": req.market_id,
            "side": side_str,
            "order_configuration": {
                "limit_limit_gtc": {
                    "base_size": format!("{}", req.size),
                    "limit_price": format!("{:.2}", req.price),
                }
            }
        });

        let body_str = body.to_string();

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

        let mut backoff_ms: u64 = 1000;

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

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

            if status_code == 429 && attempt < 4 {
                warn!(
                    attempt = attempt + 1,
                    backoff_ms = backoff_ms,
                    "coinbase: 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 == 403 {
                let resp_body = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "coinbase: forbidden (403) -- check API key permissions: {}",
                    resp_body
                )));
            }

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

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

            // Coinbase returns {"success": true, "order_id": "...", ...}
            let order_id = json
                .get("order_id")
                .and_then(|v| v.as_str())
                .filter(|s| !s.is_empty())
                .or_else(|| {
                    json.get("success_response")
                        .and_then(|r| r.get("order_id"))
                        .and_then(|v| v.as_str())
                        .filter(|s| !s.is_empty())
                })
                .map(|s| s.to_string())
                .ok_or_else(|| {
                    HorizonError::Exchange(format!(
                        "coinbase: response missing order_id: {}",
                        json
                    ))
                })?;

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

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

    /// Cancel order on Coinbase via batch cancel endpoint.
    async fn cancel_order_async(&self, order_id: &str) -> Result<(), HorizonError> {
        validate_url_param(order_id, "order_id")?;
        let path = "/api/v3/brokerage/orders/batch_cancel";
        let url = format!("{}/orders/batch_cancel", self.api_url);

        let body = serde_json::json!({
            "order_ids": [order_id]
        });
        let body_str = body.to_string();

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

        if !resp.status().is_success() {
            let status = resp.status();
            let resp_body = resp.text().await.unwrap_or_default();
            // Treat 404 / not-found as already canceled/filled
            if status.as_u16() == 404 {
                debug!(order_id = %order_id, "coinbase: order not found (may be filled)");
                return Ok(());
            }
            return Err(HorizonError::Exchange(format!(
                "coinbase cancel failed ({}): {}",
                status, resp_body
            )));
        }

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

    /// Cancel all orders on Coinbase.
    /// Lists open orders first, then batch-cancels them.
    async fn cancel_all_async(&self) -> Result<usize, HorizonError> {
        // First, list all open orders
        let list_path = "/api/v3/brokerage/orders/historical?order_status=OPEN&limit=100";
        let list_url = format!(
            "{}/orders/historical?order_status=OPEN&limit=100",
            self.api_url
        );

        let builder = self.client.get(&list_url);
        let resp = self
            .auth_headers(builder, "GET", list_path, "")
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("coinbase 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!(
                "coinbase cancel-all list failed ({}): {}",
                status, body
            )));
        }

        let json: serde_json::Value = resp.json().await.unwrap_or(serde_json::Value::Null);
        let order_ids: Vec<String> = json
            .get("orders")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|o| o.get("order_id").and_then(|v| v.as_str()).map(String::from))
                    .collect()
            })
            .unwrap_or_default();

        if order_ids.is_empty() {
            info!("coinbase: no open orders to cancel");
            return Ok(0);
        }

        // Batch cancel all collected order IDs
        let cancel_path = "/api/v3/brokerage/orders/batch_cancel";
        let cancel_url = format!("{}/orders/batch_cancel", self.api_url);
        let cancel_body = serde_json::json!({ "order_ids": order_ids });
        let cancel_body_str = cancel_body.to_string();

        let builder = self.client.post(&cancel_url).body(cancel_body_str.clone());
        let resp = self
            .auth_headers(builder, "POST", cancel_path, &cancel_body_str)
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("coinbase cancel-all batch failed: {}", e))
            })?;

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

        let count = order_ids.len();
        info!(count = count, "coinbase: canceled all orders");
        Ok(count)
    }

    /// Poll for fills from Coinbase.
    async fn poll_fills_async(&self) -> Result<Vec<Fill>, HorizonError> {
        let mut path =
            "/api/v3/brokerage/orders/historical/fills?limit=100".to_string();
        let mut url = format!(
            "{}/orders/historical/fills?limit=100",
            self.api_url
        );

        // Add cursor for incremental polling
        if let Ok(cursor) = self.last_fill_cursor.lock() {
            if let Some(ref c) = *cursor {
                path = format!("{}&cursor={}", path, c);
                url = format!("{}&cursor={}", url, c);
            }
        }

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

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

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

        // Update cursor for next poll
        if let Some(cursor) = json.get("cursor").and_then(|v| v.as_str()) {
            if !cursor.is_empty() {
                if let Ok(mut last_cursor) = self.last_fill_cursor.lock() {
                    *last_cursor = Some(cursor.to_string());
                }
            }
        }

        let mut new_fills = Vec::new();

        if let Some(fills_arr) = json.get("fills").and_then(|v| v.as_array()) {
            let mut next_id = self.next_fill_id.lock().unwrap_or_else(|e| e.into_inner());

            for fill_json in fills_arr {
                let trade_id = fill_json
                    .get("trade_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let order_id = fill_json
                    .get("order_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let product_id = fill_json
                    .get("product_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

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

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

                let commission = fill_json
                    .get("commission")
                    .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 = fill_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 = fill_json
                    .get("trade_time")
                    .and_then(|v| v.as_str())
                    .and_then(|s| s.parse::<f64>().ok())
                    .unwrap_or(0.0);

                // Reject NaN/Inf
                if !price.is_finite() || !size.is_finite() || price <= 0.0 || size <= 0.0 {
                    continue;
                }

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

                // Dedup by fill ID with FIFO eviction capped at 10k
                {
                    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;
                            }
                        }
                    }
                }

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

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

        Ok(new_fills)
    }

    /// Fetch positions from Coinbase accounts for reconciliation.
    async fn fetch_positions_async(&self) -> Result<Vec<Position>, HorizonError> {
        let path = "/api/v3/brokerage/accounts";
        let url = format!("{}/accounts", self.api_url);

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

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

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

        let mut positions = Vec::new();

        if let Some(accounts) = json.get("accounts").and_then(|v| v.as_array()) {
            for acct in accounts {
                let currency = acct
                    .get("currency")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let available = acct
                    .get("available_balance")
                    .and_then(|b| b.get("value"))
                    .and_then(|v| {
                        v.as_str()
                            .and_then(|s| s.parse::<f64>().ok())
                            .or_else(|| v.as_f64())
                    })
                    .unwrap_or(0.0);

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

                let total = available + hold;

                if total.abs() > 0.0 && currency != "USD" {
                    positions.push(Position {
                        market_id: currency,
                        side: Side::Yes,
                        size: total,
                        avg_entry_price: 0.0, // Coinbase accounts don't expose cost basis
                        realized_pnl: 0.0,
                        unrealized_pnl: 0.0,
                        token_id: None,
                        exchange: "coinbase".to_string(),
                    });
                }
            }
        }

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

        Ok(positions)
    }
}

impl Exchange for CoinbaseExchange {
    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,
                        "coinbase: 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(),
            "coinbase: 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, "coinbase: fill polling error");
            }
        }

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

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

    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_alphanumeric() {
        assert!(validate_url_param("BTC-USD", "test").is_ok());
        assert!(validate_url_param("order_12345", "test").is_ok());
    }

    #[test]
    fn test_validate_url_param_slashes() {
        assert!(validate_url_param("path/to/thing", "test").is_err());
    }
}
