use hjkl_engine::Host;
use hjkl_engine_tui::EditorRatatuiExt;
use std::path::Path;

use hjkl_bonsai::{CommentMarkerPass, Highlighter, Theme};
use hjkl_engine::types::{Attrs, Color as EngineColor, Style as EngineStyle};
use hjkl_picker::PreviewSpans;

use hjkl_app::git::{GitChange, GitChangeKind};
use hjkl_app::git_worker::GitJob;
use hjkl_buffer_tui::Sign;
use hjkl_lang::GrammarRequest;
use ratatui::style::{Color, Style};

use super::App;

/// Convert a host-agnostic [`GitChange`] into a ratatui-flavored [`Sign`]
/// with the canonical gutter characters and colours.
fn change_to_sign(c: GitChange) -> Sign {
    let (ch, style) = match c.kind {
        GitChangeKind::Add => ('+', Style::default().fg(Color::Green)),
        GitChangeKind::Modify => ('~', Style::default().fg(Color::Yellow)),
        GitChangeKind::Delete => ('_', Style::default().fg(Color::Red)),
    };
    Sign {
        row: c.row,
        ch,
        style,
        priority: 50,
    }
}

impl App {
    /// Queue a git diff-sign refresh for the current buffer (throttled).
    pub(crate) fn refresh_git_signs(&mut self) {
        self.refresh_git_signs_inner(false);
    }

    /// Queue a git diff-sign refresh for the current buffer, bypassing
    /// the 250 ms throttle.
    pub(crate) fn refresh_git_signs_force(&mut self) {
        self.refresh_git_signs_inner(true);
    }

    pub(crate) fn refresh_git_signs_inner(&mut self, force: bool) {
        use std::time::{Duration, Instant};
        const REFRESH_MIN_INTERVAL: Duration = Duration::from_millis(250);

        let path = match self.active().filename.as_deref() {
            Some(p) => p.to_path_buf(),
            None => {
                let slot = self.active_mut();
                slot.git_signs.clear();
                slot.last_git_dirty_gen = None;
                return;
            }
        };
        let dg = self.active().editor.buffer().dirty_gen();
        if !force && self.active().last_git_dirty_gen == Some(dg) {
            return;
        }
        let now = Instant::now();
        if !force && now.duration_since(self.active().last_git_refresh_at) < REFRESH_MIN_INTERVAL {
            return;
        }

        // O(1) rope clone — Arc-clone of the root node. Worker thread
        // materializes the byte buffer; main thread pays nothing here.
        let rope = self.active().editor.buffer().rope();
        let buffer_id = self.active().buffer_id;
        self.active_mut().last_git_refresh_at = now;

        self.git_worker.submit(GitJob {
            buffer_id,
            path,
            rope,
            dirty_gen: dg,
        });
    }

    /// Drain completed git-sign results from the worker and install them.
    pub(crate) fn poll_git_signs(&mut self) -> bool {
        let mut redraw = false;
        while let Some(result) = self.git_worker.try_recv() {
            if let Some(slot) = self
                .slots
                .iter_mut()
                .find(|s| s.buffer_id == result.buffer_id)
                && slot
                    .last_git_dirty_gen
                    .is_none_or(|dg| dg <= result.dirty_gen)
            {
                slot.git_signs = result.changes.into_iter().map(change_to_sign).collect();
                slot.is_untracked = result.is_untracked;
                slot.last_git_dirty_gen = Some(result.dirty_gen);
                redraw = true;
            }
        }
        redraw
    }

    /// Poll in-flight async grammar loads and wire any that completed.
    ///
    /// Returns `true` when at least one load resolved and a redraw is needed.
    pub(crate) fn poll_grammar_loads(&mut self) -> bool {
        let events = self.syntax.poll_pending_loads();
        if events.is_empty() {
            return false;
        }
        for event in &events {
            use crate::syntax::LoadEventKind;
            hjkl_syntax::SyntaxLayer::dispatch_load_event(event, |kind| match kind {
                LoadEventKind::Ready { id, name } => {
                    tracing::debug!("grammar load complete: {name} (buffer {id})");
                    // Re-attach the grammar now that it's ready.
                    if let Some(slot) = self.slots.iter().find(|s| s.buffer_id == id)
                        && let Some(ref p) = slot.filename.clone()
                    {
                        let _ = self.syntax.set_language_for_path(id, p);
                    }
                }
                LoadEventKind::Failed { id, name, error } => {
                    tracing::debug!("grammar load failed: {name} (buffer {id}): {error}");
                    self.bus.error(format!("grammar {name}: {error}"));
                }
            });
        }
        true
    }

    /// Poll in-flight anvil install handles each tick.
    pub(crate) fn poll_anvil_jobs(&mut self) -> bool {
        use hjkl_anvil::InstallStatus;

        let mut redraw = false;
        let mut to_remove: Vec<String> = Vec::new();

        for (name, handle) in self.anvil_handles.iter() {
            while let Some(status) = handle.try_recv() {
                redraw = true;
                let log_line = format_anvil_status(&status);
                self.anvil_log
                    .entry(name.clone())
                    .or_default()
                    .push(log_line);

                match &status {
                    InstallStatus::Done { .. } => {
                        self.bus.info(format!("anvil: installed {name}"));
                        to_remove.push(name.clone());
                    }
                    InstallStatus::Failed(reason) => {
                        self.bus
                            .error(format!("anvil: {name} failed \u{2014} {reason}"));
                        to_remove.push(name.clone());
                    }
                    InstallStatus::Downloading {
                        bytes_downloaded,
                        total,
                    } => {
                        let pct = match total {
                            Some(t) if *t > 0 => {
                                format!("{}%", (bytes_downloaded * 100) / t)
                            }
                            _ => format!("{bytes_downloaded} bytes"),
                        };
                        self.bus.info(format!("anvil: {name} downloading {pct}"));
                    }
                    InstallStatus::Verifying => {
                        self.bus.info(format!("anvil: {name} verifying"));
                    }
                    InstallStatus::Extracting => {
                        self.bus.info(format!("anvil: {name} extracting"));
                    }
                    InstallStatus::Installing => {
                        self.bus.info(format!("anvil: {name} installing"));
                    }
                    InstallStatus::Queued => {}
                    InstallStatus::TofuRecorded { triple, sha256 } => {
                        self.bus
                            .info(format!("anvil: {name} TOFU hash recorded for {triple}"));
                        let _ = sha256;
                    }
                }
            }
        }

        for name in to_remove {
            self.anvil_handles.remove(&name);
        }

        redraw
    }

    /// Handle a `take_content_reset` event on the active buffer.
    pub(crate) fn handle_active_content_reset(&mut self, buffer_id: crate::syntax::BufferId) {
        self.syntax.reset(buffer_id);
        let active_idx = self.focused_slot_idx();
        self.slots[active_idx]
            .editor
            .install_ratatui_syntax_spans(Vec::new());
    }

    /// Run `render_viewport` for the active buffer and install the result.
    /// Returns empty spans when no grammar is attached or grammar is loading.
    pub(crate) fn recompute_and_install(&mut self) {
        if !self.syntax_enabled {
            return;
        }
        let buffer_id = self.active().buffer_id;
        let (top, height) = {
            // Compute union viewport across all windows showing the same slot.
            let focused_slot = self.focused_slot_idx();
            let (focused_top, focused_height) = {
                let vp = self.active().editor.host().viewport();
                (vp.top_row, vp.height as usize)
            };
            let mut union_top = focused_top;
            let mut union_bot = focused_top + focused_height;
            for w in self.windows.iter().flatten() {
                if w.slot == focused_slot
                    && let Some(rect) = w.last_rect
                {
                    union_top = union_top.min(w.top_row);
                    union_bot = union_bot.max(w.top_row + rect.h as usize);
                }
            }
            (union_top, union_bot - union_top)
        };

        let active_idx = self.focused_slot_idx();
        // Push colorizer option state from the active editor's settings before
        // rendering — Options own the source of truth, SyntaxLayer mirrors so
        // walk_rows can gate without a re-borrow into the editor.
        let (clz, clz_ft, rb) = {
            let s = self.slots[active_idx].editor.settings();
            (
                s.colorizer,
                s.colorizer_filetypes.clone(),
                s.rainbow_brackets,
            )
        };
        self.syntax.set_colorizer(clz, clz_ft);
        self.syntax.set_rainbow_brackets(rb);
        let buf = self.slots[active_idx].editor.buffer();

        let out = self.syntax.render_viewport(buffer_id, buf, top, height);

        if let Some(out) = out {
            let start = out.key.1;
            let end = start + out.spans.len();
            self.slots[active_idx]
                .editor
                .patch_ratatui_syntax_spans_range(start..end, &out.spans);
            self.slots[active_idx].diag_signs = out.signs;
        } else {
            // No spans available (no language or grammar still loading).
            // Clear stale spans and let the renderer draw plain text.
            self.slots[active_idx]
                .editor
                .install_ratatui_syntax_spans(Vec::new());
        }

        self.refresh_git_signs();
    }

    /// Compute syntax highlight spans for a one-off preview snippet.
    pub fn preview_spans_for(&self, path: &Path, bytes: &[u8]) -> PreviewSpans {
        self.preview_spans_for_range(path, bytes, 0..bytes.len())
    }

    /// Viewport-clipped variant of [`Self::preview_spans_for`].
    pub fn preview_spans_for_range(
        &self,
        path: &Path,
        bytes: &[u8],
        byte_range: std::ops::Range<usize>,
    ) -> PreviewSpans {
        let grammar = match self.directory.request_for_path(path) {
            GrammarRequest::Cached(g) => g,
            GrammarRequest::Loading { .. } | GrammarRequest::Unknown | _ => {
                return PreviewSpans::default();
            }
        };
        let name = grammar.name().to_string();
        let mut cache = match self.preview_highlighters.lock() {
            Ok(c) => c,
            Err(_) => return PreviewSpans::default(),
        };
        let h = match cache.entry(name) {
            std::collections::hash_map::Entry::Occupied(o) => o.into_mut(),
            std::collections::hash_map::Entry::Vacant(v) => match Highlighter::new(grammar) {
                Ok(h) => v.insert(h),
                Err(_) => return PreviewSpans::default(),
            },
        };
        h.reset();
        h.parse_initial(bytes);
        let directory = std::sync::Arc::clone(&self.directory);
        let resolve = move |name: &str| match directory.request_by_name(name) {
            GrammarRequest::Cached(g) => Some(g),
            GrammarRequest::Loading { .. } | GrammarRequest::Unknown | _ => None,
        };
        let mut flat = h.highlight_range_with_injections(bytes, byte_range, resolve);
        drop(cache);
        CommentMarkerPass::new().apply(&mut flat, bytes);
        let theme = self.theme.syntax.clone();
        let ranges: Vec<(std::ops::Range<usize>, EngineStyle)> = flat
            .into_iter()
            .filter_map(|span| {
                theme.style(span.capture()).map(|s| {
                    let fg = s.fg.map(|c| EngineColor(c.r, c.g, c.b));
                    let bg = s.bg.map(|c| EngineColor(c.r, c.g, c.b));
                    let mut attrs = Attrs::empty();
                    if s.modifiers.bold {
                        attrs |= Attrs::BOLD;
                    }
                    if s.modifiers.italic {
                        attrs |= Attrs::ITALIC;
                    }
                    if s.modifiers.underline {
                        attrs |= Attrs::UNDERLINE;
                    }
                    if s.modifiers.reverse {
                        attrs |= Attrs::REVERSE;
                    }
                    if s.modifiers.strikethrough {
                        attrs |= Attrs::STRIKE;
                    }
                    (span.byte_range.clone(), EngineStyle { fg, bg, attrs })
                })
            })
            .collect();
        PreviewSpans::from_byte_ranges(&ranges, bytes)
    }

    /// `:syntax on` / `:syntax off` — toggle bonsai highlighting app-wide.
    pub(crate) fn set_syntax_enabled(&mut self, enabled: bool) {
        if self.syntax_enabled == enabled {
            return;
        }
        self.syntax_enabled = enabled;
        if !enabled {
            for slot in &mut self.slots {
                slot.editor.install_ratatui_syntax_spans(Vec::new());
                slot.diag_signs.clear();
            }
        } else {
            for i in 0..self.slots.len() {
                let buffer_id = self.slots[i].buffer_id;
                if let Some(p) = self.slots[i].filename.clone() {
                    let _ = self.syntax.set_language_for_path(buffer_id, &p);
                }
            }
            self.recompute_and_install();
        }
    }
}

/// Number of off-screen rows above/below the visible window to include in the
/// highlighter's byte range for picker preview injection resolution.
const VIEWPORT_SLACK_ROWS: usize = 50;

/// Find the byte offset where row `target_row` begins (row 0 = byte 0). For
/// `target_row` past the end, returns `bytes.len()`.
fn byte_offset_of_row(bytes: &[u8], target_row: usize) -> usize {
    if target_row == 0 {
        return 0;
    }
    let mut row = 0usize;
    for (i, b) in bytes.iter().enumerate() {
        if *b == b'\n' {
            row += 1;
            if row == target_row {
                return i + 1;
            }
        }
    }
    bytes.len()
}

/// Bridge: route `hjkl-picker`'s preview-pane highlighter through the
/// editor's bonsai pipeline.
impl hjkl_picker::PreviewHighlighter for App {
    fn spans_for(&self, path: &Path, bytes: &[u8]) -> PreviewSpans {
        self.preview_spans_for(path, bytes)
    }

    fn spans_for_viewport(
        &self,
        path: &Path,
        bytes: &[u8],
        top_row: usize,
        height: usize,
    ) -> PreviewSpans {
        let start_row = top_row.saturating_sub(VIEWPORT_SLACK_ROWS);
        let end_row = top_row
            .saturating_add(height)
            .saturating_add(VIEWPORT_SLACK_ROWS);
        let start = byte_offset_of_row(bytes, start_row);
        let end = byte_offset_of_row(bytes, end_row);
        self.preview_spans_for_range(path, bytes, start..end)
    }
}

/// Format an [`hjkl_anvil::InstallStatus`] as a human-readable log line.
fn format_anvil_status(status: &hjkl_anvil::InstallStatus) -> String {
    use hjkl_anvil::InstallStatus;
    match status {
        InstallStatus::Queued => "queued".into(),
        InstallStatus::Downloading {
            bytes_downloaded,
            total,
        } => match total {
            Some(t) if *t > 0 => format!(
                "downloading {}% ({bytes_downloaded}/{t} bytes)",
                (bytes_downloaded * 100) / t
            ),
            _ => format!("downloading {bytes_downloaded} bytes"),
        },
        InstallStatus::Verifying => "verifying checksum".into(),
        InstallStatus::Extracting => "extracting archive".into(),
        InstallStatus::Installing => "installing binary".into(),
        InstallStatus::Done { bin_path } => format!("done → {}", bin_path.display()),
        InstallStatus::Failed(reason) => format!("failed: {reason}"),
        InstallStatus::TofuRecorded { triple, sha256 } => {
            format!("tofu recorded for {triple}: {}", &sha256[..8])
        }
    }
}

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

    /// Regression: install_render_result no longer exists; the equivalent is
    /// recompute_and_install. This test verifies the picker preview injection
    /// wiring still works.
    #[test]
    #[ignore = "network + compiler: fetches markdown + rust grammars"]
    fn preview_spans_for_markdown_includes_rust_injection() {
        let app = App::new(None, false, None, None).unwrap();

        assert!(
            app.directory.by_name("markdown").is_some(),
            "markdown grammar should resolve"
        );
        assert!(
            app.directory.by_name("rust").is_some(),
            "rust grammar should resolve"
        );

        let source = b"# Title\n\n```rust\nfn main() {}\n```\n";
        let path = PathBuf::from("test.md");
        let spans = app.preview_spans_for(&path, source);

        const RUST_ROW: usize = 3;
        assert!(
            spans.by_row.len() > RUST_ROW,
            "expected at least {} rows, got {}",
            RUST_ROW + 1,
            spans.by_row.len()
        );
        let rust_row = &spans.by_row[RUST_ROW];
        assert!(
            rust_row.len() >= 3,
            "expected ≥3 styled spans on the rust row (keyword/function/punct from injection); \
             got {} spans: {:?}",
            rust_row.len(),
            rust_row
        );
    }
}
