//! Terminal-Host: stellt den Zellenpuffer über Ratatui dar und liefert
//! Tastatur- und Größenereignisse aus crossterm.
//!
//! Er lebt hier statt in `tb-runtime`, damit die Ausführungsschicht ohne
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).

use std::io::{self, IsTerminal, Stdout};
use std::time::{Duration, Instant};

use crossterm::event::{
    self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
    MouseEventKind,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;

use tb_runtime::host::{taste, umschalt, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;

use crate::screen::ScreenWidget;
use crate::signale::Signalquelle;

/// Host für laufende Programme im Terminal.
///
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
pub struct TerminalHost {
    guard: crate::terminal::TerminalGuard<Stdout>,
    terminal: Terminal<CrosstermBackend<Stdout>>,
    /// Nullpunkt der monotonen Zeit (`Host::jetzt_ms`).
    start: Instant,
    signale: Signalquelle,
    /// Zuletzt bekannte Darstellungsgröße in Zellen. Mausereignisse
    /// außerhalb dieser Fläche werden verworfen (design.md, D7).
    flaeche: (usize, usize),
}

impl TerminalHost {
    pub fn new() -> io::Result<Self> {
        if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
            return Err(io::Error::other("Kein interaktives Terminal"));
        }
        let guard = crate::terminal::TerminalGuard::enter(io::stdout())?;
        let out = io::stdout();
        let terminal = Terminal::new(CrosstermBackend::new(out))?;
        let flaeche = terminal
            .size()
            .map(|s| (s.width as usize, s.height as usize))
            .unwrap_or((80, 25));
        Ok(TerminalHost {
            guard,
            terminal,
            start: Instant::now(),
            signale: Signalquelle::neu(),
            flaeche,
        })
    }

    /// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt.
    pub fn groesse(&self) -> io::Result<(usize, usize)> {
        let s = self.terminal.size()?;
        Ok((s.width as usize, s.height as usize))
    }
}

impl Host for TerminalHost {
    fn shell(&mut self, command: &str) -> Result<Option<i32>, tb_runtime::errors::RuntimeError> {
        let result = self.guard.shell(command, &self.signale);
        let _ = self.terminal.clear();
        result
            .map(Some)
            .map_err(|_| tb_runtime::errors::RuntimeError(53))
    }
    fn present(&mut self, screen: &TextScreen) {
        let _ = self.terminal.draw(|f| {
            if f.area().width < 80 || f.area().height < 25 {
                f.render_widget(
                    ratatui::widgets::Paragraph::new("Terminal zu klein: Minimum 80x25"),
                    f.area(),
                );
            } else {
                f.render_widget(ScreenWidget(screen), f.area());
            }
        });
    }

    fn jetzt_ms(&mut self) -> u64 {
        self.start.elapsed().as_millis() as u64
    }

    fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
        loop {
            if let Some(e) = self.next_event(false) {
                return Some(e);
            }
            let timeout = match deadline_ms {
                Some(deadline) => {
                    let rest = deadline.saturating_sub(self.jetzt_ms());
                    if rest == 0 {
                        return None;
                    }
                    Duration::from_millis(rest.min(10))
                }
                None => Duration::from_millis(10),
            };
            // Begrenzt warten, damit auch SIGINT/SIGTERM ohne Terminaltaste
            // abgeholt werden. Die VM kennt dieses Backend-Intervall nicht.
            if event::poll(timeout).is_err() {
                return Some(Ereignis::Ende);
            }
        }
    }

    fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
        if blockierend {
            return self.warten(None);
        }
        loop {
            // Signale vor dem Terminal: sie liegen schon an, wenn wir hier
            // ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
            if let Some(n) = self.signale.abholen() {
                return Some(Ereignis::Signal(n));
            }
            if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
                return None;
            }
            match event::read() {
                Ok(Event::Key(k)) if k.kind == KeyEventKind::Press => {
                    if let Some(e) = taste_zu_ereignis(k) {
                        return Some(e);
                    }
                }
                Ok(Event::Resize(cols, rows)) => {
                    self.flaeche = (cols as usize, rows as usize);
                    return Some(Ereignis::Groesse {
                        cols: cols as usize,
                        rows: rows as usize,
                    });
                }
                Ok(Event::Mouse(m)) => {
                    if let Some(e) = maus_zu_ereignis(m, self.flaeche) {
                        return Some(e);
                    }
                }
                Ok(_) => {}
                Err(_) => return Some(Ereignis::Ende),
            }
        }
    }
}

/// crossterm-Maus → Ereignis in Zellenkoordinaten (design.md, D7).
///
/// crossterm zählt ab 0, der Dialekt ab 1 — umgerechnet wird hier, damit es
/// in der Laufzeit nur eine Zählweise gibt. Ereignisse außerhalb der
/// Darstellungsfläche und Ereignisse ohne Entsprechung liefern `None`.
pub fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
    let (cols, rows) = flaeche;
    let spalte = m.column as usize + 1;
    let zeile = m.row as usize + 1;
    if spalte > cols || zeile > rows {
        return None;
    }
    let knopf = |b: MouseButton| match b {
        MouseButton::Left => 1,
        MouseButton::Right => 2,
        MouseButton::Middle => 4,
    };
    let (art, taste) = match m.kind {
        MouseEventKind::Down(b) => (MausArt::Druck, knopf(b)),
        MouseEventKind::Up(b) => (MausArt::Loslassen, knopf(b)),
        MouseEventKind::Drag(b) => (MausArt::Bewegung, knopf(b)),
        MouseEventKind::Moved => (MausArt::Bewegung, 0),
        // Rollrad kennt das Vorbild nicht — Stufe 2 (PLAN.md).
        MouseEventKind::ScrollDown
        | MouseEventKind::ScrollUp
        | MouseEventKind::ScrollLeft
        | MouseEventKind::ScrollRight => return None,
    };
    let mut shift = 0u8;
    if m.modifiers.contains(KeyModifiers::SHIFT) {
        shift |= 1;
    }
    if m.modifiers.contains(KeyModifiers::CONTROL) {
        shift |= 2;
    }
    if m.modifiers.contains(KeyModifiers::ALT) {
        shift |= 4;
    }
    Some(Ereignis::Maus(MausEreignis {
        art,
        taste,
        shift,
        zeile,
        spalte,
    }))
}

/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
pub fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
    // Strg+Untbr bzw. Strg+C: Abbruchwunsch.
    if k.modifiers.contains(KeyModifiers::CONTROL)
        && matches!(k.code, KeyCode::Char('c') | KeyCode::Pause)
    {
        return Some(Ereignis::Abbruch);
    }
    let s = match k.code {
        KeyCode::Char(c) => c.to_string(),
        KeyCode::Enter => taste::ENTER.to_string(),
        KeyCode::Backspace => taste::BACKSPACE.to_string(),
        KeyCode::Esc => taste::ESC.to_string(),
        KeyCode::Tab | KeyCode::BackTab => taste::TAB.to_string(),
        // Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
        KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
        KeyCode::Home => taste::sonder(71),
        KeyCode::Up => taste::sonder(72),
        KeyCode::PageUp => taste::sonder(73),
        KeyCode::Left => taste::sonder(75),
        KeyCode::Right => taste::sonder(77),
        KeyCode::End => taste::sonder(79),
        KeyCode::Down => taste::sonder(80),
        KeyCode::PageDown => taste::sonder(81),
        KeyCode::Insert => taste::sonder(82),
        KeyCode::Delete => taste::sonder(83),
        _ => return None,
    };
    let mut shift = 0u8;
    if k.modifiers.contains(KeyModifiers::SHIFT) || k.code == KeyCode::BackTab {
        shift |= umschalt::SHIFT;
    }
    if k.modifiers.contains(KeyModifiers::CONTROL) {
        shift |= umschalt::CTRL;
    }
    if k.modifiers.contains(KeyModifiers::ALT) {
        shift |= umschalt::ALT;
    }
    Some(Ereignis::Taste(s, shift))
}

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

    #[test]
    fn backtab_ist_tab_mit_shift_auch_ohne_modifier() {
        for modifiers in [KeyModifiers::NONE, KeyModifiers::SHIFT, KeyModifiers::ALT] {
            assert_eq!(
                taste_zu_ereignis(KeyEvent::new(KeyCode::BackTab, modifiers)),
                taste_zu_ereignis(KeyEvent::new(KeyCode::Tab, modifiers | KeyModifiers::SHIFT))
            );
        }
    }

    #[test]
    fn zeichentaste_wird_zu_inkey_form() {
        let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
        assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into(), 0)));
    }

    #[test]
    fn sondertaste_traegt_fuehrendes_nullzeichen() {
        let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
        let Some(Ereignis::Taste(s, _)) = taste_zu_ereignis(k) else {
            panic!("keine Taste");
        };
        assert_eq!(s.chars().count(), 2);
        assert_eq!(s.chars().next(), Some('\0'));
        assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59
    }

    fn maus(kind: MouseEventKind, col: u16, row: u16, mods: KeyModifiers) -> MouseEvent {
        MouseEvent {
            kind,
            column: col,
            row,
            modifiers: mods,
        }
    }

    #[test]
    fn maus_zaehlt_ab_eins() {
        let e = maus_zu_ereignis(
            maus(
                MouseEventKind::Down(MouseButton::Left),
                0,
                0,
                KeyModifiers::NONE,
            ),
            (80, 25),
        );
        assert_eq!(
            e,
            Some(Ereignis::Maus(MausEreignis {
                art: MausArt::Druck,
                taste: 1,
                shift: 0,
                zeile: 1,
                spalte: 1,
            }))
        );
    }

    #[test]
    fn letzte_zelle_liegt_noch_drin() {
        let e = maus_zu_ereignis(
            maus(
                MouseEventKind::Up(MouseButton::Right),
                79,
                24,
                KeyModifiers::NONE,
            ),
            (80, 25),
        );
        let Some(Ereignis::Maus(m)) = e else {
            panic!("kein Mausereignis")
        };
        assert_eq!((m.zeile, m.spalte), (25, 80));
        assert_eq!((m.art, m.taste), (MausArt::Loslassen, 2));
    }

    #[test]
    fn ausserhalb_der_flaeche_wird_verworfen() {
        let e = maus_zu_ereignis(
            maus(MouseEventKind::Moved, 80, 0, KeyModifiers::NONE),
            (80, 25),
        );
        assert_eq!(e, None);
        let e = maus_zu_ereignis(
            maus(MouseEventKind::Moved, 0, 25, KeyModifiers::NONE),
            (80, 25),
        );
        assert_eq!(e, None);
    }

    #[test]
    fn nach_groessenaenderung_gilt_die_neue_flaeche() {
        // Dieselbe Position: bei 80x25 draussen, bei 120x40 drin.
        let p = maus(MouseEventKind::Moved, 100, 30, KeyModifiers::NONE);
        assert_eq!(maus_zu_ereignis(p, (80, 25)), None);
        let Some(Ereignis::Maus(m)) = maus_zu_ereignis(p, (120, 40)) else {
            panic!("kein Mausereignis")
        };
        assert_eq!((m.zeile, m.spalte), (31, 101));
    }

    #[test]
    fn umschaltzustand_als_bitfeld() {
        let e = maus_zu_ereignis(
            maus(
                MouseEventKind::Down(MouseButton::Middle),
                5,
                5,
                KeyModifiers::SHIFT | KeyModifiers::ALT,
            ),
            (80, 25),
        );
        let Some(Ereignis::Maus(m)) = e else {
            panic!("kein Mausereignis")
        };
        assert_eq!(m.shift, 1 | 4);
        assert_eq!(m.taste, 4);
    }

    #[test]
    fn rollrad_hat_keine_entsprechung() {
        let e = maus_zu_ereignis(
            maus(MouseEventKind::ScrollUp, 1, 1, KeyModifiers::NONE),
            (80, 25),
        );
        assert_eq!(e, None);
    }

    #[test]
    fn umschaltzustand_der_taste_wird_mitgefuehrt() {
        // Strg+A: Strg allein ist kein Abbruch, nur Strg+C ist es.
        let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
        assert_eq!(
            taste_zu_ereignis(k),
            Some(Ereignis::Taste("a".into(), umschalt::CTRL))
        );
        let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::SHIFT);
        let Some(Ereignis::Taste(_, shift)) = taste_zu_ereignis(k) else {
            panic!("keine Taste")
        };
        assert_eq!(shift, umschalt::ALT | umschalt::SHIFT);
    }

    #[test]
    fn strg_c_ist_abbruch() {
        let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Abbruch));
    }
}
