//! Module for visualizing backtest results and candle charts.
//!
//! It needs to enable `draws` feature to use it. Take a look at [trailing stop](https://github.com/raonagos/bts-rs/blob/master/examples/trailing_stop.rs#L70) for example.
#![allow(unused_variables)]
#![allow(unused_mut)]

use crate::engine::{Backtest, Candle};
use crate::errors::{Error, Result};
#[cfg(feature = "metrics")]
use crate::metrics::{Event, Metrics};

use chrono::Duration;
use plotters::backend::{BitMapBackend, DrawingBackend, SVGBackend};
use plotters::coord::Shift;
use plotters::prelude::*;
use plotters::style::WHITE;

/// Aspect ratio for the generated charts.
const ASPECT_RATIO: f64 = 0.5625;
/// Size of the X-axis labels.
const X_LABEL_SIZE: i32 = 20;
/// Size of the Y-axis labels.
const Y_LABEL_SIZE: i32 = 20;

/// Output formats for the generated charts with output filename.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum DrawOutput {
    /// Save to the output SVG file.
    Svg(String),
    /// Save to the output PNG file.
    Png(String),
    /// Save to the output HTML file (not implemented).
    Html(String),
    /// Print to the current console (not implemented).
    #[default]
    Inner,
}

/// Configuration options for chart generation.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub struct DrawOptions {
    /// Chart title.
    title: Option<String>,
    /// Output format and path.
    output: DrawOutput,
    /// Whether to show the volume chart.
    show_volume: bool,
    #[cfg(feature = "metrics")]
    /// Whether to show the metrics chart.
    show_metrics: bool,
}

impl DrawOptions {
    /// Sets the chart title.
    pub fn title(mut self, title: impl ToString) -> Self {
        self.title = Some(title.to_string());
        self
    }

    /// Sets the output format and path.
    pub fn draw_output(mut self, output: DrawOutput) -> Self {
        self.output = output;
        self
    }

    /// Enables or disables the volume chart.
    pub fn show_volume(mut self, show: bool) -> Self {
        self.show_volume = show;
        self
    }

    #[cfg(feature = "metrics")]
    /// Enables or disables the metrics chart.
    pub fn show_metrics(mut self, show: bool) -> Self {
        self.show_metrics = show;
        self
    }
}

/// Chart drawing utility for backtest visualization.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Draw {
    candles: Vec<Candle>,
    #[cfg(feature = "metrics")]
    metrics: Metrics,
    options: DrawOptions,
}

impl From<&Backtest> for Draw {
    fn from(value: &Backtest) -> Self {
        Self {
            options: DrawOptions::default(),
            #[cfg(feature = "metrics")]
            metrics: Metrics::from(value),
            candles: value.candles().cloned().collect(),
        }
    }
}

impl Draw {
    /// Creates a new `Draw` instance.
    pub fn new(candles: Vec<Candle>, options: DrawOptions, #[cfg(feature = "metrics")] metrics: Metrics) -> Self {
        Self {
            candles,
            #[cfg(feature = "metrics")]
            metrics,
            options,
        }
    }

    /// Sets the drawing options.
    pub fn with_options(mut self, options: DrawOptions) -> Self {
        self.options = options;
        self
    }

    /// Generates and saves the chart based on the configured options.
    pub fn plot(&self) -> Result<()> {
        let candles = &self.candles;
        if candles.is_empty() {
            return Err(Error::CandleDataEmpty);
        }

        let title = self.options.title.as_deref().unwrap_or("BTS Chart");
        let mut height_factor = 1.0;
        if self.options.show_volume {
            height_factor += 0.4;
        }
        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            height_factor += 0.4;
        }

        let candle_count = candles.len() as u32;
        let width = 1280.max(10 * candle_count);
        let height = ((width as f64 * ASPECT_RATIO * height_factor) as u32).min(900);

        match &self.options.output {
            DrawOutput::Svg(path) => self.plot_svg(path, width, height, title),
            DrawOutput::Png(path) => self.plot_png(path, width, height, title),
            DrawOutput::Html(path) => self.plot_html(path, width, height, title),
            DrawOutput::Inner => self.plot_inner(width, height, title),
        }
    }

    /// Saves the chart as an SVG file.
    fn plot_svg(&self, path: &str, width: u32, height: u32, title: &str) -> Result<()> {
        let root = SVGBackend::new(path, (width, height)).into_drawing_area();
        root.fill(&WHITE).map_err(|e| Error::Plotters(e.to_string()))?;
        self.draw_chart(&root, title)
    }

    /// Saves the chart as a PNG file.
    fn plot_png(&self, path: &str, width: u32, height: u32, title: &str) -> Result<()> {
        let root = BitMapBackend::new(path, (width, height)).into_drawing_area();
        root.fill(&WHITE).map_err(|e| Error::Plotters(e.to_string()))?;
        self.draw_chart(&root, title)
    }

    /// Saves the chart as an HTML file (not implemented).
    fn plot_html(&self, _path: &str, _width: u32, _height: u32, _title: &str) -> Result<()> {
        Err(Error::Msg("HTML output is not implemented".to_string()))
    }

    /// Displays the chart in the current console (not implemented).
    fn plot_inner(&self, _width: u32, _height: u32, _title: &str) -> Result<()> {
        Err(Error::Msg("Inner display is not implemented".to_string()))
    }

    /// Draws the main chart with price, volume, and metrics.
    fn draw_chart<DB: DrawingBackend>(&self, drawing_area: &DrawingArea<DB, Shift>, title: &str) -> Result<()> {
        let total_height = drawing_area.dim_in_pixel().1 as f64;
        let mut volume_height = 0.0;
        if self.options.show_volume {
            volume_height = total_height * 0.2;
        }

        let mut metrics_height = 0.0;
        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            metrics_height = total_height * 0.2;
        }

        let price_height = total_height - volume_height - metrics_height;

        let (mut metrics_area, mut rest_area) = (drawing_area.clone(), drawing_area.clone());
        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            (metrics_area, rest_area) = drawing_area.split_vertically(metrics_height as u32)
        }

        let (price_area, volume_area) = if self.options.show_volume {
            rest_area.split_vertically(price_height as u32)
        } else {
            (rest_area.clone(), rest_area.clone())
        };

        // draw all charts
        self.draw_price_chart(&price_area, title)?;
        if self.options.show_volume {
            self.draw_volume_chart(&volume_area)?;
        }
        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            self.draw_metrics_chart(&metrics_area)?;
        }

        drawing_area.present().map_err(|e| Error::Plotters(e.to_string()))
    }

    /// Draws the price chart (candlesticks).
    fn draw_price_chart<DB: DrawingBackend>(&self, drawing_area: &DrawingArea<DB, Shift>, title: &str) -> Result<()> {
        let min_price = self.candles.iter().map(|c| c.low()).fold(f64::INFINITY, f64::min);
        let max_price = self.candles.iter().map(|c| c.high()).fold(f64::NEG_INFINITY, f64::max);
        let first_time = self.candles.first().ok_or(Error::CandleNotFound)?.open_time();
        let last_time = self.candles.last().ok_or(Error::CandleNotFound)?.close_time();
        let price_range = max_price - min_price;
        let price_padding = price_range * 0.1;

        #[cfg(feature = "metrics")]
        let balances = self
            .metrics
            .events()
            .filter_map(|evt| match evt {
                Event::WalletUpdate { datetime, balance, .. } => Some((*datetime, *balance)),
                _ => None,
            })
            // unique by time
            .fold(Vec::new(), |mut acc, (datetime, balance)| {
                if !acc.iter().any(|(d, _)| d == &datetime) {
                    acc.push((datetime, balance));
                }
                acc
            });

        #[cfg(not(feature = "metrics"))]
        let (min_balance, max_balance) = (0.0, 0.0);
        #[cfg(feature = "metrics")]
        let (min_balance, max_balance) = (
            balances.iter().map(|(_, b)| *b).fold(f64::INFINITY, f64::min),
            balances.iter().map(|(_, b)| *b).fold(f64::NEG_INFINITY, f64::max),
        );

        let (top, bottom) = if self.options.show_volume { (0, 0) } else { (10, 10) };
        let drawing_area = drawing_area.margin(top, bottom, 70, 70);
        let mut builder = ChartBuilder::on(&drawing_area);
        if !self.options.show_volume {
            builder.x_label_area_size(X_LABEL_SIZE);
        }

        let mut chart = builder
            .caption(title, ("sans-serif", 30).into_font())
            .y_label_area_size(Y_LABEL_SIZE)
            .right_y_label_area_size(Y_LABEL_SIZE)
            .build_cartesian_2d(
                first_time..last_time,
                min_price - price_padding..max_price + price_padding,
            )
            .map_err(|e| Error::Plotters(e.to_string()))?
            .set_secondary_coord(first_time..last_time, min_balance..max_balance);

        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            chart
                .configure_secondary_axes()
                .y_desc("Balance")
                .label_style(("sans-serif", Y_LABEL_SIZE))
                .y_labels(5)
                .draw()
                .map_err(|e| Error::Plotters(e.to_string()))?;
        }

        let candle_count = self.candles.len();
        let x_labels = candle_count / 15;

        let mut mesh = chart.configure_mesh();
        mesh.y_desc("Price")
            .y_label_style(("sans-serif", Y_LABEL_SIZE))
            .y_labels(5);

        if self.options.show_volume {
            mesh.disable_x_axis();
        } else {
            mesh.x_desc("Time")
                .x_label_style(("sans-serif", X_LABEL_SIZE))
                .x_labels(x_labels);
        }

        mesh.draw().map_err(|e| Error::Plotters(e.to_string()))?;

        let candle_width = {
            let total_width = drawing_area.dim_in_pixel().0 as f64;
            let available_width = total_width - (X_LABEL_SIZE * 2) as f64;
            (available_width / candle_count as f64).max(5.0) as u32
        };

        chart
            .draw_series(self.candles.iter().map(|c| {
                let x = c.open_time();
                let open = c.open();
                let high = c.high();
                let low = c.low();
                let close = c.close();
                let color = if close >= open { GREEN.filled() } else { RED.filled() };
                CandleStick::new(x, open, high, low, close, color, color, candle_width)
            }))
            .map_err(|e| Error::Plotters(e.to_string()))?;

        #[cfg(feature = "metrics")]
        if self.options.show_metrics {
            use crate::PercentCalculus;

            let initial_balance = self.metrics.initial_balance();
            let red_balances = balances.iter().filter(|(_, balance)| *balance < initial_balance);
            let blue_balances = balances.iter().filter(|(_, balance)| *balance >= initial_balance);

            let opened_positions = self
                .metrics
                .events()
                .filter_map(|e| match e {
                    Event::AddPosition(date_time, position) => Some((date_time, position.entry_price())),
                    _ => None,
                })
                .map(|(datetime, price)| {
                    Circle::new(
                        (*datetime, price.expect("Invalid price").addpercent(5.0)),
                        2,
                        BLUE.filled(),
                    )
                });
            let closed_positions = self
                .metrics
                .events()
                .filter_map(|e| match e {
                    Event::DelPosition(date_time, position) => Some((date_time, position.entry_price())),
                    _ => None,
                })
                .map(|(datetime, price)| {
                    Circle::new(
                        (*datetime, price.expect("Invalid price").addpercent(5.0)),
                        2,
                        RED.filled(),
                    )
                });

            chart
                .draw_series(opened_positions)
                .map_err(|e| Error::Plotters(e.to_string()))?;
            chart
                .draw_series(closed_positions)
                .map_err(|e| Error::Plotters(e.to_string()))?;

            chart
                .draw_secondary_series(LineSeries::new(
                    blue_balances.map(|(datetime, balance)| (*datetime, *balance)),
                    BLUE,
                ))
                .map_err(|e| Error::Plotters(e.to_string()))?;

            chart
                .draw_secondary_series(LineSeries::new(
                    red_balances.map(|(datetime, balance)| (*datetime, *balance)),
                    RED,
                ))
                .map_err(|e| Error::Plotters(e.to_string()))?;
        }

        Ok(())
    }

    /// Draws the volume chart.
    fn draw_volume_chart<DB: DrawingBackend>(&self, drawing_area: &DrawingArea<DB, Shift>) -> Result<()> {
        let max_volume = self
            .candles
            .iter()
            .map(|c| c.volume())
            .fold(f64::NEG_INFINITY, f64::max);
        let volume_padding = max_volume * 0.1;
        let first_time = self.candles.first().ok_or(Error::CandleNotFound)?.open_time();
        let last_time = self.candles.last().ok_or(Error::CandleNotFound)?.close_time();
        let drawing_area = drawing_area.margin(0, 10, 70, 70);

        let mut chart = ChartBuilder::on(&drawing_area)
            .x_label_area_size(X_LABEL_SIZE)
            .y_label_area_size(Y_LABEL_SIZE)
            .build_cartesian_2d(first_time..last_time, 0.0..max_volume + volume_padding)
            .map_err(|e| Error::Plotters(e.to_string()))?;

        let candle_count = self.candles.len();
        let x_labels = candle_count / 15;

        chart
            .configure_mesh()
            .x_desc("Time")
            .x_label_style(("sans-serif", X_LABEL_SIZE))
            .y_label_style(("sans-serif", Y_LABEL_SIZE))
            .x_labels(x_labels)
            .y_labels(3)
            .draw()
            .map_err(|e| Error::Plotters(e.to_string()))?;

        chart
            .draw_series(self.candles.iter().map(|c| {
                let x = c.open_time();
                let volume = c.volume();
                let color = if c.ask() >= c.bid() {
                    GREEN.mix(0.3)
                } else {
                    RED.mix(0.3)
                };
                Rectangle::new([(x, 0.0), (x + Duration::days(1), volume)], color.filled())
            }))
            .map(|_| ())
            .map_err(|e| Error::Plotters(e.to_string()))
    }

    /// Draws the metrics chart (if the "metrics" feature is enabled).
    #[cfg(feature = "metrics")]
    fn draw_metrics_chart<DB: DrawingBackend>(&self, drawing_area: &DrawingArea<DB, Shift>) -> Result<()> {
        let max_drawdown = self.metrics.max_drawdown();
        let profit_factor = self.metrics.profit_factor();
        let sharpe_ratio = self.metrics.sharpe_ratio(0.0);
        let win_rate = self.metrics.win_rate();

        let drawing_area = drawing_area.margin(30, 0, 70, 70);
        let mut metrics_chart = ChartBuilder::on(&drawing_area)
            .margin(20)
            .build_cartesian_2d(0.0..1.0, 0f64..100f64)
            .map_err(|e| Error::Plotters(e.to_string()))?;

        metrics_chart
            .configure_mesh()
            .disable_x_mesh()
            .disable_y_mesh()
            .draw()
            .map_err(|e| Error::Plotters(e.to_string()))?;

        let text = Text::new(
            format!(
                "Max Drawdown: {:.2}% | Profit Factor: {:.2} | Sharpe Ratio: {:.2} | Win Rate: {:.2}%",
                max_drawdown, profit_factor, sharpe_ratio, win_rate
            ),
            (0.0, 50.0),
            ("sans-serif", 28).into_font(),
        );

        metrics_chart
            .draw_series([text])
            .map(|_| ())
            .map_err(|e| Error::Plotters(e.to_string()))
    }
}
