//! Multi-chart dashboard that displays tiled live charts in a 2x2 grid.
//!
//! Uses the same inline rendering pattern as `realtime_btc.rs` and
//! [`InteractiveChart`](crate::InteractiveChart): reserve vertical space,
//! compute an anchor row, then redraw in-place with `MoveTo` and
//! `BeginSynchronizedUpdate`/`EndSynchronizedUpdate`.
//!
//! # Example
//!
//! ```no_run
//! use termichart_charts::Dashboard;
//!
//! // Run a 4-panel crypto dashboard at 100x30
//! Dashboard::default_crypto()
//!     .size(100, 30)
//!     .run()
//!     .unwrap();
//! ```

use std::io::Write;
use std::time::Duration;

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent},
    execute, queue,
    terminal::{self, BeginSynchronizedUpdate, EndSynchronizedUpdate},
};

use termichart_core::{Candle, OverlayKind};
use termichart_data::{Exchange, ExchangeClient};

use crate::ChartBuilder;

/// A single panel inside the dashboard, bound to one symbol on one exchange.
struct DashboardPanel {
    /// Trading pair or coin id (e.g. `"BTCUSDT"`).
    symbol: String,
    /// Which exchange to fetch data from.
    exchange: Exchange,
    /// Cached candle data; refreshed every poll cycle.
    candles: Vec<Candle>,
}

/// Multi-chart dashboard that renders several candlestick charts side by side
/// in a tiled 2x2 layout, refreshing live from exchange data.
pub struct Dashboard {
    panels: Vec<DashboardPanel>,
    /// Total terminal width (columns) to occupy.
    width: u16,
    /// Total terminal height (rows) to occupy, including the status bar.
    height: u16,
    /// Polling interval for data refresh.
    poll_interval: Duration,
}

impl Dashboard {
    /// Create a dashboard with four default crypto symbols from Binance:
    /// BTC/USDT, ETH/USDT, SOL/USDT, and BNB/USDT.
    pub fn default_crypto() -> Self {
        Self {
            panels: vec![
                DashboardPanel {
                    symbol: "BTCUSDT".into(),
                    exchange: Exchange::Binance,
                    candles: vec![],
                },
                DashboardPanel {
                    symbol: "ETHUSDT".into(),
                    exchange: Exchange::Binance,
                    candles: vec![],
                },
                DashboardPanel {
                    symbol: "SOLUSDT".into(),
                    exchange: Exchange::Binance,
                    candles: vec![],
                },
                DashboardPanel {
                    symbol: "BNBUSDT".into(),
                    exchange: Exchange::Binance,
                    candles: vec![],
                },
            ],
            width: 80,
            height: 24,
            poll_interval: Duration::from_secs(5),
        }
    }

    /// Create a dashboard with custom symbols, all fetched from the same
    /// exchange.  Up to 4 symbols are supported (extras are ignored).
    pub fn with_symbols(symbols: Vec<String>, exchange: Exchange) -> Self {
        let panels = symbols
            .into_iter()
            .take(4)
            .map(|s| DashboardPanel {
                symbol: s,
                exchange,
                candles: vec![],
            })
            .collect();
        Self {
            panels,
            width: 80,
            height: 24,
            poll_interval: Duration::from_secs(5),
        }
    }

    /// Set the total size (columns x rows) of the dashboard area.
    pub fn size(mut self, width: u16, height: u16) -> Self {
        self.width = width;
        self.height = height;
        self
    }

    /// Set the polling interval for data refresh (default: 5 seconds).
    pub fn interval(mut self, interval: Duration) -> Self {
        self.poll_interval = interval;
        self
    }

    // -- internal helpers ---------------------------------------------------

    /// How many candles fit in a single panel given its width.
    /// Each candle body is 3 columns wide with a 1-column gap, and the y-axis
    /// label area takes about 10 columns.
    fn candles_for_width(panel_width: u16) -> usize {
        let chart_cols = (panel_width as usize).saturating_sub(10);
        (chart_cols / 4).max(5)
    }

    /// Compute panel dimensions: (panel_width, panel_height).
    ///
    /// The grid is 2 columns wide and 2 rows tall; the last row of the total
    /// area is reserved for the status bar.
    fn panel_dims(&self) -> (u16, u16) {
        let cols = if self.panels.len() <= 1 {
            1u16
        } else {
            2u16
        };
        let rows = if self.panels.len() <= 2 {
            1u16
        } else {
            2u16
        };
        let pw = self.width / cols;
        // Reserve 1 row for the status bar at the bottom.
        let ph = self.height.saturating_sub(1) / rows;
        (pw, ph)
    }

    /// Fetch candle data for every panel, updating `self.panels[i].candles`.
    fn refresh_data(&mut self) {
        let (pw, _) = self.panel_dims();
        let num_candles = Self::candles_for_width(pw);
        for panel in &mut self.panels {
            let client = ExchangeClient::new(panel.exchange);
            if let Some(candles) = client.fetch_candles(&panel.symbol, "1m", num_candles) {
                if !candles.is_empty() {
                    panel.candles = candles;
                }
            }
        }
    }

    /// Render one panel as a small candlestick chart string.
    fn render_panel(panel: &DashboardPanel, pw: u16, ph: u16) -> String {
        if panel.candles.is_empty() {
            // Return an empty placeholder of the right size.
            let blank_line = " ".repeat(pw as usize);
            return (0..ph)
                .map(|_| blank_line.clone())
                .collect::<Vec<_>>()
                .join("\n");
        }

        let last = panel.candles.last().unwrap();
        let bull = last.close >= last.open;
        let arrow = if bull { "\u{25B2}" } else { "\u{25BC}" };
        let title = format!(
            "{} {} ${:.2}",
            format_symbol(&panel.symbol),
            arrow,
            last.close
        );

        match ChartBuilder::candlestick()
            .size(pw, ph)
            .title(title)
            .heikin_ashi()
            .overlay(OverlayKind::Sma(7))
            .candles(panel.candles.clone())
            .render()
        {
            Ok(s) => s,
            Err(_) => {
                let blank_line = " ".repeat(pw as usize);
                (0..ph)
                    .map(|_| blank_line.clone())
                    .collect::<Vec<_>>()
                    .join("\n")
            }
        }
    }

    /// Combine two chart strings side by side.  Each chart is split into
    /// lines, then corresponding lines are concatenated.
    fn merge_horizontal(left: &str, right: &str, left_width: u16) -> String {
        let left_lines: Vec<&str> = left.lines().collect();
        let right_lines: Vec<&str> = right.lines().collect();
        let max_rows = left_lines.len().max(right_lines.len());
        let lw = left_width as usize;

        let mut merged = String::new();
        for i in 0..max_rows {
            let l = left_lines.get(i).copied().unwrap_or("");
            let r = right_lines.get(i).copied().unwrap_or("");

            // Pad left panel to exactly `lw` visible characters.
            // We must account for ANSI escape sequences which have zero display
            // width.
            let visible_len = strip_ansi_len(l);
            merged.push_str(l);
            if visible_len < lw {
                for _ in 0..(lw - visible_len) {
                    merged.push(' ');
                }
            }
            merged.push_str(r);
            if i + 1 < max_rows {
                merged.push('\n');
            }
        }
        merged
    }

    /// Build the full frame string by rendering each panel and tiling them
    /// in a 2x2 grid.
    fn build_frame(&self) -> String {
        let (pw, ph) = self.panel_dims();
        let panel_count = self.panels.len();

        // Render each panel.
        let rendered: Vec<String> = self
            .panels
            .iter()
            .map(|p| Self::render_panel(p, pw, ph))
            .collect();

        match panel_count {
            0 => String::new(),
            1 => rendered[0].clone(),
            2 => Self::merge_horizontal(&rendered[0], &rendered[1], pw),
            3 => {
                let top = Self::merge_horizontal(&rendered[0], &rendered[1], pw);
                // Third panel alone on the bottom row.
                let blank = " ".repeat(pw as usize);
                let blank_block: String = (0..ph)
                    .map(|i| {
                        if i + 1 < ph {
                            format!("{}\n", blank)
                        } else {
                            blank.clone()
                        }
                    })
                    .collect();
                let bottom = Self::merge_horizontal(&rendered[2], &blank_block, pw);
                format!("{}\n{}", top, bottom)
            }
            _ => {
                // 4 panels: 2x2 grid
                let top = Self::merge_horizontal(&rendered[0], &rendered[1], pw);
                let bottom = Self::merge_horizontal(&rendered[2], &rendered[3], pw);
                format!("{}\n{}", top, bottom)
            }
        }
    }

    /// Build the status bar shown below the charts.
    fn status_bar(&self, elapsed_secs: u64) -> String {
        let symbols: Vec<&str> = self.panels.iter().map(|p| p.symbol.as_str()).collect();
        format!(
            "\x1b[7m DASHBOARD \x1b[0m  {}  |  {}:{:02}  \x1b[2m'q' quit\x1b[0m\x1b[K",
            symbols.join("  "),
            elapsed_secs / 60,
            elapsed_secs % 60,
        )
    }

    /// Run the live dashboard. Blocks until the user presses `q` or `Esc`.
    ///
    /// The dashboard is rendered inline (not in an alternate screen) using the
    /// same anchor-row pattern as the other interactive views in this crate.
    pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let mut stdout = std::io::stdout();

        if self.panels.is_empty() {
            eprintln!("Dashboard: no panels configured.");
            return Ok(());
        }

        // Total rows = chart area + 1 status bar.
        let total_lines = self.height;

        // Reserve vertical space.
        for _ in 0..total_lines {
            writeln!(stdout)?;
        }
        stdout.flush()?;

        // Enter raw mode and hide cursor.
        terminal::enable_raw_mode()?;
        execute!(stdout, cursor::Hide)?;

        let (_, cursor_row) = cursor::position()?;
        let anchor_row = cursor_row.saturating_sub(total_lines);

        let start = std::time::Instant::now();
        let mut last_fetch = std::time::Instant::now() - self.poll_interval; // force immediate fetch

        loop {
            // Fetch data if the poll interval has elapsed.
            if last_fetch.elapsed() >= self.poll_interval {
                self.refresh_data();
                last_fetch = std::time::Instant::now();
            }

            // Render the frame.
            let frame = self.build_frame();
            let frame = frame.replace('\n', "\r\n");
            let status = self.status_bar(start.elapsed().as_secs());

            queue!(
                stdout,
                BeginSynchronizedUpdate,
                cursor::MoveTo(0, anchor_row)
            )?;
            stdout.write_all(frame.as_bytes())?;
            write!(stdout, "\r\n{}", status)?;
            queue!(stdout, EndSynchronizedUpdate)?;
            stdout.flush()?;

            // Poll for key events until the next refresh is due.
            let remaining = self
                .poll_interval
                .checked_sub(last_fetch.elapsed())
                .unwrap_or(Duration::from_millis(50));

            if event::poll(remaining)? {
                if let Event::Key(KeyEvent { code, .. }) = event::read()? {
                    match code {
                        KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => break,
                        KeyCode::Char('c') => break,
                        _ => {}
                    }
                }
            }
        }

        // Restore terminal state.
        execute!(
            stdout,
            cursor::MoveTo(0, anchor_row + total_lines),
            cursor::Show
        )?;
        terminal::disable_raw_mode()?;
        println!("Dashboard session ended.");

        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Utility helpers
// ---------------------------------------------------------------------------

/// Return the visible (non-ANSI) length of a string.
///
/// Strips CSI sequences (`\x1b[...m`, `\x1b[...H`, etc.) to count only
/// printable characters.
fn strip_ansi_len(s: &str) -> usize {
    let mut len = 0usize;
    let mut in_esc = false;
    for ch in s.chars() {
        if in_esc {
            // CSI sequences end with a letter (A-Z, a-z) or `m`.
            if ch.is_ascii_alphabetic() {
                in_esc = false;
            }
        } else if ch == '\x1b' {
            in_esc = true;
        } else {
            len += 1;
        }
    }
    len
}

/// Format a symbol for display: `"BTCUSDT"` -> `"BTC/USDT"`.
fn format_symbol(sym: &str) -> String {
    // Try common quote currencies.
    for quote in &["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB", "USD"] {
        if sym.ends_with(quote) && sym.len() > quote.len() {
            let base = &sym[..sym.len() - quote.len()];
            return format!("{}/{}", base, quote);
        }
    }
    sym.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_symbol() {
        assert_eq!(format_symbol("BTCUSDT"), "BTC/USDT");
        assert_eq!(format_symbol("ETHUSDT"), "ETH/USDT");
        assert_eq!(format_symbol("SOLUSDT"), "SOL/USDT");
        assert_eq!(format_symbol("BNBUSDT"), "BNB/USDT");
        assert_eq!(format_symbol("UNKNOWN"), "UNKNOWN");
    }

    #[test]
    fn test_strip_ansi_len() {
        assert_eq!(strip_ansi_len("hello"), 5);
        assert_eq!(strip_ansi_len("\x1b[31mred\x1b[0m"), 3);
        assert_eq!(strip_ansi_len("\x1b[1;32mbold green\x1b[0m"), 10);
        assert_eq!(strip_ansi_len(""), 0);
    }

    #[test]
    fn test_default_crypto_panels() {
        let d = Dashboard::default_crypto();
        assert_eq!(d.panels.len(), 4);
        assert_eq!(d.panels[0].symbol, "BTCUSDT");
        assert_eq!(d.panels[1].symbol, "ETHUSDT");
        assert_eq!(d.panels[2].symbol, "SOLUSDT");
        assert_eq!(d.panels[3].symbol, "BNBUSDT");
    }

    #[test]
    fn test_with_symbols() {
        let d = Dashboard::with_symbols(
            vec!["AAVEUSD".into(), "LINKUSD".into()],
            Exchange::Binance,
        );
        assert_eq!(d.panels.len(), 2);
        assert_eq!(d.panels[0].symbol, "AAVEUSD");
    }

    #[test]
    fn test_panel_dims() {
        let d = Dashboard::default_crypto().size(100, 30);
        let (pw, ph) = d.panel_dims();
        assert_eq!(pw, 50);
        // (30 - 1) / 2 = 14
        assert_eq!(ph, 14);
    }

    #[test]
    fn test_merge_horizontal_simple() {
        let left = "AAA\nBBB";
        let right = "111\n222";
        let merged = Dashboard::merge_horizontal(left, right, 4);
        // Left lines padded to 4 visible chars, then right appended.
        assert_eq!(merged, "AAA 111\nBBB 222");
    }

    #[test]
    fn test_candles_for_width() {
        // (40 - 10) / 4 = 7
        assert_eq!(Dashboard::candles_for_width(40), 7);
        // (10 - 10) / 4 = 0, clamped to 5
        assert_eq!(Dashboard::candles_for_width(10), 5);
    }

    #[test]
    fn test_build_frame_empty_panels() {
        let d = Dashboard {
            panels: vec![],
            width: 80,
            height: 24,
            poll_interval: Duration::from_secs(5),
        };
        assert_eq!(d.build_frame(), "");
    }

    #[test]
    fn test_status_bar() {
        let d = Dashboard::default_crypto();
        let bar = d.status_bar(125);
        assert!(bar.contains("BTCUSDT"));
        assert!(bar.contains("ETHUSDT"));
        assert!(bar.contains("2:05"));
    }
}
