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

#[derive(Debug, PartialEq)]
pub enum MarketCondition {
    // TrendFollow
    UpBreak,
    DownBreak,
    UpInit,
    DownInit,
    NearUpBreak,
    NearDownBreak,
    // MeanReversion
    RevDown,
    RevUp,
    HotMarket,
    ColdMarket,
    // Other
    Neutral,
    RangeBound,
    Todo,
    Unknown,
}

impl MarketCondition {
    pub fn to_numeric(&self) -> f64 {
        match self {
            MarketCondition::HotMarket => 1.0,
            MarketCondition::RevDown => 0.9,
            MarketCondition::UpBreak => 0.8,
            MarketCondition::NearUpBreak => 0.7,
            MarketCondition::UpInit => 0.6,
            MarketCondition::Neutral => 0.5,
            MarketCondition::RangeBound => 0.5,
            MarketCondition::Todo => 0.5,
            MarketCondition::Unknown => 0.5,
            MarketCondition::DownInit => 0.4,
            MarketCondition::NearDownBreak => 0.3,
            MarketCondition::DownBreak => 0.2,
            MarketCondition::RevUp => 0.1,
            MarketCondition::ColdMarket => 0.0,
        }
    }
}

#[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: RefCell<TrendMetrics>,
    rsi: TrendMetrics,
    adx: TrendMetrics,
    stochastic: TrendMetrics,
    bollinger_band: RefCell<EdgeValue>,
    atr: Option<f64>,
}

const RSI_LOW_LIMIT_BOUND: f64 = 0.3;
const RSI_LOWER_BOUND: f64 = 0.4;
const RSI_UPPER_BOUND: f64 = 0.6;
const RSI_UP_LIMIT_BOUND: f64 = 0.7;

impl MarketDataMetrics {
    fn new(short_period: usize, long_period: usize) -> Self {
        Self {
            price: TrendMetrics::new(short_period, long_period).into(),
            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().into(),
        }
    }
}

#[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.borrow().short_term_val.current_ema(),
            self.metrics.price.borrow().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
    }

    pub 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.borrow().width;
        let bollinger_band_expanding = self.metrics.bollinger_band.borrow().is_width_expanding();
        let trend_type = self.metrics.price.borrow().trend_type_with_angle();
        let crossover = self.metrics.price.borrow_mut().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 self
            .metrics
            .bollinger_band
            .borrow_mut()
            .is_breakout(last_price_point.price, previous_price_point.price)
            == BreakOutType::UpBreakout
            && trend_type == TrendType::StrongUp
            && rsi > RSI_UPPER_BOUND
        {
            return MarketCondition::UpBreak;
        }

        if self
            .metrics
            .bollinger_band
            .borrow_mut()
            .is_breakout(last_price_point.price, previous_price_point.price)
            == BreakOutType::DowndBreakout
            && trend_type == TrendType::StrongDown
            && rsi < RSI_LOWER_BOUND
        {
            return MarketCondition::DownBreak;
        }

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

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

        if last_price_point.price > self.metrics.bollinger_band.borrow().upper
            && trend_type == TrendType::WeakUp
            && rsi > RSI_UPPER_BOUND
            && self.metrics.price.borrow().short_term_val.current_ema() < 0.0
        {
            return MarketCondition::RevDown;
        }

        if last_price_point.price < self.metrics.bollinger_band.borrow().lower
            && trend_type == TrendType::WeakDown
            && rsi < RSI_LOWER_BOUND
            && self.metrics.price.borrow().short_term_val.current_ema() > 0.0
        {
            return MarketCondition::RevUp;
        }

        if bollinger_band_expanding && trend_type == TrendType::StrongUp && rsi > RSI_UP_LIMIT_BOUND
        {
            return MarketCondition::HotMarket;
        }

        if bollinger_band_expanding
            && trend_type == TrendType::StrongDown
            && rsi < RSI_LOW_LIMIT_BOUND
        {
            return MarketCondition::ColdMarket;
        }

        if self
            .metrics
            .bollinger_band
            .borrow()
            .is_approaching_upper(last_price_point.price)
            && self.metrics.price.borrow().is_trend_up()
        {
            return MarketCondition::NearUpBreak;
        }

        if self
            .metrics
            .bollinger_band
            .borrow()
            .is_approaching_lower(last_price_point.price)
            && !self.metrics.price.borrow().is_trend_up()
        {
            return MarketCondition::NearDownBreak;
        }

        if self
            .metrics
            .bollinger_band
            .borrow()
            .is_tight_consolidation(last_price_point.price)
            && bollinger_width < atr
        {
            return MarketCondition::Neutral;
        }

        if bollinger_width > atr {
            return MarketCondition::Todo;
        }

        MarketCondition::RangeBound
    }

    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::UpBreak => {
                    return TradeAction::BuyOpen(TradeDetail::new(
                        self.last_price + atr,
                        confidence,
                    ));
                }
                MarketCondition::UpInit => {
                    return TradeAction::BuyOpen(TradeDetail::new(
                        self.last_price + atr,
                        confidence,
                    ));
                }
                MarketCondition::NearUpBreak => {
                    return TradeAction::BuyOpen(TradeDetail::new(
                        self.last_price + atr,
                        confidence,
                    ));
                }
                MarketCondition::DownBreak => {
                    return TradeAction::SellOpen(TradeDetail::new(
                        self.last_price - atr,
                        confidence,
                    ));
                }
                MarketCondition::DownInit => {
                    return TradeAction::SellOpen(TradeDetail::new(
                        self.last_price - atr,
                        confidence,
                    ));
                }
                MarketCondition::NearDownBreak => {
                    return TradeAction::SellOpen(TradeDetail::new(
                        self.last_price - atr,
                        confidence,
                    ));
                }
                _ => {
                    return TradeAction::None;
                }
            },
            TradingStrategy::MeanReversion => {
                let center_price = self.metrics.bollinger_band.borrow().center;

                match condition {
                    MarketCondition::RevDown => {
                        return TradeAction::SellOpen(TradeDetail::new(center_price, confidence))
                    }
                    MarketCondition::HotMarket => {
                        return TradeAction::SellOpen(TradeDetail::new(
                            self.last_price - atr,
                            confidence,
                        ))
                    }
                    MarketCondition::RevUp => {
                        return TradeAction::BuyOpen(TradeDetail::new(center_price, confidence))
                    }
                    MarketCondition::ColdMarket => {
                        return TradeAction::BuyOpen(TradeDetail::new(
                            self.last_price + atr,
                            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::UpBreak | MarketCondition::UpInit => TradeAction::BuyClose,
                MarketCondition::DownBreak | MarketCondition::DownInit => TradeAction::SellClose,
                _ => TradeAction::None,
            },
            TradingStrategy::MeanReversion => match condition {
                MarketCondition::RevDown | MarketCondition::HotMarket => TradeAction::SellClose,
                MarketCondition::RevUp | MarketCondition::ColdMarket => 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.borrow_mut().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.into();

            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";
        const GREY: &str = "\x1b[0;90m";

        let color1 = match self.metrics.price.borrow().trend_type_with_angle() {
            TrendType::StrongUp => LIGHT_GREEN,
            TrendType::WeakUp => GREEN,
            TrendType::StrongDown => RED,
            TrendType::WeakDown => ORANGE,
            TrendType::NoTrend => GREY,
        };

        let color2 = match self.metrics.bollinger_band.borrow().is_width_expanding() {
            true => LIGHT_GREEN,
            false => RED,
        };

        let price = self.last_price;
        let upper = self.metrics.bollinger_band.borrow().upper;
        let lower = self.metrics.bollinger_band.borrow().lower;
        let rsi = self.metrics.rsi.current_value.unwrap_or(0.5);
        let atr = self.atr();
        let width = self.metrics.bollinger_band.borrow().width;
        let condition = self.assess_market_condition();
        let ema_diff = match self.metrics.price.borrow().ema_difference_rate() {
            Some(diff) => diff * 100.0,
            None => 0.0,
        };
        let short_term_change_rate =
            self.metrics.price.borrow().short_term_val.ema_change_rate() * 10000.0;
        let long_term_change_rate =
            self.metrics.price.borrow().long_term_val.ema_change_rate() * 10000.0;

        if price <= lower {
            log::info!(
                "{:<15?}: {}{:<.1}({:<.2}, {:<.3}, {:<.3}){} - {:<.1} - {:<.1} rsi({:<.2}) atr({:.1}) {}width({:<.1})",
                condition,
                color1,
                price,
                ema_diff,
                short_term_change_rate,
                long_term_change_rate,
                RESET,
                lower,
                upper,
                rsi,
                atr,
                color2,
                width,
            );
        } else if upper >= price && price >= lower {
            log::info!(
                "{:<15?}: {:<.1} - {}{:<.1}({:<.2}, {:<.3}, {:<.3}){} - {:<.1} rsi({:<.2}) atr({:.1}) {}width({:<.1})",
                condition,
                lower,
                color1,
                price,
                ema_diff,
                short_term_change_rate,
                long_term_change_rate,
                RESET,
                upper,
                rsi,
                atr,
                color2,
                width,
            );
        } else {
            log::info!(
                "{:<15?}: {:<.1} - {:<.1} - {}{:<.1}({:<.2}, {:<.3}, {:<.3}){} rsi({:<.2}) atr({:.1}) {}width({:<.1})",
                condition,
                lower,
                upper,
                color1,
                price,
                ema_diff,
                short_term_change_rate,
                long_term_change_rate,
                RESET,
                rsi,
                atr,
                color2,
                width,
            );
        }
    }
}
