use std::{collections::HashMap, env};

use debot_utils::ToDateTimeString;
use serde::{Deserialize, Serialize};

use crate::trend_value::{TrendValue, ValueChange};

use lazy_static::lazy_static;

lazy_static! {
    static ref MOMENTUM_ADJUSTMENT: f64 = env::var("MOMENTUM_ADJUSTMENT")
        .unwrap_or_else(|_| "1.0".to_string())
        .parse()
        .unwrap_or(1.0);
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct PricePoint {
    pub timestamp: i64,
    pub timestamp_str: String,
    relative_timestamp: Option<i64>,
    confidence: f64,
    pub price: f64,
}

impl PricePoint {
    pub fn new(price: f64, timestamp: Option<i64>, confidence: f64) -> Self {
        let time = timestamp.unwrap_or_else(|| chrono::Utc::now().timestamp());
        Self {
            timestamp: time,
            timestamp_str: time.to_datetime_string(),
            relative_timestamp: None,
            confidence,
            price,
        }
    }
}

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

#[derive(Debug, Clone)]
struct TrendMetrics {
    pub short_term_val: TrendValue,
    pub long_term_val: TrendValue,
}

impl TrendMetrics {
    fn new(short_period: usize, long_period: usize) -> Self {
        Self {
            short_term_val: TrendValue::new(short_period),
            long_term_val: TrendValue::new(long_period),
        }
    }
}

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

#[derive(Debug, Clone)]
struct MarketDataMetrics {
    ema: TrendMetrics,
    rsi: TrendMetrics,
    adx: TrendMetrics,
    atr: HashMap<usize, f64>,
    confidence: f64,
}

impl MarketDataMetrics {
    fn new(short_period: usize, long_period: usize) -> Self {
        Self {
            ema: TrendMetrics::new(short_period, long_period),
            rsi: TrendMetrics::new(short_period, long_period),
            adx: TrendMetrics::new(short_period, long_period),
            atr: HashMap::new(),
            confidence: 0.0,
        }
    }
}

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

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TradingStrategy {
    TrendFollowLong,
    TrendFollowShort,
    TrendFollowReactive,
}

#[derive(Debug, PartialEq, Eq)]
enum CrossoverType {
    None,
    Golden,
    Death,
}

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

        MarketData {
            name,
            prices: Vec::with_capacity(max_size),
            last_price: 0.0,
            first_timestamp: None,
            config,
            metrics,
        }
    }

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

    pub fn atr(&self, period: usize) -> Option<f64> {
        self.metrics.atr.get(&period).copied()
    }

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

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

        let mut price_point = PricePoint::new(price, timestamp, self.metrics.confidence);

        if let Some(prev_point) = self.prices.last() {
            if let Some(relative_time) = price_point.timestamp.checked_sub(prev_point.timestamp) {
                price_point.relative_timestamp = Some(relative_time);
            }
        }

        if self.first_timestamp.is_none() {
            self.first_timestamp = Some(price_point.timestamp);
        }

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

        let prev_timestamp = if self.prices.len() >= 2 {
            Some(self.prices[self.prices.len() - 2].timestamp)
        } else {
            None
        };

        self.update_ema(price, price_point.timestamp, prev_timestamp);
        self.update_rsi();
        self.update_adx();
        self.update_confidence();

        self.last_price = price;

        price_point
    }

    pub fn predict(&self, period: usize, strategy: TradingStrategy) -> Prediction {
        let mut price = self.prices[self.prices.len() - 1].price;
        let atr = self.atr(period);

        if atr.is_none() || self.prices.len() < self.config.long_period {
            return Prediction {
                confidence: 0.0,
                price,
            };
        }
        let atr = atr.unwrap();
        let confidence = self.metrics.confidence;

        match strategy {
            TradingStrategy::TrendFollowLong => match confidence {
                x if x >= 0.5 => {
                    price += atr;
                }
                _ => {}
            },
            TradingStrategy::TrendFollowShort => match confidence {
                x if x <= -0.5 => {
                    price -= atr;
                }
                _ => {}
            },
            _ => {}
        }

        Prediction { confidence, price }
    }

    fn update_confidence(&mut self) {
        let check_confidence = |metrics: &TrendMetrics, self_ref: &MarketData| {
            let mut confidence = 0.0;

            confidence += match self_ref.check_crossover(metrics) {
                CrossoverType::None => 0.0,
                CrossoverType::Golden => 0.25,
                CrossoverType::Death => -0.25,
            };

            confidence
        };

        let mut confidence = 0.0;

        let ema = check_confidence(&self.metrics.ema, &self);
        if ema != 0.0 {
            log::info!("ema = {}", ema);
            confidence += ema;
        }
        let rsi = check_confidence(&self.metrics.rsi, &self);
        if rsi != 0.0 {
            log::info!("rsi = {}", rsi);
            confidence += rsi;
        }
        let adx = check_confidence(&self.metrics.adx, &self);
        if adx != 0.0 {
            log::info!("adx = {}", adx);
            confidence += adx;
        }
        if confidence <= -0.5 || confidence >= 0.5 {
            log::info!("conficence = {}", confidence);
        }

        self.metrics.confidence = confidence;
    }

    #[allow(dead_code)]
    fn predict_next_price_ema(&self, period: usize) -> f64 {
        let price = self.prices[self.prices.len() - 1].price;
        let ema = self.calculate_ema(period);
        2.0 * ema - price
    }

    fn check_crossover(&self, metrics: &TrendMetrics) -> CrossoverType {
        // Short term crosses above long term
        if metrics.short_term_val.current_value > metrics.long_term_val.current_value
            && metrics.short_term_val.previous_value <= metrics.long_term_val.previous_value
        {
            return CrossoverType::Golden;
        }

        // Short term crosses below long term
        if metrics.short_term_val.current_value < metrics.long_term_val.current_value
            && metrics.short_term_val.previous_value >= metrics.long_term_val.previous_value
        {
            return CrossoverType::Death;
        }

        CrossoverType::None
    }

    #[allow(dead_code)]
    fn check_trend_crossover(&self, metrics: &TrendMetrics) -> CrossoverType {
        // Short term crosses above long term
        if metrics.short_term_val.current_trend > metrics.long_term_val.current_trend
            && metrics.short_term_val.previous_trend <= metrics.long_term_val.previous_trend
        {
            return CrossoverType::Golden;
        }

        // Short term crosses below long term
        if metrics.short_term_val.current_trend < metrics.long_term_val.current_trend
            && metrics.short_term_val.previous_trend >= metrics.long_term_val.previous_trend
        {
            return CrossoverType::Death;
        }

        CrossoverType::None
    }

    fn update_rsi(&mut self) {
        let new_rsi_short = self.calculate_rsi(self.config.short_period);
        if new_rsi_short > 0.0 {
            self.metrics.rsi.short_term_val.update_value(new_rsi_short);
        }

        let new_rsi_long = self.calculate_rsi(self.config.long_period);
        if new_rsi_long > 0.0 {
            self.metrics.rsi.long_term_val.update_value(new_rsi_long);
        }
    }

    fn update_adx(&mut self) {
        let new_adx_short = self.calculate_adx(self.config.short_period);
        if new_adx_short > 0.0 {
            self.metrics.adx.short_term_val.update_value(new_adx_short);
        }

        let new_adx_long = self.calculate_adx(self.config.long_period);
        if new_adx_long > 0.0 {
            self.metrics.adx.long_term_val.update_value(new_adx_long);
        }
    }

    fn update_ema(&mut self, price: f64, timestamp: i64, prev_timestamp: Option<i64>) {
        let time_difference = prev_timestamp.map_or(1.0, |prev| (timestamp - prev) as f64);

        let update_one_ema = |ema: &mut TrendValue, period: usize| {
            let weight = 2.0 / (period as f64 + 1.0);
            let prev_ema = ema.current_value;
            ema.update_value(if ema.current_value == 0.0 {
                price
            } else if self.prices.len() >= period {
                (price - prev_ema) * weight * time_difference + prev_ema
            } else {
                ema.current_value
            });
        };

        update_one_ema(
            &mut self.metrics.ema.short_term_val,
            self.config.short_period,
        );
        update_one_ema(&mut self.metrics.ema.long_term_val, self.config.long_period);
    }

    fn calculate_sma(&self, period: usize) -> f64 {
        let price = self.prices[self.prices.len() - 1].price;
        if self.prices.len() < period {
            return price;
        }

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

    fn calculate_ema(&self, period: usize) -> f64 {
        if self.prices.len() < period {
            return self.prices[self.prices.len() - 1].price;
        }

        let mut ema = self.calculate_sma(period);

        let multiplier = 2.0 / (period as f64 + 1.0);

        for p in self.prices.iter().skip(self.prices.len() - period + 1) {
            ema = (p.price - ema) * multiplier + ema;
        }

        ema
    }

    fn calculate_rsi(&self, period: usize) -> f64 {
        if self.prices.len() < period + 1 {
            return 0.0;
        }

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

        for i in (self.prices.len() - period)..self.prices.len() {
            let change = self.prices[i].price - self.prices[i - 1].price;
            if change >= 0.0 {
                gains += change;
            } else {
                losses += change.abs();
            }
        }

        let avg_gain = gains / period as f64;
        let avg_loss = losses / period as f64;

        let rs = if avg_loss != 0.0 {
            avg_gain / avg_loss
        } else {
            0.0
        };

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

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

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

        for i in 1..=period {
            let (high, low) = self.extract_high_low(period);
            tr += high - low;

            let up_move = high - self.prices[i - 1].price;
            let down_move = self.prices[i - 1].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 atr = tr / period as f64;
        let plus_di = 100.0 * (plus_dm / atr);
        let minus_di = 100.0 * (minus_dm / atr);
        let dx = 100.0 * (plus_di - minus_di).abs() / (plus_di + minus_di);
        log::info!("dx{} = {}", period, dx);

        dx
    }

    pub fn update_atr(&mut self, period: usize) {
        if self.prices.len() < period {
            return;
        }

        let window_size = period;
        let atr_prices = &self.prices;
        let atr = self.metrics.atr.get(&period);
        let mut tr_sum = 0.0;

        if atr.is_none() {
            for window in atr_prices.windows(window_size) {
                let high = window
                    .iter()
                    .map(|pp| pp.price)
                    .fold(f64::NEG_INFINITY, f64::max);
                let low = window
                    .iter()
                    .map(|pp| pp.price)
                    .fold(f64::INFINITY, f64::min);
                let window_tr = high - low;
                tr_sum += window_tr;
            }
            let new_atr = tr_sum / atr_prices.windows(window_size).len() as f64;
            self.metrics.atr.insert(period, new_atr);
        } else {
            let alpha = 0.8;
            let atr = atr.unwrap();
            let (current_high, current_low) = self.extract_high_low(period);
            let tr = current_high - current_low;
            let new_atr = atr * (1.0 - alpha) + tr * alpha;
            self.metrics.atr.insert(period, new_atr);
        }
    }

    fn extract_high_low(&self, period: usize) -> (f64, f64) {
        let prices_len = self.prices.len();
        if prices_len < period {
            return (0.0, 0.0);
        }

        let start_index = prices_len - period;
        let mut high = f64::NEG_INFINITY;
        let mut low = f64::INFINITY;

        for price_point in &self.prices[start_index..] {
            high = f64::max(high, price_point.price);
            low = f64::min(low, price_point.price);
        }

        (high, low)
    }
}
