use std::path::Path;

use hjkl_buffer::Span as BufferSpan;
use hjkl_engine::types::Style as EngineStyle;

/// Skip preview entirely past this byte count — likely a binary or
/// large generated artefact that wouldn't render usefully anyway. Also
/// bounds in-memory cost since the loader keeps every line.
pub const PREVIEW_MAX_BYTES: u64 = 1_000_000;

/// Per-row span table + style table for the preview pane. The
/// `BufferView` consumer takes `Vec<Vec<Span>>` plus a resolver
/// closure mapping `style: u32` → engine `Style`; both live here so
/// the renderer can wire them together cheaply.
#[derive(Default, Clone)]
pub struct PreviewSpans {
    /// One vec per buffer row, each entry covering a half-open byte
    /// range with an opaque style id.
    pub by_row: Vec<Vec<BufferSpan>>,
    /// Style id → engine style. Index with the `style` field of each
    /// `BufferSpan`.
    pub styles: Vec<EngineStyle>,
}

impl PreviewSpans {
    /// Build from flat byte-range styled spans (app-side highlighter feeds this).
    ///
    /// `ranges` is a slice of `(byte_range, style)` pairs covering the
    /// raw file bytes in `bytes`. Each range is split across rows at `\n`
    /// boundaries and stored as local-byte-offset `BufferSpan` entries.
    pub fn from_byte_ranges(
        ranges: &[(std::ops::Range<usize>, EngineStyle)],
        bytes: &[u8],
    ) -> Self {
        let mut row_starts: Vec<usize> = vec![0];
        for (i, &b) in bytes.iter().enumerate() {
            if b == b'\n' {
                row_starts.push(i + 1);
            }
        }
        let row_count = row_starts.len();

        let mut styles: Vec<EngineStyle> = Vec::new();
        let mut by_row: Vec<Vec<BufferSpan>> = vec![Vec::new(); row_count];

        for (byte_range, rat) in ranges {
            let style_id = match styles.iter().position(|s| s == rat) {
                Some(i) => i,
                None => {
                    styles.push(*rat);
                    styles.len() - 1
                }
            } as u32;

            let span_start = byte_range.start;
            let span_end = byte_range.end;
            let start_row = row_starts
                .partition_point(|&rs| rs <= span_start)
                .saturating_sub(1);
            let mut row = start_row;
            while row < row_count {
                let row_byte_start = row_starts[row];
                let row_byte_end = row_starts
                    .get(row + 1)
                    .map_or(bytes.len(), |&s| s.saturating_sub(1));
                if row_byte_start >= span_end {
                    break;
                }
                let local_start = span_start.saturating_sub(row_byte_start);
                let local_end = row_local_end(span_start, span_end, row_byte_start, row_byte_end);
                if local_end > local_start {
                    by_row[row].push(BufferSpan::new(local_start, local_end, style_id));
                }
                row += 1;
            }
        }

        Self { by_row, styles }
    }
}

/// Row-local end offset for a span clipped to one row.
///
/// A MULTI-ROW span that covers this row's end — a markdown fenced code
/// block, a multi-line string — records one byte PAST the row's content (the
/// newline slot), which the renderer reads as "paint this bg across the whole
/// row". A span confined to one row keeps its exact end even at end-of-line.
/// Mirrors `hjkl_syntax::row_local_end`; duplicated rather than shared so
/// `hjkl-picker` keeps no syntax dependency.
fn row_local_end(
    span_start: usize,
    span_end: usize,
    row_byte_start: usize,
    row_byte_end: usize,
) -> usize {
    let multi_row = span_start < row_byte_start || span_end > row_byte_end;
    if multi_row && span_end >= row_byte_end {
        row_byte_end.saturating_sub(row_byte_start) + 1
    } else {
        span_end.min(row_byte_end).saturating_sub(row_byte_start)
    }
}

/// Build `PreviewSpans` from a flat list of highlight spans using a
/// `resolve_style` closure instead of a `&dyn Theme` reference.
///
/// `resolve_style` receives the capture name (e.g. `"keyword"`) and
/// returns `Some(Style)` when the theme has a mapping for that name.
/// This keeps `hjkl-picker` free of a `hjkl-tree-sitter` dependency —
/// callers supply the resolver from their own theme binding.
pub fn build_preview_spans<F>(
    flat: &[(std::ops::Range<usize>, &str)],
    bytes: &[u8],
    resolve_style: F,
) -> PreviewSpans
where
    F: Fn(&str) -> Option<EngineStyle>,
{
    let mut row_starts: Vec<usize> = vec![0];
    for (i, &b) in bytes.iter().enumerate() {
        if b == b'\n' {
            row_starts.push(i + 1);
        }
    }
    let row_count = row_starts.len();

    let mut styles: Vec<EngineStyle> = Vec::new();
    let mut by_row: Vec<Vec<BufferSpan>> = vec![Vec::new(); row_count];

    for (byte_range, capture) in flat {
        let Some(rat) = resolve_style(capture) else {
            continue;
        };
        let style_id = match styles.iter().position(|s| *s == rat) {
            Some(i) => i,
            None => {
                styles.push(rat);
                styles.len() - 1
            }
        } as u32;
        let span_start = byte_range.start;
        let span_end = byte_range.end;
        let start_row = row_starts
            .partition_point(|&rs| rs <= span_start)
            .saturating_sub(1);
        let mut row = start_row;
        while row < row_count {
            let row_byte_start = row_starts[row];
            let row_byte_end = row_starts
                .get(row + 1)
                .map_or(bytes.len(), |&s| s.saturating_sub(1));
            if row_byte_start >= span_end {
                break;
            }
            let local_start = span_start.saturating_sub(row_byte_start);
            let local_end = row_local_end(span_start, span_end, row_byte_start, row_byte_end);
            if local_end > local_start {
                by_row[row].push(BufferSpan::new(local_start, local_end, style_id));
            }
            row += 1;
        }
    }
    PreviewSpans { by_row, styles }
}

/// Load a single file for the preview pane. Returns `(content, status)`.
pub fn load_preview(abs: &Path) -> (String, String) {
    let meta = match std::fs::metadata(abs) {
        Ok(m) => m,
        Err(e) => return (String::new(), format!("{e}")),
    };
    if meta.len() > PREVIEW_MAX_BYTES {
        let mb = meta.len() as f64 / 1_048_576.0;
        return (String::new(), format!("{mb:.1}MB — too large"));
    }
    let bytes = match std::fs::read(abs) {
        Ok(b) => b,
        Err(e) => return (String::new(), format!("{e}")),
    };
    let scan_end = bytes.len().min(8192);
    if bytes[..scan_end].contains(&0u8) {
        return (String::new(), "binary".into());
    }
    let Ok(text) = std::str::from_utf8(&bytes) else {
        return (String::new(), "non-utf8".into());
    };
    // No line truncation here — the byte cap above already bounds the load,
    // and the viewport only renders what fits on screen. Truncating by line
    // count would silently drop grep matches that fall past the cap.
    (text.to_owned(), String::new())
}

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

    /// A multi-row span covering a row's end is recorded one byte past that
    /// row's content — the marker `BufferView` reads to paint the span's bg
    /// across the whole row (a markdown code block in the preview pane).
    /// A span confined to one row keeps its exact end.
    #[test]
    fn multi_row_span_marks_the_rows_it_covers_to_eol() {
        let bytes = b"aaa\n\nbb\ncc\n";
        let style = EngineStyle::default();
        let spans = PreviewSpans::from_byte_ranges(&[(0..7, style)], bytes);
        assert_eq!(
            (spans.by_row[0][0].start_byte, spans.by_row[0][0].end_byte),
            (0, 4)
        );
        assert_eq!(
            (spans.by_row[1][0].start_byte, spans.by_row[1][0].end_byte),
            (0, 1),
            "a blank row inside the span still gets one"
        );
        assert_eq!(
            (spans.by_row[2][0].start_byte, spans.by_row[2][0].end_byte),
            (0, 3)
        );
        assert!(spans.by_row[3].is_empty());

        let single = PreviewSpans::from_byte_ranges(&[(0..3, style)], bytes);
        assert_eq!(
            (single.by_row[0][0].start_byte, single.by_row[0][0].end_byte),
            (0, 3),
            "a single-row span keeps its exact end even at end-of-line"
        );
    }

    /// Same rule through the capture-name builder.
    #[test]
    fn build_preview_spans_marks_multi_row_spans_too() {
        let bytes = b"aaa\nbbb\n";
        let spans =
            build_preview_spans(&[(0..7, "string")], bytes, |_| Some(EngineStyle::default()));
        assert_eq!(
            (spans.by_row[0][0].start_byte, spans.by_row[0][0].end_byte),
            (0, 4)
        );
        assert_eq!(
            (spans.by_row[1][0].start_byte, spans.by_row[1][0].end_byte),
            (0, 4)
        );
    }
}
