use crate::MLError;
use serde::{Deserialize, Serialize};
use std::fmt;

/// Order type for execution strategy.
///
/// Three execution modes with calibrated transaction costs (Wave 2.5).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OrderType {
    /// Immediate execution, taker fee (15 bps)
    Market = 0,
    /// Passive order, maker rebate (5 bps)
    LimitMaker = 1,
    /// Immediate-or-cancel (10 bps)
    IoC = 2,
}

impl OrderType {
    /// Transaction cost as decimal (e.g., 0.0015 for 15 bps).
    pub fn transaction_cost(&self) -> f64 {
        match self {
            OrderType::Market => 0.0015,
            OrderType::LimitMaker => 0.0005,
            OrderType::IoC => 0.0010,
        }
    }

    /// Transaction cost in basis points (e.g., 15.0 for Market).
    pub fn cost_bps(&self) -> f32 {
        match self {
            OrderType::Market => 15.0,
            OrderType::LimitMaker => 5.0,
            OrderType::IoC => 10.0,
        }
    }

    /// Cost as decimal from bps (convenience wrapper).
    pub fn cost_decimal(&self) -> f64 {
        (self.cost_bps() as f64) / 10000.0
    }

    /// Convert from index (0-2).
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        match idx {
            0 => Ok(OrderType::Market),
            1 => Ok(OrderType::LimitMaker),
            2 => Ok(OrderType::IoC),
            _ => Err(MLError::InvalidInput(format!(
                "Invalid order type index: {}",
                idx
            ))),
        }
    }
}

impl fmt::Display for OrderType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OrderType::Market => write!(f, "Market"),
            OrderType::LimitMaker => write!(f, "LimitMaker"),
            OrderType::IoC => write!(f, "IoC"),
        }
    }
}

/// Exposure level for position sizing (-100% to +100%)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExposureLevel {
    Short100 = 0, // -100% of max position
    Short75 = 1,  // -75%
    Short50 = 2,  // -50%
    Short25 = 3,  // -25%
    Flat = 4,     // 0% (neutral)
    Long25 = 5,   // +25%
    Long50 = 6,   // +50%
    Long75 = 7,   // +75%
    Long100 = 8,  // +100%
}

impl ExposureLevel {
    /// Get target portfolio value percentage (-1.0 to +1.0 in 0.25 steps)
    pub fn target_exposure(&self) -> f64 {
        match self {
            ExposureLevel::Short100 => -1.0,
            ExposureLevel::Short75 => -0.75,
            ExposureLevel::Short50 => -0.5,
            ExposureLevel::Short25 => -0.25,
            ExposureLevel::Flat => 0.0,
            ExposureLevel::Long25 => 0.25,
            ExposureLevel::Long50 => 0.5,
            ExposureLevel::Long75 => 0.75,
            ExposureLevel::Long100 => 1.0,
        }
    }

    /// Convert from index (0-8)
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        match idx {
            0 => Ok(ExposureLevel::Short100),
            1 => Ok(ExposureLevel::Short75),
            2 => Ok(ExposureLevel::Short50),
            3 => Ok(ExposureLevel::Short25),
            4 => Ok(ExposureLevel::Flat),
            5 => Ok(ExposureLevel::Long25),
            6 => Ok(ExposureLevel::Long50),
            7 => Ok(ExposureLevel::Long75),
            8 => Ok(ExposureLevel::Long100),
            _ => Err(MLError::InvalidInput(format!(
                "Invalid exposure level index: {} (expected 0-8)",
                idx
            ))),
        }
    }
}

impl fmt::Display for ExposureLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ExposureLevel::Short100 => write!(f, "Short100"),
            ExposureLevel::Short75 => write!(f, "Short75"),
            ExposureLevel::Short50 => write!(f, "Short50"),
            ExposureLevel::Short25 => write!(f, "Short25"),
            ExposureLevel::Flat => write!(f, "Flat"),
            ExposureLevel::Long25 => write!(f, "Long25"),
            ExposureLevel::Long50 => write!(f, "Long50"),
            ExposureLevel::Long75 => write!(f, "Long75"),
            ExposureLevel::Long100 => write!(f, "Long100"),
        }
    }
}

/// Urgency level for execution timing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Urgency {
    Patient = 0,    // Wait for better price
    Normal = 1,     // Standard execution
    Aggressive = 2, // Immediate execution
}

impl Urgency {
    /// Get urgency weight (0.5-1.5)
    pub fn urgency_weight(&self) -> f64 {
        match self {
            Urgency::Patient => 0.5,
            Urgency::Normal => 1.0,
            Urgency::Aggressive => 1.5,
        }
    }

    /// Convert from index (0-2)
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        match idx {
            0 => Ok(Urgency::Patient),
            1 => Ok(Urgency::Normal),
            2 => Ok(Urgency::Aggressive),
            _ => Err(MLError::InvalidInput(format!(
                "Invalid urgency index: {}",
                idx
            ))),
        }
    }
}

impl fmt::Display for Urgency {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Urgency::Patient => write!(f, "Patient"),
            Urgency::Normal => write!(f, "Normal"),
            Urgency::Aggressive => write!(f, "Aggressive"),
        }
    }
}

/// Factored trading action combining exposure, order type, and urgency.
///
/// 5 exposure levels x 3 order types x 3 urgency levels = 45 actions.
/// Index mapping: `index = exposure * 9 + order * 3 + urgency` (0-44).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FactoredAction {
    pub exposure: ExposureLevel,
    pub order: OrderType,
    pub urgency: Urgency,
}

impl FactoredAction {
    /// Create a new trading action
    pub fn new(exposure: ExposureLevel, order: OrderType, urgency: Urgency) -> Self {
        Self {
            exposure,
            order,
            urgency,
        }
    }

    /// Map action index (0-44) to (exposure, order, urgency)
    /// Index = exposure * 9 + order * 3 + urgency
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        if idx >= 81 {
            return Err(MLError::InvalidInput(format!(
                "Action index {} out of bounds (0-80)",
                idx
            )));
        }

        let exposure_idx = idx / 9;
        let order_idx = (idx % 9) / 3;
        let urgency_idx = idx % 3;

        Ok(Self {
            exposure: ExposureLevel::from_index(exposure_idx)?,
            order: OrderType::from_index(order_idx)?,
            urgency: Urgency::from_index(urgency_idx)?,
        })
    }

    /// Map (exposure, order, urgency) to action index (0-80)
    /// Index = exposure * 9 + order * 3 + urgency
    pub fn to_index(&self) -> usize {
        let exposure_idx = self.exposure as usize;
        let order_idx = self.order as usize;
        let urgency_idx = self.urgency as usize;

        exposure_idx * 9 + order_idx * 3 + urgency_idx
    }

    /// Get target portfolio value percentage
    pub fn target_exposure(&self) -> f64 {
        self.exposure.target_exposure()
    }

    /// Get transaction cost multiplier
    pub fn transaction_cost(&self) -> f64 {
        self.order.transaction_cost()
    }

    /// Get urgency weight (0.5-1.5)
    pub fn urgency_weight(&self) -> f64 {
        self.urgency.urgency_weight()
    }

    /// Create a FactoredAction from a TradingAction.
    /// Maps Buy -> Long100/Market/Normal, Sell -> Short100/Market/Normal, Hold -> Flat/Market/Normal.
    pub fn from_trading_action(action: crate::trading_action::TradingAction) -> Self {
        use crate::trading_action::TradingAction;
        match action {
            TradingAction::Buy => {
                Self::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal)
            }
            TradingAction::Sell => {
                Self::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal)
            }
            TradingAction::Hold => {
                Self::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal)
            }
        }
    }

    /// Convert FactoredAction to TradingAction for reward calculation.
    ///
    /// Maps exposure levels to simple Buy/Sell/Hold actions:
    /// - Long100, Long50 -> Buy
    /// - Flat -> Hold
    /// - Short50, Short100 -> Sell
    pub fn to_trading_action(&self) -> crate::trading_action::TradingAction {
        use crate::trading_action::TradingAction;
        match self.exposure {
            ExposureLevel::Long100 | ExposureLevel::Long75 | ExposureLevel::Long50 | ExposureLevel::Long25 => TradingAction::Buy,
            ExposureLevel::Flat => TradingAction::Hold,
            ExposureLevel::Short25 | ExposureLevel::Short50 | ExposureLevel::Short75 | ExposureLevel::Short100 => TradingAction::Sell,
        }
    }

    /// Calculate total transaction cost for this action given trade value
    ///
    /// # Arguments
    ///
    /// * `trade_value` - Absolute value of the trade (price x position_size x |exposure|)
    ///
    /// # Returns
    ///
    /// Total cost in dollars based on order type:
    /// - **Market**: 0.15% (0.0015 x trade_value)
    /// - **LimitMaker**: 0.05% (0.0005 x trade_value)
    /// - **IoC**: 0.10% (0.0010 x trade_value)
    pub fn calculate_transaction_cost(&self, trade_value: f64) -> f64 {
        trade_value * self.transaction_cost()
    }

    /// Check if this action is a buy (long exposure)
    pub fn is_buy(&self) -> bool {
        matches!(
            self.exposure,
            ExposureLevel::Long100 | ExposureLevel::Long50
        )
    }

    /// Check if this action is a sell (short exposure)
    pub fn is_sell(&self) -> bool {
        matches!(
            self.exposure,
            ExposureLevel::Short100 | ExposureLevel::Short75 | ExposureLevel::Short50 | ExposureLevel::Short25
        )
    }

    /// Check if this action is neutral (flat exposure)
    pub fn is_hold(&self) -> bool {
        matches!(self.exposure, ExposureLevel::Flat)
    }

    /// Convert action to position delta
    ///
    /// Calculates the change in position needed to reach the target exposure.
    ///
    /// # Arguments
    /// * `current_position` - Current absolute position (e.g., 1.0, -0.5)
    /// * `max_position` - Maximum allowed position magnitude (typically 2.0)
    ///
    /// # Returns
    /// Position delta to achieve target exposure
    pub fn to_position_delta(&self, current_position: f64, max_position: f64) -> f64 {
        let target_exposure = self.target_exposure();
        let target_position = target_exposure * max_position;
        target_position - current_position
    }
}

impl fmt::Display for FactoredAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}+{}+{}", self.exposure, self.order, self.urgency)
    }
}

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

    #[test]
    fn test_factored_action_round_trip_all_45() {
        for idx in 0..45 {
            let action = FactoredAction::from_index(idx).unwrap();
            assert_eq!(
                action.to_index(),
                idx,
                "round-trip failed for index {}",
                idx
            );
        }
    }

    #[test]
    fn test_factored_action_out_of_bounds() {
        assert!(FactoredAction::from_index(81).is_err());
        assert!(FactoredAction::from_index(100).is_err());
    }

    #[test]
    fn test_factored_action_to_trading_action() {
        use crate::trading_action::TradingAction;
        let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
        assert_eq!(buy.to_trading_action(), TradingAction::Buy);
        let sell =
            FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal);
        assert_eq!(sell.to_trading_action(), TradingAction::Sell);
        let hold = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal);
        assert_eq!(hold.to_trading_action(), TradingAction::Hold);
    }

    #[test]
    fn test_factored_action_round_trip_trading() {
        use crate::trading_action::TradingAction;
        assert_eq!(
            FactoredAction::from_trading_action(TradingAction::Buy).to_trading_action(),
            TradingAction::Buy
        );
        assert_eq!(
            FactoredAction::from_trading_action(TradingAction::Sell).to_trading_action(),
            TradingAction::Sell
        );
        assert_eq!(
            FactoredAction::from_trading_action(TradingAction::Hold).to_trading_action(),
            TradingAction::Hold
        );
    }

    #[test]
    fn test_exposure_level_values() {
        assert_eq!(ExposureLevel::Short100 as usize, 0);
        assert_eq!(ExposureLevel::Short75 as usize, 1);
        assert_eq!(ExposureLevel::Short50 as usize, 2);
        assert_eq!(ExposureLevel::Short25 as usize, 3);
        assert_eq!(ExposureLevel::Flat as usize, 4);
        assert_eq!(ExposureLevel::Long25 as usize, 5);
        assert_eq!(ExposureLevel::Long50 as usize, 6);
        assert_eq!(ExposureLevel::Long75 as usize, 7);
        assert_eq!(ExposureLevel::Long100 as usize, 8);
    }

    #[test]
    fn test_urgency_values() {
        assert_eq!(Urgency::Patient as usize, 0);
        assert_eq!(Urgency::Normal as usize, 1);
        assert_eq!(Urgency::Aggressive as usize, 2);
    }

    #[test]
    fn test_target_exposure() {
        assert_eq!(ExposureLevel::Short100.target_exposure(), -1.0);
        assert_eq!(ExposureLevel::Short50.target_exposure(), -0.5);
        assert_eq!(ExposureLevel::Flat.target_exposure(), 0.0);
        assert_eq!(ExposureLevel::Long50.target_exposure(), 0.5);
        assert_eq!(ExposureLevel::Long100.target_exposure(), 1.0);
    }

    #[test]
    fn test_urgency_weights() {
        assert_eq!(Urgency::Patient.urgency_weight(), 0.5);
        assert_eq!(Urgency::Normal.urgency_weight(), 1.0);
        assert_eq!(Urgency::Aggressive.urgency_weight(), 1.5);
    }

    #[test]
    fn test_is_buy_sell_hold() {
        let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
        assert!(buy.is_buy());
        assert!(!buy.is_sell());
        assert!(!buy.is_hold());

        let sell =
            FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal);
        assert!(sell.is_sell());
        assert!(!sell.is_buy());
        assert!(!sell.is_hold());

        let hold = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal);
        assert!(hold.is_hold());
        assert!(!hold.is_buy());
        assert!(!hold.is_sell());
    }

    #[test]
    fn test_position_delta() {
        let action =
            FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
        let delta = action.to_position_delta(0.0, 2.0);
        assert!((delta - 2.0).abs() < 1e-10);
    }

    #[test]
    fn test_calculate_transaction_cost() {
        let action =
            FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Aggressive);
        let cost = action.calculate_transaction_cost(10_000.0);
        assert!((cost - 15.0).abs() < 1e-10);
    }

    #[test]
    fn test_display() {
        let action =
            FactoredAction::new(ExposureLevel::Long50, OrderType::LimitMaker, Urgency::Patient);
        let s = format!("{}", action);
        assert_eq!(s, "Long50+LimitMaker+Patient");
    }
}
