use crate::{
    edge_value::EdgeValue,
    trade_chance::TradeDetail,
    trend_metrics::{self, CrossOverType, TrendMetrics, TrendType},
    PricePoint, TradeAction,
};
use std::collections::VecDeque;

#[derive(Debug, PartialEq)]
pub enum MarketCondition {
    UptrendBreakout,
    DowntrendBreakout,
    ReversalDownward,
    ReversalUpward,
    UptrendInitiated,
    DowntrendInitiated,
    RangeBound,
    Unknown,
}

#[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,
    max_size: usize,
}

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

const RSI_LOWER_BOUND: f64 = 0.3;
const RSI_UPPER_BOUND: f64 = 0.7;

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

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

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

impl MarketData {
    pub fn new(
        name: String,
        short_period: usize,
        long_period: usize,
        trading_period: usize,
        max_size: usize,
    ) -> MarketData {
        let config = MarketDataConfig {
            short_period,
            long_period,
            trading_period,
            max_size,
        };
        let metrics = MarketDataMetrics::new(short_period, long_period);

        MarketData {
            name,
            prices: VecDeque::with_capacity(max_size),
            last_price: 0.0,
            config,
            metrics,
        }
    }

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

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

    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.short_term_val.current_ema(),
            self.metrics.price.long_term_val.current_ema(),
            self.metrics.rsi.current_value.unwrap_or(0.5),
        );

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

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

        self.last_price = price;

        price_point
    }

    fn assess_market_condition(&self) -> MarketCondition {
        let rsi = self.metrics.rsi.current_value.unwrap_or(0.5);
        let atr = self.atr();
        let bollinger_width = self.metrics.bollinger_band.width;
        let trend_type = self.metrics.price.trend_type();
        let crossover = self.metrics.price.is_crossover();

        let last_price_point = match self.prices.back() {
            Some(price_point) => price_point,
            None => return MarketCondition::Unknown,
        };

        let previous_price_point = match self.prices.iter().rev().nth(1) {
            Some(price_point) => price_point,
            None => return MarketCondition::Unknown,
        };

        if previous_price_point.price <= self.metrics.bollinger_band.upper
            && last_price_point.price > self.metrics.bollinger_band.upper
            && trend_type == TrendType::StrongUp
            && rsi > RSI_UPPER_BOUND
        {
            return MarketCondition::UptrendBreakout;
        }

        if previous_price_point.price >= self.metrics.bollinger_band.lower
            && last_price_point.price < self.metrics.bollinger_band.lower
            && trend_type == TrendType::StrongDown
            && rsi < RSI_LOWER_BOUND
        {
            return MarketCondition::DowntrendBreakout;
        }

        if crossover == CrossOverType::GoldenCross
            && self.metrics.bollinger_band.is_width_expanding()
        {
            return MarketCondition::UptrendInitiated;
        }

        if crossover == CrossOverType::DeadCross
            && !self.metrics.bollinger_band.is_width_expanding()
        {
            return MarketCondition::DowntrendInitiated;
        }

        if last_price_point.price > self.metrics.bollinger_band.upper
            && trend_type == TrendType::WeakUp
            && rsi > RSI_UPPER_BOUND
        {
            return MarketCondition::ReversalDownward;
        }

        if last_price_point.price < self.metrics.bollinger_band.lower
            && trend_type == TrendType::WeakDown
            && rsi < RSI_LOWER_BOUND
        {
            return MarketCondition::ReversalUpward;
        }

        if bollinger_width < atr {
            return MarketCondition::RangeBound;
        }

        MarketCondition::Unknown
    }

    pub fn is_open_signaled(&self, strategy: TradingStrategy) -> TradeAction {
        let atr = self.atr();
        if atr == 0.0 {
            return TradeAction::None;
        }

        self.log_price_info();

        let condition = self.assess_market_condition();
        let confidence = 1.0;

        match strategy {
            TradingStrategy::TrendFollow => match condition {
                MarketCondition::UptrendBreakout | MarketCondition::UptrendInitiated => {
                    return TradeAction::BuyOpen(TradeDetail::new(
                        self.last_price + atr,
                        confidence,
                    ));
                }
                MarketCondition::DowntrendBreakout | MarketCondition::DowntrendInitiated => {
                    return TradeAction::SellOpen(TradeDetail::new(
                        self.last_price - atr,
                        confidence,
                    ));
                }
                _ => {
                    return TradeAction::None;
                }
            },
            TradingStrategy::MeanReversion => {
                let predicted_price = self.metrics.bollinger_band.center;

                match condition {
                    MarketCondition::ReversalDownward => {
                        return TradeAction::SellOpen(TradeDetail::new(predicted_price, confidence))
                    }
                    MarketCondition::ReversalUpward => {
                        return TradeAction::BuyOpen(TradeDetail::new(predicted_price, confidence))
                    }
                    _ => {
                        return TradeAction::None;
                    }
                }
            }
        }
    }

    pub fn is_close_signaled(&self, strategy: TradingStrategy) -> TradeAction {
        let condition = self.assess_market_condition();

        match strategy {
            TradingStrategy::TrendFollow => match condition {
                MarketCondition::UptrendBreakout | MarketCondition::UptrendInitiated => {
                    TradeAction::BuyClose
                }
                MarketCondition::DowntrendBreakout | MarketCondition::DowntrendInitiated => {
                    TradeAction::SellClose
                }
                _ => TradeAction::None,
            },
            TradingStrategy::MeanReversion => match condition {
                MarketCondition::ReversalDownward => TradeAction::SellClose,
                MarketCondition::ReversalUpward => TradeAction::BuyClose,
                _ => TradeAction::None,
            },
        }
    }

    fn update_rsi(&mut self) {
        if let Some(short_rsi) = self.calculate_rsi(self.config.short_period) {
            if let Some(long_rsi) = self.calculate_rsi(self.config.long_period) {
                self.metrics.rsi.update(short_rsi, short_rsi, long_rsi);
            }
        }
    }

    fn update_adx(&mut self) {
        if let Some(short_adx) = self.calculate_adx(self.config.short_period) {
            if let Some(long_adx) = self.calculate_adx(self.config.long_period) {
                self.metrics.adx.update(short_adx, short_adx, long_adx);
            }
        }
    }

    fn update_stochastic(&mut self) {
        if let Some(short_stochastic) = self.calculate_stochastic(self.config.short_period) {
            if let Some(long_stochastic) = self.calculate_stochastic(self.config.long_period) {
                self.metrics
                    .stochastic
                    .update(short_stochastic, short_stochastic, long_stochastic);
            }
        }
    }

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

    fn update_bollinger_band(&mut self) {
        if let Some(band) = self.calculate_bollinger_band(self.config.trading_period) {
            self.metrics.bollinger_band = band;

            if let Some(short_rsi) = self.calculate_rsi(self.config.short_period) {
                if let Some(long_rsi) = self.calculate_rsi(self.config.long_period) {
                    self.metrics.rsi.update(short_rsi, short_rsi, long_rsi);
                }
            }
        }
    }

    fn calculate_bollinger_band(&self, period: usize) -> Option<EdgeValue> {
        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(EdgeValue::new(
            mean + 2.0 * standard_deviation,
            mean - 2.0 * standard_deviation,
            mean,
            period,
        ))
    }

    fn calculate_rsi(&self, period: usize) -> Option<f64> {
        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(60) {
            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;

        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;
        Some((plus_di - minus_di).abs() / (plus_di + minus_di))
    }

    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))
            })
    }

    fn log_price_info(&self) {
        const GREEN: &str = "\x1b[0;32m";
        const LIGHT_GREEN: &str = "\x1b[0;92m";
        const RED: &str = "\x1b[0;31m";
        const ORANGE: &str = "\x1b[0;33m";
        const RESET: &str = "\x1b[0m";

        let color = match self.metrics.price.trend_type() {
            trend_metrics::TrendType::StrongUp => LIGHT_GREEN,
            trend_metrics::TrendType::WeakUp => GREEN,
            trend_metrics::TrendType::StrongDown => RED,
            trend_metrics::TrendType::WeakDown => ORANGE,
        };

        log::info!(
            "{}{:<.1} {} ema({:<.1} - {:<.1}) rsi({:<.2}) atr({:.1}) band-width({:<.1})",
            color,
            self.last_price,
            RESET,
            self.metrics.price.short_term_val.current_ema(),
            self.metrics.price.long_term_val.current_ema(),
            self.metrics.rsi.current_value.unwrap_or(0.5),
            self.atr(),
            self.metrics.bollinger_band.width,
        );
    }
}
