//! ICMarkets broker implementation via cTrader Open API.
//!
//! Replaces the legacy FIX stub with a production cTrader protobuf client
//! that routes orders, queries positions, and streams executions through
//! the `BrokerInterface` trait.

use crate::brokers::config::ICMarketsConfig;
use crate::trading::data_interface::{BrokerConnectionStatus, BrokerError, BrokerInterface};
use crate::trading_operations::TradingOrder;
use async_trait::async_trait;
use common::{Execution as ExecutionReport, OrderSide, OrderStatus, OrderType, Position};
use rust_decimal::prelude::ToPrimitive;
use std::collections::HashMap;
use tracing::{debug, info, warn};
use uuid::Uuid;

use ctrader_openapi::{
    config::{CTraderConfig, CTraderEnvironment},
    proto::{ProtoOaOrderType, ProtoOaTradeSide},
    CTraderClient,
};

use tokio::sync::RwLock;

/// Default lot size for forex symbols (100,000 units = 10,000,000 in cTrader volume cents).
const DEFAULT_LOT_SIZE: i64 = 100_000;

/// ICMarkets client backed by cTrader Open API.
pub struct ICMarketsClient {
    config: ICMarketsConfig,
    client: RwLock<Option<CTraderClient>>,
}

impl std::fmt::Debug for ICMarketsClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ICMarketsClient")
            .field("enabled", &self.config.enabled)
            .field("account_id", &self.config.account_id)
            .field("environment", &self.config.environment)
            .finish()
    }
}

impl ICMarketsClient {
    /// Create a new ICMarkets client (disconnected).
    pub fn new(config: ICMarketsConfig) -> Self {
        Self {
            config,
            client: RwLock::new(None),
        }
    }
}

// ── Type conversion helpers ──────────────────────────────────────────

fn order_side_to_proto(side: OrderSide) -> i32 {
    match side {
        OrderSide::Buy => 1,  // BUY
        OrderSide::Sell => 2, // SELL
    }
}

fn order_type_to_proto(ot: OrderType) -> i32 {
    match ot {
        OrderType::Market => 1,    // MARKET
        OrderType::Limit => 2,     // LIMIT
        OrderType::Stop => 3,      // STOP
        OrderType::StopLimit => 6, // STOP_LIMIT
        OrderType::Iceberg | OrderType::TrailingStop | OrderType::Hidden | _ => 1, // default to MARKET for unsupported types
    }
}

/// Convert a decimal lot quantity to cTrader volume (in units, 1 lot = lot_size units).
fn lots_to_volume(lots: rust_decimal::Decimal) -> i64 {
    let lots_f64 = lots.to_f64().unwrap_or(0.0);
    (lots_f64 * DEFAULT_LOT_SIZE as f64) as i64
}

// ── BrokerInterface: real cTrader implementation ─────────────────────

#[async_trait]
impl BrokerInterface for ICMarketsClient {
    async fn connect(&mut self) -> Result<(), BrokerError> {
        info!(
            account_id = self.config.account_id,
            env = %self.config.environment,
            "connecting ICMarkets via cTrader"
        );

        let env = match self.config.environment.as_str() {
            "live" => CTraderEnvironment::Live,
            _ => CTraderEnvironment::Demo,
        };

        let ctrader_config = CTraderConfig {
            client_id: self.config.client_id.clone(),
            client_secret: self.config.client_secret.clone(),
            access_token: self.config.access_token.clone(),
            account_id: self.config.account_id,
            environment: env,
            heartbeat_interval_secs: self.config.heartbeat_interval_secs,
            request_timeout_ms: self.config.request_timeout_ms,
            max_reconnect_attempts: self.config.max_reconnect_attempts,
        };

        let ct_client = CTraderClient::connect(ctrader_config)
            .await
            .map_err(|e| BrokerError::ConnectionFailed(format!("cTrader connect: {e}")))?;

        info!("ICMarkets cTrader connection established");
        *self.client.write().await = Some(ct_client);
        Ok(())
    }

    async fn disconnect(&mut self) -> Result<(), BrokerError> {
        info!("disconnecting ICMarkets cTrader");
        *self.client.write().await = None;
        Ok(())
    }

    fn is_connected(&self) -> bool {
        // Can't call async in sync fn — check if client exists via try_read
        self.client
            .try_read()
            .map(|guard| guard.is_some())
            .unwrap_or(false)
    }

    fn connection_status(&self) -> BrokerConnectionStatus {
        if self.is_connected() {
            BrokerConnectionStatus::Connected
        } else {
            BrokerConnectionStatus::Disconnected
        }
    }

    async fn submit_order(&self, order: &TradingOrder) -> Result<String, BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let side = ProtoOaTradeSide::try_from(order_side_to_proto(order.side))
            .map_err(|_| BrokerError::InvalidOrder("invalid side".into()))?;

        let ot = ProtoOaOrderType::try_from(order_type_to_proto(order.order_type))
            .map_err(|_| BrokerError::InvalidOrder("invalid order type".into()))?;

        let volume = lots_to_volume(order.quantity);

        let limit_price = (order.order_type == OrderType::Limit
            || order.order_type == OrderType::StopLimit)
            .then(|| order.price.to_f64().unwrap_or(0.0));

        let stop_price = (order.order_type == OrderType::Stop
            || order.order_type == OrderType::StopLimit)
            .then(|| order.price.to_f64().unwrap_or(0.0));

        let comment = order.metadata.get("comment").cloned();

        debug!(
            symbol = %order.symbol,
            side = ?order.side,
            volume,
            order_type = ?order.order_type,
            "submitting order via cTrader"
        );

        let resp = ct
            .submit_order(
                &order.symbol,
                side,
                volume,
                ot,
                limit_price,
                stop_price,
                None, // stop_loss
                None, // take_profit
                comment,
            )
            .await
            .map_err(|e| BrokerError::OrderSubmissionFailed(format!("cTrader: {e}")))?;

        let broker_order_id = resp
            .client_msg_id
            .unwrap_or_else(|| format!("ct-{}", resp.payload_type));

        info!(broker_order_id = %broker_order_id, "order submitted");
        Ok(broker_order_id)
    }

    async fn cancel_order(&self, broker_order_id: &str) -> Result<(), BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let order_id: i64 = broker_order_id
            .parse()
            .map_err(|_| BrokerError::OrderNotFound(format!("invalid order ID: {broker_order_id}")))?;

        ct.cancel_order(order_id)
            .await
            .map_err(|e| BrokerError::OrderSubmissionFailed(format!("cancel: {e}")))?;

        info!(broker_order_id, "order cancelled");
        Ok(())
    }

    async fn modify_order(
        &self,
        broker_order_id: &str,
        new_order: &TradingOrder,
    ) -> Result<(), BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let order_id: i64 = broker_order_id
            .parse()
            .map_err(|_| BrokerError::OrderNotFound(format!("invalid order ID: {broker_order_id}")))?;

        let volume = Some(lots_to_volume(new_order.quantity));
        let limit_price = (new_order.order_type == OrderType::Limit
            || new_order.order_type == OrderType::StopLimit)
            .then(|| new_order.price.to_f64().unwrap_or(0.0));

        let stop_price = (new_order.order_type == OrderType::Stop
            || new_order.order_type == OrderType::StopLimit)
            .then(|| new_order.price.to_f64().unwrap_or(0.0));

        ct.amend_order(order_id, volume, limit_price, stop_price, None, None)
            .await
            .map_err(|e| BrokerError::OrderSubmissionFailed(format!("amend: {e}")))?;

        info!(broker_order_id, "order amended");
        Ok(())
    }

    async fn get_order_status(&self, broker_order_id: &str) -> Result<OrderStatus, BrokerError> {
        // cTrader doesn't have a direct "get order status" API.
        // Use reconcile to find order status from pending orders.
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let reconcile = ct
            .get_positions()
            .await
            .map_err(|e| BrokerError::ProtocolError(format!("reconcile: {e}")))?;

        let order_id: i64 = broker_order_id.parse().unwrap_or(-1);

        for order in &reconcile.orders {
            if order.order_id == order_id {
                return Ok(OrderStatus::Working);
            }
        }

        // Not found in pending orders — could be filled or cancelled
        Ok(OrderStatus::Filled)
    }

    async fn get_account_info(&self) -> Result<HashMap<String, String>, BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let info = ct
            .get_account_info()
            .await
            .map_err(|e| BrokerError::ProtocolError(format!("account info: {e}")))?;

        let mut map = HashMap::new();
        map.insert("broker".to_owned(), "ICMarkets".to_owned());
        map.insert("account_id".to_owned(), ct.account_id().to_string());
        map.insert("balance".to_owned(), info.balance.to_string());
        map.insert(
            "deposit_asset_id".to_owned(),
            info.deposit_asset_id.to_string(),
        );
        map.insert(
            "leverage_in_cents".to_owned(),
            info.leverage_in_cents.to_string(),
        );
        Ok(map)
    }

    async fn get_positions(&self) -> Result<Vec<Position>, BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let reconcile = ct
            .get_positions()
            .await
            .map_err(|e| BrokerError::ProtocolError(format!("positions: {e}")))?;

        let now = chrono::Utc::now();
        let positions = reconcile
            .positions
            .iter()
            .map(|p| {
                let quantity = rust_decimal::Decimal::from(p.trade_data.volume);
                let symbol = p.trade_data.symbol_id.to_string();
                let entry_price =
                    rust_decimal::Decimal::from(p.price.unwrap_or(0.0) as i64);

                Position {
                    id: Uuid::new_v4(),
                    symbol,
                    quantity,
                    avg_price: entry_price,
                    avg_cost: entry_price,
                    basis: rust_decimal::Decimal::ZERO,
                    average_price: entry_price,
                    market_value: rust_decimal::Decimal::ZERO,
                    unrealized_pnl: rust_decimal::Decimal::ZERO,
                    realized_pnl: rust_decimal::Decimal::ZERO,
                    created_at: now,
                    updated_at: now,
                    last_updated: now,
                    current_price: None,
                    notional_value: rust_decimal::Decimal::ZERO,
                    margin_requirement: rust_decimal::Decimal::ZERO,
                }
            })
            .collect();

        Ok(positions)
    }

    async fn subscribe_executions(
        &self,
    ) -> Result<tokio::sync::mpsc::Receiver<ExecutionReport>, BrokerError> {
        let guard = self.client.read().await;
        let ct = guard
            .as_ref()
            .ok_or_else(|| BrokerError::ConnectionFailed("not connected".into()))?;

        let mut broadcast_rx = ct.subscribe_executions();
        let (tx, rx) = tokio::sync::mpsc::channel(1000);

        // Bridge broadcast → mpsc in a background task
        tokio::spawn(async move {
            loop {
                match broadcast_rx.recv().await {
                    Ok(_msg) => {
                        let now = chrono::Utc::now();
                        let exec = ExecutionReport {
                            id: Uuid::new_v4(),
                            order_id: Uuid::nil(),
                            symbol: String::new(),
                            quantity: rust_decimal::Decimal::ZERO,
                            price: rust_decimal::Decimal::ZERO,
                            side: OrderSide::Buy,
                            fees: rust_decimal::Decimal::ZERO,
                            fee_currency: "USD".to_owned(),
                            executed_at: now,
                            timestamp: now,
                            symbol_hash: 0,
                            broker_execution_id: None,
                            counterparty: None,
                            venue: Some("cTrader".to_owned()),
                            gross_value: rust_decimal::Decimal::ZERO,
                            net_value: rust_decimal::Decimal::ZERO,
                        };

                        if tx.send(exec).await.is_err() {
                            break;
                        }
                    }
                    Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
                    Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
                        warn!(skipped = n, "execution broadcast lagged");
                    }
                }
            }
        });

        Ok(rx)
    }

    fn broker_name(&self) -> &str {
        "ICMarkets"
    }

    async fn send_heartbeat(&self) -> Result<(), BrokerError> {
        // Heartbeat is handled automatically by the cTrader connection layer.
        Ok(())
    }

    async fn reconnect(&self) -> Result<(), BrokerError> {
        warn!("reconnect requested -- cTrader reconnection not yet implemented, reconnect via connect()");
        Err(BrokerError::ConnectionFailed(
            "reconnect requires re-calling connect()".into(),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::brokers::config::ICMarketsConfig;

    #[test]
    fn new_client_is_disconnected() {
        let client = ICMarketsClient::new(ICMarketsConfig::default());
        assert!(!client.is_connected());
        assert_eq!(
            client.connection_status(),
            BrokerConnectionStatus::Disconnected
        );
    }

    #[test]
    fn lots_to_volume_standard() {
        let qty = rust_decimal::Decimal::new(1, 0); // 1.0 lot
        assert_eq!(lots_to_volume(qty), DEFAULT_LOT_SIZE);
    }

    #[test]
    fn lots_to_volume_micro() {
        let qty = rust_decimal::Decimal::new(1, 2); // 0.01 lot
        assert_eq!(lots_to_volume(qty), DEFAULT_LOT_SIZE / 100);
    }

    #[test]
    fn side_conversion() {
        assert_eq!(order_side_to_proto(OrderSide::Buy), 1);
        assert_eq!(order_side_to_proto(OrderSide::Sell), 2);
    }

    #[test]
    fn order_type_conversion() {
        assert_eq!(order_type_to_proto(OrderType::Market), 1);
        assert_eq!(order_type_to_proto(OrderType::Limit), 2);
        assert_eq!(order_type_to_proto(OrderType::Stop), 3);
        assert_eq!(order_type_to_proto(OrderType::StopLimit), 6);
    }
}
