//! IDE debugger state and commands on the session's existing VM.
use crate::{
    app::{App, Dialog, DialogKind, Execution, Field, Hit, WindowKind},
    commands::Command,
    documents::{BreakpointMark, Document, DocumentId},
    editor::line_start,
};
use anyhow::{anyhow, ensure, Result};
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
use tb_frontend::{Diagnostic, SourcePos};
use tb_vm::interp::{DebugLocation, DebugWatch, Vm};

#[derive(Default)]
pub struct Debugger {
    pub source_modules: std::collections::BTreeMap<DocumentId, DocumentId>,
    pub watches: Vec<DebugWatch>,
    pub frame: Option<u64>,
    pub trace: bool,
    pub history: bool,
    pub break_errors: bool,
    pub historical: Option<usize>,
    pub immediate: String,
    pub motion: Option<Command>,
    pub instant: Option<DebugWatch>,
    pub last_location: Option<DebugLocation>,
}
fn module_name(doc: &Document) -> String {
    match doc.content() {
        tb_vm::project_io::Content::Form(form) => form.root.name.clone(),
        _ => doc
            .source_path()
            .file_stem()
            .unwrap_or_default()
            .to_string_lossy()
            .into_owned(),
    }
}
pub fn physical_line(doc: &Document, at: usize) -> u32 {
    let first = if let tb_vm::project_io::Content::Form(form) = doc.content() {
        tb_ui::frm::read_text(
            &doc.source_path().display().to_string(),
            &tb_ui::frm::write_text(form),
        )
        .map_or(1, |f| f.code_line())
    } else {
        1
    };
    first
        + doc.code()[..at.min(doc.code().len())]
            .bytes()
            .filter(|c| *c == b'\n')
            .count() as u32
}
impl App {
    pub(crate) fn debug_setup(&mut self, vm: &mut Vm) {
        vm.debug.enabled = true;
        vm.debug.compiler = Some(self.editor.compiler.debug_compiler());
        vm.debug.trace = self.debugger.trace;
        vm.debug.history_on = self.debugger.history;
        vm.debug.break_errors = self.debugger.break_errors;
        for w in &mut self.debugger.watches {
            w.value = Err("Noch kein aktueller Halt".into());
            w.frame = None;
        }
        vm.debug.watches = self.debugger.watches.clone();
        self.debugger.frame = None;
        self.debugger.historical = None;
        self.debugger.last_location = None;
        self.debugger.instant = None;
        self.bind_debug_breakpoints(vm);
    }
    fn bind_debug_breakpoints(&mut self, vm: &mut Vm) {
        vm.clear_source_breakpoints();
        let ids = self
            .project
            .documents()
            .map(|(id, _)| id)
            .collect::<Vec<_>>();
        for id in ids {
            let doc = self.project.document(id).unwrap();
            let marks = doc
                .breakpoints
                .iter()
                .map(|m| {
                    (
                        m.valid,
                        self.project
                            .document(m.module)
                            .map(module_name)
                            .unwrap_or_default(),
                        physical_line(doc, m.at),
                    )
                })
                .collect::<Vec<_>>();
            let path = doc.source_path().display().to_string();
            let bound = marks
                .into_iter()
                .map(|(valid, module, line)| {
                    let source = vm
                        .source_files()
                        .iter()
                        .enumerate()
                        .find(|(_, s)| {
                            s.path == path && vm.module_name(s.module).eq_ignore_ascii_case(&module)
                        })
                        .map(|(id, s)| (s.module, id as u32));
                    valid
                        && source.is_some_and(|(module, source)| {
                            vm.add_source_breakpoint(module, source, line)
                        })
                })
                .collect::<Vec<_>>();
            for (mark, bound) in self
                .project
                .breakpoint_marks_mut(id)
                .unwrap()
                .iter_mut()
                .zip(bound)
            {
                mark.bound = bound;
            }
        }
    }
    fn debug_cursor(&self) -> Result<(String, u32)> {
        let view = self.project.view(self.editor_view()?)?;
        ensure!(
            !self.editor.expansions.contains_key(&self.editor_view()?),
            "Included File für einen physischen Quellort öffnen"
        );
        let doc = self.project.document(view.document())?;
        Ok((
            doc.source_path().display().to_string(),
            physical_line(doc, view.cursor),
        ))
    }
    fn debug_target(&self) -> Result<(u16, u32, u32)> {
        ensure!(self.current_compilation().is_some() && self.editor.revision == self.session.source_revision, "Cursorziele benötigen den unveränderten Quellstand der pausierten Sitzung; zuerst neu starten");
        let (path, line) = self.debug_cursor()?;
        let vm = self
            .session
            .vm
            .as_ref()
            .ok_or_else(|| anyhow!("Keine pausierte Sitzung"))?;
        let module = vm.current_module();
        let candidates = vm
            .source_files()
            .iter()
            .enumerate()
            .filter(|(_, s)| s.path == path)
            .collect::<Vec<_>>();
        let (source, file) = candidates
            .iter()
            .find(|(_, s)| s.module == module)
            .copied()
            .or_else(|| (candidates.len() == 1).then(|| candidates[0]))
            .ok_or_else(|| {
                anyhow!("Quelldatei gehört nicht eindeutig zur pausierten Kompilatrevision")
            })?;
        Ok((file.module, source as u32, line))
    }
    pub fn toggle_debug_breakpoint(&mut self) -> Result<()> {
        let view = self.project.view(self.editor_view()?)?;
        let id = view.document();
        let at = line_start(self.project.document(id)?.code(), view.cursor);
        let doc = self.project.document(id)?;
        let line = physical_line(doc, at);
        let module = if self.project.members().contains(&id) {
            Some(id)
        } else {
            self.debugger
                .source_modules
                .get(&id)
                .copied()
                .or_else(|| self.project.members().get(self.selected_member).copied())
        }
        .ok_or_else(|| anyhow!("Kein Ursprungsmodul ausgewählt"))?;
        let existing = doc
            .breakpoints
            .iter()
            .position(|m| physical_line(doc, m.at) == line && m.module == module);
        if let Some(index) = existing {
            self.project.breakpoint_marks_mut(id)?.remove(index);
        } else {
            self.project.breakpoint_marks_mut(id)?.push(BreakpointMark {
                at,
                module,
                valid: true,
                bound: false,
            });
        }
        if self.current_compilation().is_some()
            && self.editor.revision == self.session.source_revision
        {
            if let Some(mut vm) = self.session.vm.take() {
                self.bind_debug_breakpoints(&mut vm);
                self.session.vm = Some(vm);
            }
        }
        self.message =
            "Breakpoint aktualisiert · ● gebunden, ○ inaktiv bis gültiger Übersetzung".into();
        Ok(())
    }
    pub(crate) fn debug_command(&mut self, c: Command) -> Result<bool> {
        use Command::*;
        match c {
            Breakpoint => self.toggle_debug_breakpoint()?,
            ClearBreakpoints => {
                let ids = self
                    .project
                    .documents()
                    .map(|(id, _)| id)
                    .collect::<Vec<_>>();
                for id in ids {
                    self.project.breakpoint_marks_mut(id)?.clear();
                }
                if let Some(vm) = &mut self.session.vm {
                    vm.clear_source_breakpoints();
                }
            }
            Step | ProcedureStep | RunToCursor => {
                ensure!(
                    self.execution != Execution::Paused || self.session.vm.is_some(),
                    "Keine pausierte VM"
                );
                if !matches!(
                    self.execution,
                    Execution::Paused | Execution::Running | Execution::Waiting
                ) {
                    self.start_execution()?;
                }
                self.debugger.motion = Some(c);
                if self.execution == Execution::Paused {
                    self.continue_execution()?;
                } else {
                    self.debug_resume_motion()?;
                }
            }
            NextStatement => {
                self.debugger.historical = None;
                self.debug_navigate(None)?;
            }
            SetStatement => {
                ensure!(
                    self.execution == Execution::Paused,
                    "Set Next Statement benötigt Break-Modus"
                );
                let (_, source, line) = self.debug_target()?;
                let vm = self.session.vm.as_mut().unwrap();
                let frame = vm
                    .debug_location()
                    .ok_or_else(|| anyhow!("Kein Frame"))?
                    .frame;
                ensure!(
                    self.debugger.frame.is_none_or(|id| id == frame),
                    "Inspektionsrahmen ist nicht der aktive Frame"
                );
                vm.set_next_statement(frame, source, line)
                    .map_err(|e| anyhow!(e))?;
                self.debug_navigate(None)?;
            }
            AddWatch | InstantWatch | Watchpoint => self.open_dialog(
                if c == Watchpoint {
                    "Watchpoint"
                } else {
                    "Watch"
                },
                DialogKind::Watch(c),
                vec![Field::text("BASIC-Ausdruck", "")],
            ),
            DeleteWatch => self.open_dialog(
                "Delete Watch",
                DialogKind::DeleteWatch,
                vec![Field::choice(
                    "Watch",
                    self.debugger
                        .watches
                        .iter()
                        .map(|w| w.expression.clone())
                        .collect(),
                    0,
                )],
            ),
            DeleteWatches => {
                self.debugger.watches.clear();
                self.sync_watchpoints();
            }
            Trace | History | BreakErrors => {
                match c {
                    Trace => self.debugger.trace = !self.debugger.trace,
                    History => self.debugger.history = !self.debugger.history,
                    _ => self.debugger.break_errors = !self.debugger.break_errors,
                }
                if let Some(vm) = &mut self.session.vm {
                    vm.debug.trace = self.debugger.trace;
                    vm.debug.history_on = self.debugger.history;
                    vm.debug.break_errors = self.debugger.break_errors;
                }
                self.message = format!(
                    "Trace {} · History {} · Break on Errors {}",
                    self.debugger.trace, self.debugger.history, self.debugger.break_errors
                );
            }
            HistoryBack | HistoryForward => {
                let vm = self
                    .session
                    .vm
                    .as_ref()
                    .ok_or_else(|| anyhow!("Keine History"))?;
                ensure!(!vm.debug.history.is_empty(), "History ist leer");
                let last = vm.debug.history.len();
                let at = self.debugger.historical.unwrap_or(last);
                let at = if c == HistoryBack {
                    at.saturating_sub(1)
                } else {
                    (at + 1).min(last)
                };
                let location = vm.debug.history.get(at).cloned();
                self.debugger.historical = (at < last).then_some(at);
                self.debug_navigate(location)?;
            }
            Calls | Debug => {
                self.refresh_debug_watches();
                self.show_tool(if c == Calls {
                    WindowKind::Calls
                } else {
                    WindowKind::Debug
                });
            }
            Immediate => self.show_tool(WindowKind::Immediate),
            _ => return Ok(false),
        }
        Ok(true)
    }
    pub(crate) fn debug_resume_motion(&mut self) -> Result<()> {
        let motion = self.debugger.motion.take();
        let target = if motion == Some(Command::RunToCursor) {
            Some(self.debug_target()?)
        } else {
            None
        };
        if let Some(vm) = &mut self.session.vm {
            vm.set_step(false);
            vm.debug.cancel_motion();
            vm.debug.error = None;
            match motion {
                Some(Command::Step) => vm.set_step(true),
                Some(Command::ProcedureStep) => vm.step_over(),
                Some(Command::RunToCursor) => {
                    let (m, s, l) = target.unwrap();
                    vm.run_to_cursor(m, s, l).map_err(|e| anyhow!(e))?;
                }
                _ => {}
            }
        }
        self.debugger.historical = None;
        Ok(())
    }
    fn sync_watchpoints(&mut self) {
        if let Some(vm) = &mut self.session.vm {
            vm.debug.watches = self.debugger.watches.clone();
        }
    }
    pub fn refresh_debug_watches(&mut self) {
        let Some(vm) = &mut self.session.vm else {
            return;
        };
        let Some(active) = vm.debug_location() else {
            return;
        };
        let frame = self
            .debugger
            .frame
            .filter(|id| vm.debug_frames().iter().any(|f| f.id == *id))
            .unwrap_or(active.frame);
        self.debugger.frame = Some(frame);
        for watch in &mut self.debugger.watches {
            watch.frame = Some(frame);
            watch.value = if watch.condition {
                vm.evaluate_condition(frame, &watch.expression)
            } else {
                vm.evaluate_watch(frame, &watch.expression)
            };
        }
        self.sync_watchpoints();
    }
    pub fn select_debug_frame(&mut self, index: usize) -> Result<()> {
        ensure!(
            self.execution == Execution::Paused,
            "Calls-Inspektion benötigt Break-Modus"
        );
        let frame = self
            .session
            .vm
            .as_ref()
            .and_then(|vm| vm.debug_frames().get(index).cloned())
            .ok_or_else(|| anyhow!("Frame nicht erreichbar"))?;
        self.debugger.frame = Some(frame.id);
        self.refresh_debug_watches();
        self.debug_navigate(Some(frame.location))
    }
    pub(crate) fn debug_navigate(&mut self, location: Option<DebugLocation>) -> Result<()> {
        let vm = self
            .session
            .vm
            .as_ref()
            .ok_or_else(|| anyhow!("Keine laufende Sitzung"))?;
        let location = location
            .or_else(|| vm.execution_location())
            .ok_or_else(|| anyhow!("Kein Quellort"))?;
        let file = vm.source_files()[location.source as usize].path.clone();
        if file == "<Immediate>" {
            self.show_tool(WindowKind::Immediate);
            self.message = format!("Direktkommando pausiert · Zeile {}", location.line);
            return Ok(());
        }
        self.debugger.last_location = Some(location.clone());
        let owner_name = vm.module_name(location.module).to_string();
        let owner = self.project.members().iter().copied().find(|id| {
            self.project
                .document(*id)
                .is_ok_and(|doc| module_name(doc).eq_ignore_ascii_case(&owner_name))
        });
        self.session.fullscreen = false;
        if self
            .project
            .find_document(std::path::Path::new(&file))
            .is_none()
            && !std::path::Path::new(&file).is_file()
        {
            self.message = format!("Bibliotheksquelle nicht verfügbar: {file}:{} · Fortsetzen oder Prozedurschritt verwenden", location.line);
            return Ok(());
        }
        self.goto_diagnostic(&Diagnostic {
            file: Some(file.clone()),
            pos: SourcePos {
                source: location.source,
                line: location.line,
                column: location.column,
            },
            message: String::new(),
        })?;
        if let (Some(id), Some(owner)) = (self.active_document(), owner) {
            self.debugger.source_modules.insert(id, owner);
        }
        self.message = format!(
            "{} · {}:{} · Werte vom aktuellen Halt{}",
            if self.debugger.historical.is_some() {
                "HISTORIE"
            } else if self
                .session
                .vm
                .as_ref()
                .and_then(|vm| vm.debug_location())
                .is_some_and(|at| at.frame != location.frame)
            {
                "Inspektionsrahmen"
            } else {
                "Ausführungsstelle"
            },
            file,
            location.line,
            if self.session.old_revision {
                " · ALTES KOMPILAT"
            } else {
                ""
            }
        );
        if let Some(error) = self
            .session
            .vm
            .as_ref()
            .and_then(|vm| vm.debug.error.as_ref())
        {
            self.message.push_str(&format!(
                " · Behandelter Fehler {} · Handler {:?}",
                error.code, error.handler
            ));
        }
        if let Some(error) = self
            .session
            .vm
            .as_ref()
            .and_then(|vm| vm.debug.immediate_error.as_ref())
        {
            self.message.push_str(&format!(" · Direktfehler: {error}"));
        }

        Ok(())
    }
    pub fn submit_immediate(&mut self, text: &str) -> Result<()> {
        ensure!(
            self.execution == Execution::Paused,
            "Direktkommandos benötigen Break-Modus"
        );
        let vm = self
            .session
            .vm
            .as_mut()
            .ok_or_else(|| anyhow!("Keine Sitzung"))?;
        let frame = self
            .debugger
            .frame
            .or_else(|| vm.debug_location().map(|l| l.frame))
            .ok_or_else(|| anyhow!("Kein Frame"))?;
        vm.start_immediate(frame, text).map_err(|e| anyhow!(e))?;
        self.execution = Execution::Running;
        self.message = "Direktkommando läuft · Unterbrechen über Run/Break".into();
        Ok(())
    }
    pub(crate) fn debug_submit(&mut self, d: &Dialog) -> Result<()> {
        match d.kind {
            DialogKind::Watch(command) => {
                let text = d.fields[0].string();
                ensure!(!text.trim().is_empty(), "Ausdruck fehlt");
                let mut watch = DebugWatch {
                    expression: text,
                    condition: command == Command::Watchpoint,
                    frame: None,
                    value: Err("Noch kein Halt".into()),
                };
                if self.execution == Execution::Paused {
                    if let Some(vm) = &mut self.session.vm {
                        if let Some(frame) = self
                            .debugger
                            .frame
                            .or_else(|| vm.debug_location().map(|p| p.frame))
                        {
                            watch.frame = Some(frame);
                            watch.value = if watch.condition {
                                vm.evaluate_condition(frame, &watch.expression)
                            } else {
                                vm.evaluate_watch(frame, &watch.expression)
                            };
                        }
                    }
                }
                if command == Command::InstantWatch {
                    self.debugger.instant = Some(watch);
                } else {
                    self.debugger.watches.push(watch);
                    self.sync_watchpoints();
                }
                self.show_tool(WindowKind::Debug);
            }
            DialogKind::DeleteWatch => {
                let i = d.fields[0].index();
                if i < self.debugger.watches.len() {
                    self.debugger.watches.remove(i);
                    self.sync_watchpoints();
                }
            }
            _ => {}
        }
        Ok(())
    }
    pub(crate) fn debug_key(&mut self, key: KeyEvent) -> Result<bool> {
        match self.active_window().map(|w| w.kind) {
            Some(WindowKind::Immediate) => {
                match key.code {
                    K::Enter => {
                        let text = self.debugger.immediate.clone();
                        self.submit_immediate(&text)?;
                        self.debugger.immediate.clear();
                    }
                    K::Backspace => {
                        self.debugger.immediate.pop();
                    }
                    K::Char(c) if !key.modifiers.intersects(M::CONTROL | M::ALT) => {
                        self.debugger.immediate.push(c)
                    }
                    _ => {}
                }
                Ok(true)
            }
            Some(WindowKind::Calls) => {
                let frames = self
                    .session
                    .vm
                    .as_ref()
                    .map(|vm| vm.debug_frames())
                    .unwrap_or_default();
                let at = frames
                    .iter()
                    .position(|f| Some(f.id) == self.debugger.frame)
                    .unwrap_or(0);
                let next = match key.code {
                    K::Down => (at + 1).min(frames.len().saturating_sub(1)),
                    K::Up => at.saturating_sub(1),
                    K::Enter => at,
                    _ => return Ok(true),
                };
                self.select_debug_frame(next)?;
                self.show_tool(WindowKind::Calls);
                Ok(true)
            }
            Some(WindowKind::Debug) => Ok(true),
            _ => Ok(false),
        }
    }
    pub(crate) fn debug_render(&mut self, f: &mut Frame, kind: WindowKind, r: Rect) {
        let mut lines = Vec::new();
        match kind {
            WindowKind::Immediate => {
                lines.push(format!("> {}", self.debugger.immediate));
                lines.push("Enter: ausführen · Run/Break: unterbrechen · F5: fortsetzen".into());
                lines.push(self.message.clone());
                if let Some(vm) = &self.session.vm {
                    if let Some(error) = &vm.debug.immediate_error {
                        lines.push(error.clone());
                    }
                }
            }
            WindowKind::Calls => {
                if let Some(vm) = &self.session.vm {
                    for (row, frame) in vm.debug_frames().iter().enumerate() {
                        lines.push(format!(
                            "{} {} · {}:{}",
                            if Some(frame.id) == self.debugger.frame {
                                "▶"
                            } else {
                                " "
                            },
                            frame.procedure,
                            frame.file,
                            frame.location.line
                        ));
                        if row < r.height as usize {
                            self.hits.push((
                                Rect::new(r.x, r.y + row as u16, r.width, 1),
                                Hit::DebugFrame(row),
                            ));
                        }
                    }
                }
            }
            _ => {
                lines.push(if self.debugger.historical.is_some() {
                    "HISTORIE · Werte vom aktuellen Halt".into()
                } else {
                    "Watches · Werte vom aktuellen Halt (read-only)".into()
                });
                for watch in self
                    .debugger
                    .watches
                    .iter()
                    .chain(self.debugger.instant.iter())
                {
                    lines.push(format!(
                        "{}{} = {}",
                        if watch.condition { "[Watchpoint] " } else { "" },
                        watch.expression,
                        match &watch.value {
                            Ok(v) => format!("{v:?}"),
                            Err(e) => format!("FEHLER: {e}"),
                        }
                    ));
                }
                if let Some(vm) = &self.session.vm {
                    if let Some(error) = &vm.debug.error {
                        lines.push(format!(
                            "Behandelter Fehler {} · Ursache {:?} · Handler {:?}",
                            error.code, error.origin, error.handler
                        ));
                    }
                }
                for (_, doc) in self.project.documents() {
                    for mark in &doc.breakpoints {
                        lines.push(format!(
                            "{} {}:{}",
                            if mark.bound { "●" } else { "○ inaktiv" },
                            doc.source_path().display(),
                            physical_line(doc, mark.at)
                        ));
                    }
                }
            }
        }
        f.render_widget(
            Paragraph::new(lines.join("\n")).style(crate::render::pair(15, 1)),
            r,
        );
        if kind == WindowKind::Immediate && r.height > 4 {
            f.render_widget(
                tb_ui::screen::ScreenWidget(self.session.screen()),
                Rect::new(r.x, r.y + 4, r.width, r.height - 4),
            );
        }
    }
}
