//! Order Generation Logic
//!
//! Converts portfolio allocations to executable orders, accounting for:
//! - Current positions (delta orders)
//! - Order size constraints (min/max)
//! - Rebalance thresholds
//! - Database persistence

use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use std::str::FromStr;
use tracing::{debug, error, info, warn};

#[cfg(test)]
use rust_decimal_macros::dec;

use common::{Order, OrderSide, OrderType, Position, Quantity, Symbol};

/// Error types for order generation
#[derive(Debug, thiserror::Error)]
pub enum OrderError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Invalid allocation: {reason}")]
    InvalidAllocation { reason: String },

    #[error("Failed to parse BigDecimal: {0}")]
    ParseBigDecimal(#[from] bigdecimal::ParseBigDecimalError),

    #[error("Order size below minimum: {symbol} = ${size:.2} (min: ${min_size:.2})")]
    OrderSizeBelowMinimum {
        symbol: String,
        size: f64,
        min_size: f64,
    },

    #[error("Order size exceeds maximum: {symbol} = ${size:.2} (max: ${max_size:.2})")]
    OrderSizeExceedsMaximum {
        symbol: String,
        size: f64,
        max_size: f64,
    },

    #[error("Position not found: {symbol}")]
    PositionNotFound { symbol: String },

    #[error("Invalid quantity: {reason}")]
    InvalidQuantity { reason: String },

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Insufficient data: {reason}")]
    InsufficientData { reason: String },

    #[error("Regime detection error: {0}")]
    RegimeDetection(String),
}

/// Portfolio allocation with symbol weights
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioAllocation {
    /// Unique allocation identifier
    pub allocation_id: String,

    /// Strategy identifier that generated this allocation
    pub strategy_id: String,

    /// Total capital to allocate
    pub total_capital: Decimal,

    /// Symbol weights (must sum to <= 1.0)
    pub symbol_weights: HashMap<String, f64>,

    /// Rebalance threshold (0.0-1.0)
    pub rebalance_threshold: f64,

    /// Maximum position size as fraction of capital (0.0-1.0)
    pub max_position_size: f64,

    /// Allocation creation timestamp
    pub created_at: DateTime<Utc>,
}

impl PortfolioAllocation {
    /// Validate allocation parameters
    pub fn validate(&self) -> Result<(), OrderError> {
        // Check total capital
        if self.total_capital <= Decimal::ZERO {
            return Err(OrderError::InvalidAllocation {
                reason: "Total capital must be positive".to_string(),
            });
        }

        // Check weights sum
        let weight_sum: f64 = self.symbol_weights.values().sum();
        if weight_sum > 1.01 {
            // Allow small rounding tolerance
            return Err(OrderError::InvalidAllocation {
                reason: format!("Symbol weights sum to {:.4}, must be <= 1.0", weight_sum),
            });
        }

        // Check individual weights
        for (symbol, &weight) in &self.symbol_weights {
            if !(0.0..=1.0).contains(&weight) {
                return Err(OrderError::InvalidAllocation {
                    reason: format!(
                        "Weight for {} is {:.4}, must be in [0.0, 1.0]",
                        symbol, weight
                    ),
                });
            }
        }

        // Check rebalance threshold
        if !(0.0..=1.0).contains(&self.rebalance_threshold) {
            return Err(OrderError::InvalidAllocation {
                reason: format!(
                    "Rebalance threshold is {:.4}, must be in [0.0, 1.0]",
                    self.rebalance_threshold
                ),
            });
        }

        // Check max position size
        if !(0.0..=1.0).contains(&self.max_position_size) {
            return Err(OrderError::InvalidAllocation {
                reason: format!(
                    "Max position size is {:.4}, must be in [0.0, 1.0]",
                    self.max_position_size
                ),
            });
        }

        Ok(())
    }
}

/// Order generator implementation
pub struct OrderGenerator {
    pool: PgPool,
    min_order_size: f64,
    max_order_size: f64,
}

impl OrderGenerator {
    /// Create a new order generator
    ///
    /// # Arguments
    /// * `pool` - Database connection pool
    /// * `min_order_size` - Minimum order size in USD
    /// * `max_order_size` - Maximum order size in USD
    pub fn new(pool: PgPool, min_order_size: f64, max_order_size: f64) -> Self {
        Self {
            pool,
            min_order_size,
            max_order_size,
        }
    }

    /// Generate orders from portfolio allocation
    ///
    /// # Arguments
    /// * `allocation` - Portfolio allocation with symbol weights
    /// * `current_positions` - Current positions to account for
    ///
    /// # Returns
    /// Vector of orders to execute
    ///
    /// # Errors
    /// Returns error if validation fails or database operation fails
    pub async fn generate_orders(
        &self,
        allocation: &PortfolioAllocation,
        current_positions: &[Position],
    ) -> Result<Vec<Order>, OrderError> {
        let start = std::time::Instant::now();

        // Validate allocation
        allocation.validate()?;

        debug!(
            "Generating orders for allocation {} with {} symbols",
            allocation.allocation_id,
            allocation.symbol_weights.len()
        );

        // Calculate target positions
        let target_positions = self.calculate_target_positions(allocation)?;

        // Calculate current position values
        let current_position_map = self.build_position_map(current_positions);

        // Calculate deltas and generate orders
        let mut orders = Vec::new();

        for (symbol, target_value) in &target_positions {
            let current_value = current_position_map.get(symbol).copied().unwrap_or(0.0);
            let delta = target_value - current_value;

            // Check if delta exceeds rebalance threshold
            let threshold_value =
                allocation.total_capital.to_f64().unwrap_or(0.0) * allocation.rebalance_threshold;

            if delta.abs() < threshold_value {
                debug!(
                    "Skipping {} - delta ${:.2} below threshold ${:.2}",
                    symbol, delta, threshold_value
                );
                continue;
            }

            // Create order if delta is significant
            if let Some(order) = self.create_order(allocation, symbol, delta, current_positions).await? {
                orders.push(order);
            }
        }

        // Store orders in database
        self.store_orders(&orders).await?;

        let duration = start.elapsed();
        info!(
            "Generated {} orders for allocation {} in {}ms",
            orders.len(),
            allocation.allocation_id,
            duration.as_millis()
        );

        Ok(orders)
    }

    /// Calculate target position values from allocation
    fn calculate_target_positions(
        &self,
        allocation: &PortfolioAllocation,
    ) -> Result<HashMap<String, f64>, OrderError> {
        let mut target_positions = HashMap::new();
        let total_capital =
            allocation
                .total_capital
                .to_f64()
                .ok_or_else(|| OrderError::InvalidAllocation {
                    reason: "Failed to convert total capital to f64".to_string(),
                })?;

        for (symbol, &weight) in &allocation.symbol_weights {
            let target_value = total_capital * weight;
            target_positions.insert(symbol.clone(), target_value);

            debug!(
                "Target for {}: ${:.2} ({:.2}%)",
                symbol,
                target_value,
                weight * 100.0
            );
        }

        Ok(target_positions)
    }

    /// Build map of current position values
    fn build_position_map(&self, positions: &[Position]) -> HashMap<String, f64> {
        let mut position_map = HashMap::new();

        for position in positions {
            // Calculate position value: quantity * current_price (or avg_price if no current price)
            let price = position
                .current_price
                .unwrap_or(position.avg_price)
                .to_f64()
                .unwrap_or(0.0);
            let value = position.quantity.to_f64().unwrap_or(0.0) * price;

            position_map.insert(position.symbol.clone(), value);

            debug!(
                "Current position {}: ${:.2} ({} @ ${:.2})",
                position.symbol, value, position.quantity, price
            );
        }

        position_map
    }

    /// Create order from delta
    async fn create_order(
        &self,
        allocation: &PortfolioAllocation,
        symbol: &str,
        delta: f64,
        current_positions: &[Position],
    ) -> Result<Option<Order>, OrderError> {
        let abs_delta = delta.abs();

        // Check minimum order size
        if abs_delta < self.min_order_size {
            debug!(
                "Skipping {} - delta ${:.2} below minimum ${:.2}",
                symbol, abs_delta, self.min_order_size
            );
            return Ok(None);
        }

        // Check maximum order size
        if abs_delta > self.max_order_size {
            warn!(
                "Order for {} exceeds maximum: ${:.2} > ${:.2}",
                symbol, abs_delta, self.max_order_size
            );
            return Err(OrderError::OrderSizeExceedsMaximum {
                symbol: symbol.to_string(),
                size: abs_delta,
                max_size: self.max_order_size,
            });
        }

        // Determine order side
        let side = if delta > 0.0 {
            OrderSide::Buy
        } else {
            OrderSide::Sell
        };

        // Calculate quantity based on estimated contract price
        // For futures, we need to convert dollar amount to contracts
        let estimated_price = self.estimate_contract_price(symbol, current_positions)?;
        let quantity_float = abs_delta / estimated_price;
        let quantity =
            Decimal::try_from(quantity_float).map_err(|e| OrderError::InvalidQuantity {
                reason: format!("Failed to convert quantity {}: {}", quantity_float, e),
            })?;

        if quantity <= Decimal::ZERO {
            return Ok(None);
        }

        // Create order
        let symbol_obj: Symbol = symbol.into();
        let quantity_obj =
            Quantity::from_decimal(quantity).map_err(|e| OrderError::InvalidAllocation {
                reason: format!("Failed to convert quantity: {}", e),
            })?;
        let mut order = Order::new(
            symbol_obj.clone(),
            side,
            quantity_obj,
            None, // Market order - no price
            OrderType::Market,
        );

        // Set additional fields
        order.client_order_id = Some(format!("agent_{}", Uuid::new_v4()));
        order.metadata = serde_json::json!({
            "allocation_id": allocation.allocation_id,
            "strategy_id": allocation.strategy_id,
            "delta_usd": delta,
            "estimated_price": estimated_price,
        });

        debug!(
            "Created {} order for {}: {} @ ~${:.2}",
            side, symbol, quantity, estimated_price
        );

        // Apply regime-adaptive dynamic stop-loss
        let order = crate::dynamic_stop_loss::apply_dynamic_stop_loss(
            order,
            symbol,
            &self.pool,
        )
        .await
        .map_err(|e| {
            warn!("Failed to apply dynamic stop-loss for {}: {}", symbol, e);
            e
        })?;

        Ok(Some(order))
    }

    /// Estimate contract price from current positions or historical data
    fn estimate_contract_price(
        &self,
        symbol: &str,
        current_positions: &[Position],
    ) -> Result<f64, OrderError> {
        // Try to get price from current position
        if let Some(position) = current_positions.iter().find(|p| p.symbol == symbol) {
            let price = position
                .current_price
                .unwrap_or(position.avg_price)
                .to_f64()
                .unwrap_or(0.0);
            if price > 0.0 {
                return Ok(price);
            }
        }

        // Fallback to typical contract prices (hardcoded for MVP)
        // In production, this would query market data API
        let typical_price = match symbol {
            "ES.FUT" => 5000.0,  // E-mini S&P 500
            "NQ.FUT" => 20000.0, // E-mini Nasdaq
            "ZN.FUT" => 110.0,   // 10-Year Treasury Note
            "6E.FUT" => 1.10,    // Euro FX
            "CL.FUT" => 80.0,    // Crude Oil
            "GC.FUT" => 2000.0,  // Gold
            "SI.FUT" => 25.0,    // Silver
            "YM.FUT" => 40000.0, // Mini Dow
            "RTY.FUT" => 2000.0, // Russell 2000
            "ZB.FUT" => 120.0,   // 30-Year Treasury Bond
            "ZC.FUT" => 500.0,   // Corn
            "ZS.FUT" => 1400.0,  // Soybeans
            "ZW.FUT" => 600.0,   // Wheat
            "NG.FUT" => 3.0,     // Natural Gas
            "HO.FUT" => 2.5,     // Heating Oil
            "RB.FUT" => 2.5,     // RBOB Gasoline
            "6A.FUT" => 0.70,    // Australian Dollar
            "6B.FUT" => 1.30,    // British Pound
            "6C.FUT" => 0.75,    // Canadian Dollar
            "6J.FUT" => 0.007,   // Japanese Yen
            _ => 1000.0,         // Generic fallback
        };

        Ok(typical_price)
    }

    /// Store orders in database
    async fn store_orders(&self, orders: &[Order]) -> Result<(), OrderError> {
        if orders.is_empty() {
            return Ok(());
        }

        for order in orders {
            let order_id = order.id.to_string();
            let allocation_id = order
                .metadata
                .get("allocation_id")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            let symbol = order.symbol.as_str();
            let side = order.side.to_string();
            let quantity_decimal: Decimal = order.quantity.into();
            let quantity = BigDecimal::from_str(&quantity_decimal.to_string())?;
            let price: Option<BigDecimal> = order.price.and_then(|p| {
                let p_dec: Decimal = p.into();
                BigDecimal::from_str(&p_dec.to_string()).ok()
            });
            let order_type = format!("{:?}", order.order_type).to_uppercase();
            let status = format!("{:?}", order.status).to_uppercase();
            let time_in_force = format!("{:?}", order.time_in_force).to_uppercase();
            let metadata = serde_json::to_value(&order.metadata)?;

            sqlx::query!(
                r#"
                INSERT INTO agent_orders (
                    order_id, allocation_id, symbol, side, quantity, price,
                    order_type, status, time_in_force, filled_quantity,
                    client_order_id, created_at, metadata
                )
                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
                "#,
                order_id,
                allocation_id,
                symbol,
                side,
                quantity,
                price,
                order_type,
                status,
                time_in_force,
                BigDecimal::from(0_i64), // filled_quantity
                order.client_order_id,
                order.created_at.to_datetime(),
                metadata,
            )
            .execute(&self.pool)
            .await?;

            debug!("Stored order {} in database", order_id);
        }

        Ok(())
    }
}

// Re-export Uuid for tests
use uuid::Uuid;

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;

    #[test]
    fn test_allocation_validation_valid() {
        let mut weights = HashMap::new();
        weights.insert("ES.FUT".to_string(), 0.5);
        weights.insert("NQ.FUT".to_string(), 0.5);

        let allocation = PortfolioAllocation {
            allocation_id: "test".to_string(),
            strategy_id: "test".to_string(),
            total_capital: dec!(1_000_000),
            symbol_weights: weights,
            rebalance_threshold: 0.05,
            max_position_size: 0.20,
            created_at: Utc::now(),
        };

        assert!(allocation.validate().is_ok());
    }

    #[test]
    fn test_allocation_validation_zero_capital() {
        let mut weights = HashMap::new();
        weights.insert("ES.FUT".to_string(), 1.0);

        let allocation = PortfolioAllocation {
            allocation_id: "test".to_string(),
            strategy_id: "test".to_string(),
            total_capital: Decimal::ZERO,
            symbol_weights: weights,
            rebalance_threshold: 0.05,
            max_position_size: 0.20,
            created_at: Utc::now(),
        };

        assert!(allocation.validate().is_err());
    }

    #[test]
    fn test_allocation_validation_weights_exceed_one() {
        let mut weights = HashMap::new();
        weights.insert("ES.FUT".to_string(), 0.8);
        weights.insert("NQ.FUT".to_string(), 0.8);

        let allocation = PortfolioAllocation {
            allocation_id: "test".to_string(),
            strategy_id: "test".to_string(),
            total_capital: dec!(1_000_000),
            symbol_weights: weights,
            rebalance_threshold: 0.05,
            max_position_size: 0.20,
            created_at: Utc::now(),
        };

        assert!(allocation.validate().is_err());
    }

    #[test]
    fn test_estimate_contract_price_es() {
        // Wrap in tokio runtime to avoid "requires a Tokio context" error from PgPool::connect_lazy
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let pool =
                PgPool::connect_lazy("postgresql://localhost/test")
                    .expect("Failed to create pool");
            let generator = OrderGenerator::new(pool, 100.0, 100_000.0);

            let positions = vec![];
            let price = generator
                .estimate_contract_price("ES.FUT", &positions)
                .expect("Should estimate price");

            assert_eq!(price, 5000.0);
        });
    }

    #[tokio::test]
    async fn test_build_position_map() {
        let pool =
            PgPool::connect_lazy("postgresql://localhost/test").expect("Failed to create pool");
        let generator = OrderGenerator::new(pool, 100.0, 100_000.0);

        let now = Utc::now();
        let positions = vec![Position {
            id: Uuid::new_v4(),
            symbol: "ES.FUT".to_string(),
            quantity: dec!(100),
            avg_price: dec!(5000.0),
            avg_cost: dec!(5000.0),
            basis: dec!(500_000),
            average_price: dec!(5000.0),
            market_value: dec!(505_000),
            unrealized_pnl: dec!(5_000),
            realized_pnl: Decimal::ZERO,
            created_at: now,
            updated_at: now,
            last_updated: now,
            current_price: Some(dec!(5050.0)),
            margin_requirement: Decimal::ZERO,
            notional_value: dec!(505_000),
        }];

        let position_map = generator.build_position_map(&positions);

        assert_eq!(position_map.len(), 1);
        assert!(position_map.contains_key("ES.FUT"));

        // Value = 100 * 5050.0 = 505,000
        let value = position_map.get("ES.FUT").copied().unwrap_or(0.0);
        assert!((value - 505_000.0).abs() < 1.0);
    }
}
