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%).
///
/// 8-level enum representing the composite direction × magnitude outcome.
/// The 4-branch DQN produces `direction(4) × magnitude(3)`:
///   Short(0), Hold(1), Long(2), Flat(3).
/// Hold × {Small,Half,Full} all collapse into a single `Hold` variant
/// (keep current position unchanged, zero transaction cost).
/// Flat × {Small,Half,Full} all collapse into a single `Flat` variant
/// (close position to zero).
///
/// Total factored actions: 8 × 3 (order) × 3 (urgency) = 72.
/// (Actually 108 raw actions, but Hold/Flat magnitude-collapse → 72 unique.)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExposureLevel {
    ShortSmall = 0, // -0.25
    ShortHalf  = 1, // -0.50
    ShortFull  = 2, // -1.00
    Hold       = 3, // keep current position unchanged
    LongSmall  = 4, // +0.25
    LongHalf   = 5, // +0.50
    LongFull   = 6, // +1.00
    Flat       = 7, //  0.00 (close position)
}

impl ExposureLevel {
    /// Target exposure as a fraction of max position.
    /// Hold returns NaN — caller must check is_hold() and preserve current position.
    pub fn target_exposure(&self) -> f64 {
        match self {
            ExposureLevel::ShortSmall => -0.25,
            ExposureLevel::ShortHalf  => -0.50,
            ExposureLevel::ShortFull  => -1.00,
            ExposureLevel::Hold       =>  f64::NAN, // keep current position
            ExposureLevel::LongSmall  =>  0.25,
            ExposureLevel::LongHalf   =>  0.50,
            ExposureLevel::LongFull   =>  1.00,
            ExposureLevel::Flat       =>  0.00,
        }
    }

    /// Convert from index (0-7).
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        match idx {
            0 => Ok(ExposureLevel::ShortSmall),
            1 => Ok(ExposureLevel::ShortHalf),
            2 => Ok(ExposureLevel::ShortFull),
            3 => Ok(ExposureLevel::Hold),
            4 => Ok(ExposureLevel::LongSmall),
            5 => Ok(ExposureLevel::LongHalf),
            6 => Ok(ExposureLevel::LongFull),
            7 => Ok(ExposureLevel::Flat),
            _ => Err(MLError::InvalidInput(format!(
                "Invalid exposure level index: {} (expected 0-7)",
                idx
            ))),
        }
    }

    /// Direction component: 0=Short, 1=Hold, 2=Long, 3=Flat.
    pub fn direction(&self) -> u8 {
        match self {
            ExposureLevel::ShortSmall | ExposureLevel::ShortHalf | ExposureLevel::ShortFull => 0,
            ExposureLevel::Hold => 1,
            ExposureLevel::LongSmall | ExposureLevel::LongHalf | ExposureLevel::LongFull => 2,
            ExposureLevel::Flat => 3,
        }
    }

    /// Magnitude component: 0=Small, 1=Half, 2=Full, 3=N/A (Hold/Flat).
    pub fn magnitude(&self) -> u8 {
        match self {
            ExposureLevel::ShortSmall | ExposureLevel::LongSmall => 0,
            ExposureLevel::ShortHalf  | ExposureLevel::LongHalf  => 1,
            ExposureLevel::ShortFull  | ExposureLevel::LongFull  => 2,
            ExposureLevel::Hold | ExposureLevel::Flat => 3,
        }
    }

    /// Construct from direction (0-3) and magnitude (0-2) branch outputs.
    /// When dir=1 (Hold), magnitude is ignored and Hold is returned.
    /// When dir=3 (Flat), magnitude is ignored and Flat is returned.
    pub fn from_dir_mag(dir: usize, mag: usize) -> Result<Self, MLError> {
        match dir {
            0 => match mag {
                0 => Ok(ExposureLevel::ShortSmall),
                1 => Ok(ExposureLevel::ShortHalf),
                2 => Ok(ExposureLevel::ShortFull),
                _ => Err(MLError::InvalidInput(format!("Invalid magnitude: {}", mag))),
            },
            1 => Ok(ExposureLevel::Hold),
            2 => match mag {
                0 => Ok(ExposureLevel::LongSmall),
                1 => Ok(ExposureLevel::LongHalf),
                2 => Ok(ExposureLevel::LongFull),
                _ => Err(MLError::InvalidInput(format!("Invalid magnitude: {}", mag))),
            },
            3 => Ok(ExposureLevel::Flat),
            _ => Err(MLError::InvalidInput(format!("Invalid direction: {} (expected 0-3)", dir))),
        }
    }

    /// Check if this is a buy (long direction).
    pub fn is_buy(&self) -> bool {
        matches!(self, ExposureLevel::LongSmall | ExposureLevel::LongHalf | ExposureLevel::LongFull)
    }

    /// Check if this is a sell (short direction).
    pub fn is_sell(&self) -> bool {
        matches!(self, ExposureLevel::ShortSmall | ExposureLevel::ShortHalf | ExposureLevel::ShortFull)
    }

    /// Check if this is hold (keep current position unchanged).
    pub fn is_hold(&self) -> bool {
        matches!(self, ExposureLevel::Hold)
    }

    /// Check if this is flat (close position to zero).
    pub fn is_flat(&self) -> bool {
        matches!(self, ExposureLevel::Flat)
    }
}

impl fmt::Display for ExposureLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ExposureLevel::ShortSmall => write!(f, "ShortSmall"),
            ExposureLevel::ShortHalf  => write!(f, "ShortHalf"),
            ExposureLevel::ShortFull  => write!(f, "ShortFull"),
            ExposureLevel::Hold       => write!(f, "Hold"),
            ExposureLevel::LongSmall  => write!(f, "LongSmall"),
            ExposureLevel::LongHalf   => write!(f, "LongHalf"),
            ExposureLevel::LongFull   => write!(f, "LongFull"),
            ExposureLevel::Flat       => write!(f, "Flat"),
        }
    }
}

/// 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.
///
/// 8 exposure levels x 3 order types x 3 urgency levels = 72 actions.
/// Index mapping: `index = exposure * 9 + order * 3 + urgency` (0-71).
///
/// The CUDA training pipeline uses a 4-branch architecture
/// (direction × magnitude × order × urgency). Direction(4) and magnitude(3)
/// are collapsed into the 8-level `ExposureLevel` (Hold and Flat each absorb
/// three magnitude combos into one variant).
#[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-71) to (exposure, order, urgency).
    /// Index = exposure * 9 + order * 3 + urgency
    pub fn from_index(idx: usize) -> Result<Self, MLError> {
        if idx >= 72 {
            return Err(MLError::InvalidInput(format!(
                "Action index {} out of bounds (0-71)",
                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-71).
    /// 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.
    /// Buy → LongFull(+1.0), Sell → ShortFull(-1.0), Hold → Hold(unchanged).
    pub fn from_trading_action(action: crate::trading_action::TradingAction) -> Self {
        use crate::trading_action::TradingAction;
        match action {
            TradingAction::Buy => {
                Self::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal)
            }
            TradingAction::Sell => {
                Self::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal)
            }
            TradingAction::Hold => {
                Self::new(ExposureLevel::Hold, OrderType::Market, Urgency::Normal)
            }
        }
    }

    /// Convert FactoredAction to TradingAction for reward calculation.
    pub fn to_trading_action(&self) -> crate::trading_action::TradingAction {
        use crate::trading_action::TradingAction;
        if self.exposure.is_buy() {
            TradingAction::Buy
        } else if self.exposure.is_sell() {
            TradingAction::Sell
        } else {
            TradingAction::Hold
        }
    }

    /// 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 direction).
    pub fn is_buy(&self) -> bool {
        self.exposure.is_buy()
    }

    /// Check if this action is a sell (short direction).
    pub fn is_sell(&self) -> bool {
        self.exposure.is_sell()
    }

    /// Check if this action is neutral (flat).
    pub fn is_hold(&self) -> bool {
        self.exposure.is_hold()
    }

    /// 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_72() {
        for idx in 0..72 {
            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(72).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::LongFull, OrderType::Market, Urgency::Normal);
        assert_eq!(buy.to_trading_action(), TradingAction::Buy);
        let sell =
            FactoredAction::new(ExposureLevel::ShortFull, 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::ShortSmall as usize, 0);
        assert_eq!(ExposureLevel::ShortHalf as usize, 1);
        assert_eq!(ExposureLevel::ShortFull as usize, 2);
        assert_eq!(ExposureLevel::Hold as usize, 3);
        assert_eq!(ExposureLevel::LongSmall as usize, 4);
        assert_eq!(ExposureLevel::LongHalf as usize, 5);
        assert_eq!(ExposureLevel::LongFull as usize, 6);
        assert_eq!(ExposureLevel::Flat as usize, 7);
    }

    #[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::ShortSmall.target_exposure(), -0.25);
        assert_eq!(ExposureLevel::ShortHalf.target_exposure(), -0.50);
        assert_eq!(ExposureLevel::ShortFull.target_exposure(), -1.0);
        assert!(ExposureLevel::Hold.target_exposure().is_nan()); // Hold = keep current
        assert_eq!(ExposureLevel::LongSmall.target_exposure(), 0.25);
        assert_eq!(ExposureLevel::LongHalf.target_exposure(), 0.50);
        assert_eq!(ExposureLevel::LongFull.target_exposure(), 1.0);
        assert_eq!(ExposureLevel::Flat.target_exposure(), 0.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::LongFull, OrderType::Market, Urgency::Normal);
        assert!(buy.is_buy());
        assert!(!buy.is_sell());
        assert!(!buy.is_hold());

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

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

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

    #[test]
    fn test_position_delta() {
        let action =
            FactoredAction::new(ExposureLevel::LongFull, 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::LongFull, 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::LongSmall, OrderType::LimitMaker, Urgency::Patient);
        let s = format!("{}", action);
        assert_eq!(s, "LongSmall+LimitMaker+Patient");
    }

    #[test]
    fn test_from_dir_mag() {
        assert_eq!(ExposureLevel::from_dir_mag(0, 0).unwrap(), ExposureLevel::ShortSmall);
        assert_eq!(ExposureLevel::from_dir_mag(0, 1).unwrap(), ExposureLevel::ShortHalf);
        assert_eq!(ExposureLevel::from_dir_mag(0, 2).unwrap(), ExposureLevel::ShortFull);
        assert_eq!(ExposureLevel::from_dir_mag(1, 0).unwrap(), ExposureLevel::Hold);
        assert_eq!(ExposureLevel::from_dir_mag(1, 1).unwrap(), ExposureLevel::Hold);
        assert_eq!(ExposureLevel::from_dir_mag(1, 2).unwrap(), ExposureLevel::Hold);
        assert_eq!(ExposureLevel::from_dir_mag(2, 0).unwrap(), ExposureLevel::LongSmall);
        assert_eq!(ExposureLevel::from_dir_mag(2, 1).unwrap(), ExposureLevel::LongHalf);
        assert_eq!(ExposureLevel::from_dir_mag(2, 2).unwrap(), ExposureLevel::LongFull);
        assert_eq!(ExposureLevel::from_dir_mag(3, 0).unwrap(), ExposureLevel::Flat);
        assert_eq!(ExposureLevel::from_dir_mag(3, 1).unwrap(), ExposureLevel::Flat);
        assert_eq!(ExposureLevel::from_dir_mag(3, 2).unwrap(), ExposureLevel::Flat);
    }

    #[test]
    fn test_direction_magnitude() {
        assert_eq!(ExposureLevel::ShortSmall.direction(), 0);
        assert_eq!(ExposureLevel::ShortSmall.magnitude(), 0);
        assert_eq!(ExposureLevel::ShortFull.direction(), 0);
        assert_eq!(ExposureLevel::ShortFull.magnitude(), 2);
        assert_eq!(ExposureLevel::Hold.direction(), 1);
        assert_eq!(ExposureLevel::Hold.magnitude(), 3); // N/A
        assert_eq!(ExposureLevel::LongFull.direction(), 2);
        assert_eq!(ExposureLevel::LongFull.magnitude(), 2);
        assert_eq!(ExposureLevel::Flat.direction(), 3);
        assert_eq!(ExposureLevel::Flat.magnitude(), 3); // N/A
    }
}
