use crate::{
    edge_value::EdgeValue,
    trade_action::TradeDetail,
    trend_metrics::{CrossOverType, TrendMetrics},
    PricePoint, TradeAction,
};
use std::{cell::RefCell, collections::VecDeque};

#[derive(Debug, Clone, PartialEq)]
pub enum MarketCondition {
    UpBreak,
    DownBreak,
    UpInit,
    DownInit,
    HotMarket,
    ColdMarket,
    RangeBound,
    Unknown,
}

impl MarketCondition {
    pub fn to_numeric(&self) -> f64 {
        match self {
            MarketCondition::ColdMarket => 1.15,
            MarketCondition::UpBreak => 1.075,
            MarketCondition::UpInit => 1.05,
            MarketCondition::Unknown => 1.0,
            MarketCondition::DownInit => 0.95,
            MarketCondition::DownBreak => 0.925,
            MarketCondition::HotMarket => 0.85,
            MarketCondition::RangeBound => 0.99,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Prediction {
    pub confidence: f64,
    pub price: f64,
}

#[derive(Debug, Clone)]
struct MarketDataConfig {
    short_period: usize,
    long_period: usize,
    trading_period: usize,
    rebalance_period: usize,
    max_size: usize,
}

#[derive(Debug, Clone)]
struct MarketDataMetrics {
    price: RefCell<TrendMetrics>,
    rsi: RefCell<TrendMetrics>,
    adx: TrendMetrics,
    stochastic: TrendMetrics,
    bollinger_band: RefCell<EdgeValue>,
    atr: Option<f64>,
}

impl MarketDataMetrics {
    fn new(short_period: usize, long_period: usize, macd_period: usize) -> Self {
        Self {
            price: TrendMetrics::new(short_period, long_period, macd_period).into(),
            rsi: TrendMetrics::new(short_period, long_period, macd_period).into(),
            adx: TrendMetrics::new(short_period, long_period, macd_period),
            stochastic: TrendMetrics::new(short_period, long_period, macd_period),
            atr: None,
            bollinger_band: EdgeValue::new(short_period).into(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct MarketData {
    name: String,
    prices: VecDeque<PricePoint>,
    price_mean: f64,
    price_variance: f64,
    trade_period_counter: usize,
    config: MarketDataConfig,
    metrics: MarketDataMetrics,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TradingStrategy {
    TrendFollow,
    ConstantProportionPortfolio,
    MarketMake,
}

const VOLATILITY_LOWER_THRESHOLD: f64 = 0.3;

impl MarketData {
    pub fn new(
        name: String,
        short_period: usize,
        long_period: usize,
        trading_period: usize,
        rebalance_period: usize,
        max_size: usize,
    ) -> MarketData {
        let config = MarketDataConfig {
            short_period,
            long_period,
            trading_period,
            rebalance_period,
            max_size,
        };
        let metrics = MarketDataMetrics::new(short_period, long_period, trading_period);
        MarketData {
            name,
            prices: VecDeque::with_capacity(max_size),
            config,
            metrics,
            price_mean: 0.0,
            price_variance: 0.0,
            trade_period_counter: 0,
        }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn last_price(&self) -> f64 {
        self.metrics
            .price
            .borrow()
            .val
            .current_value()
            .unwrap_or_default()
    }

    pub fn atr(&self) -> f64 {
        match self.metrics.atr {
            Some(v) => v,
            None => 0.0,
        }
    }

    pub fn adx(&self) -> f64 {
        match self.metrics.adx.val.current_ema() {
            Some(v) => v,
            None => 0.0,
        }
    }

    pub fn rsi(&self) -> f64 {
        match self.metrics.rsi.borrow().val.current_ema() {
            Some(v) => v,
            None => 0.5,
        }
    }

    pub fn add_price(&mut self, price: Option<f64>, timestamp: Option<i64>) -> PricePoint {
        if self.prices.len() == self.config.max_size {
            self.prices.pop_front();
        }

        let price = match price {
            Some(price) => price,
            None => {
                if self.prices.len() == 0 {
                    0.0
                } else {
                    match self.prices.iter().last() {
                        Some(v) => v.price,
                        None => 0.0,
                    }
                }
            }
        };

        let price_point = PricePoint::new(
            price,
            timestamp,
            self.metrics
                .price
                .borrow()
                .short_term_val
                .current_ema()
                .unwrap_or_default(),
            self.metrics
                .price
                .borrow()
                .long_term_val
                .current_ema()
                .unwrap_or_default(),
            self.metrics.rsi.borrow().val.current_ema().unwrap_or(0.5),
        );

        self.prices.push_back(price_point.clone());

        self.update_mean_and_variance(self.config.trading_period);
        self.update_ema(price);
        self.update_rsi();
        self.update_adx();
        self.update_stochastic();
        self.update_atr();
        self.update_bollinger_band();

        self.trade_period_counter += 1;
        if self.trade_period_counter > self.config.rebalance_period {
            self.trade_period_counter = 1;
        }

        price_point
    }

    pub fn is_open_signaled(
        &self,
        strategy: TradingStrategy,
        _position_weight: f64,
    ) -> Vec<TradeAction> {
        let mut actions: Vec<TradeAction> = vec![];

        match strategy {
            TradingStrategy::ConstantProportionPortfolio => {
                if self.trade_period_counter == 1 {
                    let action = TradeAction::Rebalance;
                    actions.push(action);
                }
            }
            TradingStrategy::TrendFollow => {
                let is_crossover = self
                    .metrics
                    .price
                    .borrow_mut()
                    .is_crossover()
                    .unwrap_or(CrossOverType::None);
                let macd = self
                    .metrics
                    .price
                    .borrow()
                    .macd
                    .current_value()
                    .unwrap_or_default();

                let volatility = self.calculate_relative_volatility();
                let confidence = 1.0;

                if let Some(volatility) = volatility {
                    if volatility > VOLATILITY_LOWER_THRESHOLD {
                        match is_crossover {
                            CrossOverType::GoldenCross => {
                                if macd < 0.0 {
                                    let action = TradeAction::BuyOpen(TradeDetail::new(
                                        None, None, confidence,
                                    ));
                                    actions.push(action);
                                }
                            }
                            CrossOverType::DeadCross => {
                                if macd > 0.0 {
                                    let action = TradeAction::SellOpen(TradeDetail::new(
                                        None, None, confidence,
                                    ));
                                    actions.push(action);
                                }
                            }
                            _ => {}
                        };
                    }
                }
            }
            _ => {}
        }

        actions
    }

    pub fn is_close_signaled(&self, _strategy: TradingStrategy) -> TradeAction {
        TradeAction::None
    }

    fn update_mean_and_variance(&mut self, period: usize) {
        let prices_window = if self.prices.len() > period {
            self.prices
                .iter()
                .skip(self.prices.len() - period)
                .collect::<Vec<_>>()
        } else {
            self.prices.iter().collect::<Vec<_>>()
        };

        let count = prices_window.len() as f64;
        if count == 0.0 {
            return;
        }

        let sum: f64 = prices_window.iter().map(|pp| pp.price).sum();
        let new_mean = sum / count;

        let new_variance = prices_window
            .iter()
            .map(|pp| {
                let diff = pp.price - new_mean;
                diff * diff
            })
            .sum::<f64>()
            / count;

        self.price_mean = new_mean;
        self.price_variance = new_variance;
    }

    fn calculate_volatility(&self) -> Option<f64> {
        if self.prices.is_empty() {
            None
        } else {
            Some(self.price_variance.sqrt())
        }
    }

    fn calculate_relative_volatility(&self) -> Option<f64> {
        let volatility = self.calculate_volatility().unwrap_or_default();
        let atr = self.atr();
        if atr != 0.0 && volatility != 0.0 {
            Some(volatility / atr)
        } else {
            None
        }
    }

    fn update_rsi(&mut self) {
        let periods = [
            self.config.trading_period,
            self.config.short_period,
            self.config.long_period,
        ];
        let values: Vec<_> = periods
            .iter()
            .filter_map(|&period| self.calculate_rsi(period))
            .collect();

        if values.len() == periods.len() {
            self.metrics
                .rsi
                .borrow_mut()
                .update(values[0], values[1], values[2]);
        }
    }

    fn update_adx(&mut self) {
        let periods = [
            self.config.trading_period,
            self.config.short_period,
            self.config.long_period,
        ];
        let values: Vec<_> = periods
            .iter()
            .filter_map(|&period| self.calculate_adx(period))
            .collect();

        if values.len() == periods.len() {
            self.metrics.adx.update(values[0], values[1], values[2]);
        }
    }

    fn update_stochastic(&mut self) {
        let periods = [
            self.config.trading_period,
            self.config.short_period,
            self.config.long_period,
        ];
        let values: Vec<_> = periods
            .iter()
            .filter_map(|&period| self.calculate_stochastic(period))
            .collect();

        if values.len() == periods.len() {
            self.metrics
                .stochastic
                .update(values[0], values[1], values[2]);
        }
    }

    fn update_ema(&mut self, price: f64) {
        self.metrics.price.borrow_mut().update(price, price, price);
    }

    fn update_bollinger_band(&mut self) {
        if let Some((upper, lower, center)) =
            self.calculate_bollinger_band(self.config.trading_period)
        {
            // band.width_trend.update_value(band.width);
            self.metrics
                .bollinger_band
                .borrow_mut()
                .update_value(upper, lower, center);
        }
    }

    fn calculate_bollinger_band(&self, period: usize) -> Option<(f64, f64, f64)> {
        if self.prices.len() < period {
            return None;
        }

        let sum: f64 = self
            .prices
            .iter()
            .skip(self.prices.len() - period)
            .map(|pp| pp.price)
            .sum();
        let mean = sum / period as f64;

        let variance_sum: f64 = self
            .prices
            .iter()
            .skip(self.prices.len() - period)
            .map(|pp| (pp.price - mean).powi(2))
            .sum();
        let variance = variance_sum / period as f64;
        let standard_deviation = variance.sqrt();

        Some((
            mean + 2.0 * standard_deviation,
            mean - 2.0 * standard_deviation,
            mean,
        ))
    }

    fn calculate_rsi(&self, period: usize) -> Option<f64> {
        let window_size = 10;

        if self.prices.len() < period {
            return None;
        }

        let mut gains = 0.0;
        let mut losses = 0.0;

        let recent_prices = self
            .prices
            .iter()
            .skip(self.prices.len().saturating_sub(period))
            .cloned()
            .collect::<Vec<_>>();

        for window in recent_prices.windows(window_size) {
            if let (Some(first), Some(last)) = (window.first(), window.last()) {
                let change = last.price - first.price;
                if change >= 0.0 {
                    gains += change;
                } else {
                    losses += change.abs();
                }
            }
        }

        let rs = if losses != 0.0 { gains / losses } else { 100.0 };

        Some(1.0 - (1.0 / (1.0 + rs)))
    }

    fn calculate_adx(&mut self, period: usize) -> Option<f64> {
        if self.prices.len() < period {
            return None;
        }

        let (high, low) = self.extract_high_low(period);
        let atr = (high - low) / period as f64;

        if atr == 0.0 {
            return None;
        }

        let mut plus_dm = 0.0;
        let mut minus_dm = 0.0;

        for price_point in &self
            .prices
            .iter()
            .skip(self.prices.len() - period)
            .collect::<Vec<_>>()
        {
            let up_move = high - price_point.price;
            let down_move = price_point.price - low;
            if up_move > down_move && up_move > 0.0 {
                plus_dm += up_move;
            }
            if down_move > up_move && down_move > 0.0 {
                minus_dm += down_move;
            }
        }

        let plus_di = plus_dm / atr;
        let minus_di = minus_dm / atr;

        let denominator = plus_di + minus_di;
        if denominator == 0.0 {
            return None;
        }

        Some((plus_di - minus_di).abs() / denominator)
    }

    fn calculate_stochastic(&mut self, period: usize) -> Option<f64> {
        if self.prices.len() < period {
            return None;
        }

        let (high, low) = self.extract_high_low(period);
        let current_close = match self.prices.iter().last() {
            Some(v) => v.price,
            None => {
                return None;
            }
        };

        if high == low {
            None
        } else {
            Some((current_close - low) / (high - low))
        }
    }

    fn update_atr(&mut self) {
        let period = self.config.trading_period;
        if self.prices.len() <= period {
            // +1 is to included the previos_close price
            return;
        }

        if self.metrics.atr.is_none() {
            let mut tr_sum = 0.0;
            for i in 0..=self.prices.len() - period {
                let (current_high, current_low) = self.extract_high_low(period);
                let previous_close = self.prices[i].price;

                let tr = [
                    current_high - current_low,
                    (current_high - previous_close).abs(),
                    (current_low - previous_close).abs(),
                ]
                .iter()
                .cloned()
                .fold(f64::NAN, f64::max);

                tr_sum += tr;
            }

            let initial_atr = tr_sum / (self.prices.len() - period + 1) as f64;
            self.metrics.atr = Some(initial_atr);
        } else {
            let (current_high, current_low) = self.extract_high_low(period);
            let previous_close = self.prices[self.prices.len() - period - 1].price;

            let current_tr = [
                current_high - current_low,
                (current_high - previous_close).abs(),
                (current_low - previous_close).abs(),
            ]
            .iter()
            .cloned()
            .fold(f64::NAN, f64::max);

            let alpha = 2.0 / (period as f64 + 1.0);
            let new_atr = self.metrics.atr.unwrap() * (1.0 - alpha) + current_tr * alpha;
            self.metrics.atr = Some(new_atr);
        }
    }

    fn extract_high_low(&self, period: usize) -> (f64, f64) {
        self.prices
            .iter()
            .skip(self.prices.len() - period)
            .fold((f64::NEG_INFINITY, f64::INFINITY), |(high, low), pp| {
                (high.max(pp.price), low.min(pp.price))
            })
    }

    pub fn get_market_detail(&self) -> (f64, f64, f64, f64) {
        let is_crossover = self
            .metrics
            .price
            .borrow_mut()
            .is_crossover()
            .unwrap_or(CrossOverType::None)
            .to_numeric();

        let atr = self.atr();
        let volatility = self.calculate_volatility().unwrap_or_default();
        let spread = self.last_price();

        let rel_volatility = if atr != 0.0 && volatility != 0.0 {
            let mut a = volatility / atr;
            if a > 1.0 {
                a = 1.0;
            }
            a
        } else {
            0.0
        };

        let macd = self
            .metrics
            .price
            .borrow()
            .macd
            .current_value()
            .unwrap_or_default();

        if is_crossover > 0.5 && macd > 0.0 {
            return (0.5, spread, volatility, rel_volatility);
        }

        if is_crossover < 0.5 && macd < 0.0 {
            return (0.5, spread, volatility, rel_volatility);
        }

        return (is_crossover, spread, volatility, rel_volatility);
    }
}
