//! Interactive chart viewer with keyboard-driven pan and zoom.
//!
//! Renders a chart inline (80x24 by default, not fullscreen/alternate screen)
//! and lets the user scroll through data with arrow keys and zoom in/out
//! with `+`/`-`. Press `q` or `Esc` to exit.
//!
//! Uses the same inline rendering pattern as `realtime_btc.rs`:
//! - Reserve vertical space with blank lines
//! - Query cursor position for an anchor row
//! - Use `MoveTo(0, anchor_row)` each frame
//! - Replace `'\n'` with `'\r\n'` for raw mode
//! - Wrap each frame in `BeginSynchronizedUpdate` / `EndSynchronizedUpdate`

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

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

use termichart_core::{Candle, ChartType, Point};

use crate::ChartBuilder;

/// The default number of visible items when the viewer first opens.
const DEFAULT_VISIBLE: usize = 40;
/// The minimum number of items that can be visible (maximum zoom-in).
const MIN_VISIBLE: usize = 5;
/// How many items to pan per arrow key press.
const PAN_STEP: usize = 3;
/// How many items to add/remove per zoom step.
const ZOOM_STEP: usize = 5;

/// An interactive chart viewer that renders inline and responds to keyboard
/// input for panning and zooming through data.
pub struct InteractiveChart {
    candles: Vec<Candle>,
    points: Vec<Point>,
    chart_type: ChartType,
    width: u16,
    height: u16,
    // View state -------------------------------------------------------
    /// The start index into the data array for the current view window.
    offset: usize,
    /// How many data items are currently visible.
    visible_count: usize,
}

impl InteractiveChart {
    /// Creates a new interactive candlestick chart viewer.
    pub fn new_candlestick(candles: Vec<Candle>) -> Self {
        let len = candles.len();
        let visible = DEFAULT_VISIBLE.min(len).max(MIN_VISIBLE.min(len));
        let offset = len.saturating_sub(visible);
        Self {
            candles,
            points: Vec::new(),
            chart_type: ChartType::Candlestick,
            width: 80,
            height: 24,
            offset,
            visible_count: visible,
        }
    }

    /// Creates a new interactive line chart viewer.
    pub fn new_line(points: Vec<Point>) -> Self {
        let len = points.len();
        let visible = DEFAULT_VISIBLE.min(len).max(MIN_VISIBLE.min(len));
        let offset = len.saturating_sub(visible);
        Self {
            candles: Vec::new(),
            points,
            chart_type: ChartType::Line,
            width: 80,
            height: 24,
            offset,
            visible_count: visible,
        }
    }

    /// Sets custom dimensions for the chart (default is 80x24).
    pub fn size(mut self, width: u16, height: u16) -> Self {
        self.width = width;
        self.height = height;
        self
    }

    /// Returns the total number of data items.
    fn data_len(&self) -> usize {
        match self.chart_type {
            ChartType::Candlestick => self.candles.len(),
            ChartType::Line => self.points.len(),
            // Bar and Histogram are not supported in interactive mode;
            // treat as having no data.
            ChartType::Bar | ChartType::Histogram => 0,
        }
    }

    /// The maximum number of visible items (all data).
    fn max_visible(&self) -> usize {
        self.data_len()
    }

    /// Clamp `offset` and `visible_count` to valid bounds.
    fn clamp_view(&mut self) {
        let len = self.data_len();
        if len == 0 {
            self.offset = 0;
            self.visible_count = 0;
            return;
        }

        // Clamp visible_count.
        self.visible_count = self.visible_count.clamp(MIN_VISIBLE.min(len), len);

        // Clamp offset so the window stays within bounds.
        if self.offset + self.visible_count > len {
            self.offset = len.saturating_sub(self.visible_count);
        }
    }

    // -- Navigation helpers ---------------------------------------------

    fn pan_left(&mut self) {
        self.offset = self.offset.saturating_sub(PAN_STEP);
    }

    fn pan_right(&mut self) {
        let len = self.data_len();
        self.offset = (self.offset + PAN_STEP).min(len.saturating_sub(self.visible_count));
    }

    fn zoom_in(&mut self) {
        let new_visible = self.visible_count.saturating_sub(ZOOM_STEP);
        let min = MIN_VISIBLE.min(self.data_len());
        self.visible_count = new_visible.max(min);

        // Adjust offset to keep the view centred after zoom.
        let len = self.data_len();
        if self.offset + self.visible_count > len {
            self.offset = len.saturating_sub(self.visible_count);
        }
    }

    fn zoom_out(&mut self) {
        let max = self.max_visible();
        self.visible_count = (self.visible_count + ZOOM_STEP).min(max);

        // Adjust offset to keep the window within bounds.
        let len = self.data_len();
        if self.offset + self.visible_count > len {
            self.offset = len.saturating_sub(self.visible_count);
        }
    }

    // -- Rendering ------------------------------------------------------

    /// Render the current visible window to a string via `ChartBuilder`.
    fn render_frame(&self) -> Result<String, Box<dyn std::error::Error>> {
        // chart_height = total height minus 1 row for the status bar
        let chart_height = self.height.saturating_sub(1);

        let output = match self.chart_type {
            ChartType::Candlestick => {
                let end = (self.offset + self.visible_count).min(self.candles.len());
                let slice = self.candles[self.offset..end].to_vec();

                ChartBuilder::candlestick()
                    .size(self.width, chart_height)
                    .candles(slice)
                    .render()?
            }
            ChartType::Line => {
                let end = (self.offset + self.visible_count).min(self.points.len());
                let slice = self.points[self.offset..end].to_vec();

                ChartBuilder::line()
                    .size(self.width, chart_height)
                    .points(slice)
                    .render()?
            }
            ChartType::Bar | ChartType::Histogram => {
                // Not supported in interactive mode; render empty.
                String::new()
            }
        };

        Ok(output)
    }

    /// Build the status bar string that appears below the chart.
    fn status_bar(&self) -> String {
        let len = self.data_len();
        let end = (self.offset + self.visible_count).min(len);
        let zoom_pct = if len > 0 {
            (self.visible_count as f64 / len as f64 * 100.0).round() as usize
        } else {
            100
        };

        // Format: inverse-video label, then info, then dimmed help.
        format!(
            "\x1b[7m INTERACTIVE \x1b[0m  items {}-{} of {}  |  visible: {}  |  zoom: {}%  \x1b[2m\u{2190}\u{2192} pan  +/- zoom  q quit\x1b[0m\x1b[K",
            self.offset + 1,
            end,
            len,
            self.visible_count,
            zoom_pct,
        )
    }

    /// Runs the interactive loop. Blocks until user presses `q` or `Esc`.
    ///
    /// The chart is rendered inline (not in an alternate screen). The method
    /// enables raw mode, hides the cursor, and restores the terminal on exit.
    pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let mut stdout = std::io::stdout();

        if self.data_len() == 0 {
            eprintln!("InteractiveChart: no data to display.");
            return Ok(());
        }

        self.clamp_view();

        // total_lines = chart height + 1 status bar row
        let total_lines = self.height + 1;

        // Reserve vertical space so inline rendering does not scroll off.
        for _ in 0..total_lines {
            writeln!(stdout)?;
        }
        stdout.flush()?;

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

        // Query cursor position to compute the anchor row.
        let (_, cursor_row) = cursor::position()?;
        let anchor_row = cursor_row.saturating_sub(total_lines);

        // Draw the first frame immediately before entering the event loop.
        self.draw_frame(&mut stdout, anchor_row)?;

        let poll_timeout = Duration::from_millis(50);

        loop {
            if !event::poll(poll_timeout)? {
                continue;
            }

            let evt = event::read()?;
            let mut needs_redraw = false;

            match evt {
                Event::Key(key) => {
                    if key.kind != KeyEventKind::Press {
                        continue;
                    }
                    match key.code {
                        KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => break,
                        KeyCode::Left => {
                            self.pan_left();
                            needs_redraw = true;
                        }
                        KeyCode::Right => {
                            self.pan_right();
                            needs_redraw = true;
                        }
                        KeyCode::Char('+') | KeyCode::Char('=') => {
                            self.zoom_in();
                            needs_redraw = true;
                        }
                        KeyCode::Char('-') | KeyCode::Char('_') => {
                            self.zoom_out();
                            needs_redraw = true;
                        }
                        _ => {}
                    }
                }
                Event::Resize(w, h) => {
                    self.width = w.min(200);
                    self.height = h.min(60).saturating_sub(1); // leave room
                    needs_redraw = true;
                }
                _ => {}
            }

            if needs_redraw {
                self.clamp_view();
                self.draw_frame(&mut stdout, anchor_row)?;
            }
        }

        // Cleanup: restore terminal state.
        execute!(
            stdout,
            cursor::MoveTo(0, anchor_row + total_lines),
            cursor::Show
        )?;
        terminal::disable_raw_mode()?;

        Ok(())
    }

    /// Render one frame to stdout at the given anchor row.
    fn draw_frame(
        &self,
        stdout: &mut std::io::Stdout,
        anchor_row: u16,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let chart_output = self.render_frame()?;
        // In raw mode newlines must be \r\n for correct positioning.
        let chart_output = chart_output.replace('\n', "\r\n");
        let status = self.status_bar();

        queue!(
            stdout,
            BeginSynchronizedUpdate,
            cursor::MoveTo(0, anchor_row)
        )?;
        stdout.write_all(chart_output.as_bytes())?;
        // Write the status bar on the last line.
        write!(stdout, "\r{}", status)?;
        queue!(stdout, EndSynchronizedUpdate)?;
        stdout.flush()?;

        Ok(())
    }
}
