use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::{Component, Path};

use ratatui::style::Style;
use ratatui::text::{Line, Span};
use unicode_width::UnicodeWidthStr;

#[derive(Clone, Copy)]
pub struct StyleConfig {
    pub filename: Style,
    pub path: Style,
    pub dim: Style,
    pub separator: Style,
}

impl Default for StyleConfig {
    fn default() -> Self {
        Self {
            filename: Style::default().add_modifier(ratatui::style::Modifier::BOLD),
            path: Style::default(),
            dim: Style::default().add_modifier(ratatui::style::Modifier::DIM),
            separator: Style::default().add_modifier(ratatui::style::Modifier::DIM),
        }
    }
}

pub fn format_path_line<'a>(path: &'a Path, max_width: usize) -> Line<'a> {
    let spans = format_path_spans(path, max_width, StyleConfig::default());
    Line::from(spans)
}

pub fn format_path_line_with_style<'a>(
    path: &'a Path,
    max_width: usize,
    style: StyleConfig,
) -> Line<'a> {
    let spans = format_path_spans(path, max_width, style);
    Line::from(spans)
}

/// Backwards-compatible API: default min_tail = 1
pub fn format_path_spans(path: &Path, max_width: usize, style: StyleConfig) -> Vec<Span<'static>> {
    format_path_spans_with_min_tail(path, max_width, style, 1)
}

/// Main formatting entry. `min_tail` is the minimum number of trailing
/// directory segments to prefer preserving when eliding. Callers that show
/// flat lists (e.g., recent files) may set `min_tail = 2` to improve context.
pub fn format_path_spans_with_min_tail(
    path: &Path,
    max_width: usize,
    style: StyleConfig,
    min_tail: usize,
) -> Vec<Span<'static>> {
    let (filename, dirs) = split_path(path);

    let filename_str = os_to_str(filename).into_owned();
    let file_w = width(&filename_str);

    // If there are no directory components, show filename only (no separator).
    if dirs.is_empty() {
        return vec![Span::styled(filename_str, style.filename)];
    }

    let sep = " — ";
    let sep_w = width(sep);

    if file_w + sep_w >= max_width {
        return vec![Span::styled(filename_str, style.filename)];
    }

    let mut remaining = max_width - file_w - sep_w;

    let mut spans: Vec<Span<'static>> = Vec::with_capacity(8);
    spans.push(Span::styled(filename_str.clone(), style.filename));
    spans.push(Span::styled(sep.to_string(), style.separator));

    // 1) Try full path
    if write_full_owned(&mut spans, &dirs, &mut remaining, &style) {
        return spans;
    }

    // 2) If single directory that is too wide, try compressing it directly
    if dirs.len() == 1 {
        let s = os_to_str(dirs[0]).into_owned();
        for level in 1..=3 {
            let comp = compress_segment(&s, level);
            if width(&comp) <= remaining {
                spans.push(Span::styled(comp, style.path));
                return spans;
            }
        }
    }

    // 3) Compression passes (try shrinking long directory names before eliding)
    for level in 1..=3 {
        if write_compressed_elided_owned(&mut spans, &dirs, &mut remaining, level, &style, min_tail) {
            return spans;
        }
    }

    // 4) Middle elision for multi-segment paths
    if write_middle_elided_owned(&mut spans, &dirs, &mut remaining, &style, min_tail) {
        return spans;
    }

    // 5) Minimal fallback: try last segment compressed
    if let Some(last) = dirs.last()
        && write_minimal_owned(&mut spans, last, &mut remaining, &style) {
            return spans;
        }

    vec![Span::styled(filename_str, style.filename)]
}

// Owned-span variants of earlier write functions
fn write_full_owned(
    spans: &mut Vec<Span<'static>>,
    dirs: &[&OsStr],
    remaining: &mut usize,
    style: &StyleConfig,
) -> bool {
    // Pre-check total width required for the full (un-elided) directory list.
    // This avoids partially writing into `spans` and mutating `remaining` on
    // failure which would make later fallbacks less likely to succeed.
    let mut total_needed: usize = 0;
    for (i, d) in dirs.iter().enumerate() {
        if i > 0 {
            total_needed += 1; // '/'
        }
        let s = os_to_str(d);
        total_needed += width(&s);
    }

    if total_needed > *remaining {
        return false;
    }

    // Enough room — delegate to the writer which will succeed.
    write_dirs_owned(spans, dirs, remaining, style)
}

fn write_dirs_owned(
    spans: &mut Vec<Span<'static>>,
    dirs: &[&OsStr],
    remaining: &mut usize,
    style: &StyleConfig,
) -> bool {
    let start = spans.len();

    for (i, d) in dirs.iter().enumerate() {
        if i > 0 {
            if *remaining < 1 {
                spans.truncate(start);
                return false;
            }
            spans.push(Span::styled("/".to_string(), style.dim));
            *remaining -= 1;
        }

        let s = os_to_str(d);
        let w = width(&s);

        if w > *remaining {
            spans.truncate(start);
            return false;
        }

        spans.push(Span::styled(s.into_owned(), style.path));
        *remaining -= w;
    }

    true
}

fn write_middle_elided_owned(
    spans: &mut Vec<Span<'static>>,
    dirs: &[&OsStr],
    remaining: &mut usize,
    style: &StyleConfig,
    min_tail: usize,
) -> bool {
    if dirs.len() < 2 {
        return write_full_owned(spans, dirs, remaining, style);
    }

    let start = spans.len();
    let initial_rem = *remaining;

    // First attempt: preserve first directory if it fits
    let first = os_to_str(dirs[0]).into_owned();
    let fw = width(&first);

    if fw <= *remaining {
        spans.push(Span::styled(first.clone(), style.path));
        *remaining -= fw;

        let elide_w = width("…/");
        for tail_len in (1..dirs.len()).rev() {
            if tail_len < min_tail {
                continue;
            }

            let checkpoint_len = spans.len();
            let checkpoint_rem = *remaining;

            if *remaining < elide_w {
                spans.truncate(start);
                *remaining = initial_rem;
                break;
            }

            spans.push(Span::styled("…/".to_string(), style.dim));
            *remaining -= elide_w;

            let tail = &dirs[dirs.len() - tail_len..];

            let mut ok = true;
            for (i, d) in tail.iter().enumerate() {
                if i > 0 {
                    if *remaining < 1 {
                        ok = false;
                        break;
                    }
                    spans.push(Span::styled("/".to_string(), style.dim));
                    *remaining -= 1;
                }

                let s = os_to_str(d).into_owned();
                let w = width(&s);

                if w > *remaining {
                    ok = false;
                    break;
                }

                spans.push(Span::styled(s, style.path));
                *remaining -= w;
            }

            if ok {
                return true;
            }

            spans.truncate(checkpoint_len);
            *remaining = checkpoint_rem;
        }

        // restore and try without preserving first
        spans.truncate(start);
        *remaining = initial_rem;
    }

    // Second attempt: elide without first segment
    let elide_w = width("…/");
    for tail_len in (1..dirs.len()).rev() {
        if tail_len < min_tail {
            continue;
        }

        let checkpoint_len = spans.len();
        let checkpoint_rem = *remaining;

        if *remaining < elide_w {
            spans.truncate(start);
            *remaining = initial_rem;
            return false;
        }

        spans.push(Span::styled("…/".to_string(), style.dim));
        *remaining -= elide_w;

        let tail = &dirs[dirs.len() - tail_len..];

        let mut ok = true;
        for (i, d) in tail.iter().enumerate() {
            if i > 0 {
                if *remaining < 1 {
                    ok = false;
                    break;
                }
                spans.push(Span::styled("/".to_string(), style.dim));
                *remaining -= 1;
            }

            let s = os_to_str(d).into_owned();
            let w = width(&s);

            if w > *remaining {
                ok = false;
                break;
            }

            spans.push(Span::styled(s, style.path));
            *remaining -= w;
        }

        if ok {
            return true;
        }

        spans.truncate(checkpoint_len);
        *remaining = checkpoint_rem;
    }

    spans.truncate(start);
    *remaining = initial_rem;
    false
}

fn write_compressed_elided_owned(
    spans: &mut Vec<Span<'static>>,
    dirs: &[&OsStr],
    remaining: &mut usize,
    level: usize,
    style: &StyleConfig,
    min_tail: usize,
) -> bool {
    let mut compressed: Vec<String> = Vec::with_capacity(dirs.len());

    for d in dirs {
        let s = os_to_str(d).into_owned();
        compressed.push(compress_segment(&s, level));
    }

    write_middle_elided_strs_owned(spans, &compressed, remaining, style, min_tail)
}

fn write_middle_elided_strs_owned(
    spans: &mut Vec<Span<'static>>,
    dirs: &[String],
    remaining: &mut usize,
    style: &StyleConfig,
    min_tail: usize,
) -> bool {
    if dirs.len() < 2 {
        return false;
    }

    let start = spans.len();
    let initial_rem = *remaining;

    // Try preserving the first segment
    let first = &dirs[0];
    let fw = width(first);

    if fw <= *remaining {
        spans.push(Span::styled(first.clone(), style.path));
        *remaining -= fw;

        let elide_w = width("…/");
        for tail_len in (1..dirs.len()).rev() {
            if tail_len < min_tail {
                continue;
            }

            let checkpoint_len = spans.len();
            let checkpoint_rem = *remaining;

            if *remaining < elide_w {
                spans.truncate(start);
                *remaining = initial_rem;
                break;
            }

            spans.push(Span::styled("…/".to_string(), style.dim));
            *remaining -= elide_w;

            let tail = &dirs[dirs.len() - tail_len..];

            let mut ok = true;
            for (i, d) in tail.iter().enumerate() {
                if i > 0 {
                    if *remaining < 1 {
                        ok = false;
                        break;
                    }
                    spans.push(Span::styled("/".to_string(), style.dim));
                    *remaining -= 1;
                }

                let w = width(d);

                if w > *remaining {
                    ok = false;
                    break;
                }

                spans.push(Span::styled(d.clone(), style.path));
                *remaining -= w;
            }

            if ok {
                return true;
            }

            spans.truncate(checkpoint_len);
            *remaining = checkpoint_rem;
        }

        spans.truncate(start);
        *remaining = initial_rem;
    }

    // Try eliding without the first segment
    let elide_w = width("…/");
    for tail_len in (1..dirs.len()).rev() {
        if tail_len < min_tail {
            continue;
        }

        let checkpoint_len = spans.len();
        let checkpoint_rem = *remaining;

        if *remaining < elide_w {
            spans.truncate(start);
            *remaining = initial_rem;
            return false;
        }

        spans.push(Span::styled("…/".to_string(), style.dim));
        *remaining -= elide_w;

        let tail = &dirs[dirs.len() - tail_len..];

        let mut ok = true;
        for (i, d) in tail.iter().enumerate() {
            if i > 0 {
                if *remaining < 1 {
                    ok = false;
                    break;
                }
                spans.push(Span::styled("/".to_string(), style.dim));
                *remaining -= 1;
            }

            let w = width(d);

            if w > *remaining {
                ok = false;
                break;
            }

            spans.push(Span::styled(d.clone(), style.path));
            *remaining -= w;
        }

        if ok {
            return true;
        }

        spans.truncate(checkpoint_len);
        *remaining = checkpoint_rem;
    }

    spans.truncate(start);
    *remaining = initial_rem;
    false
}

fn write_minimal_owned(
    spans: &mut Vec<Span<'static>>,
    last: &OsStr,
    remaining: &mut usize,
    style: &StyleConfig,
) -> bool {
    let s = os_to_str(last).into_owned();

    if width("…/") + width(&s) > *remaining {
        // try compressed variants of the last segment as a final attempt
        for level in 1..=3 {
            let comp = compress_segment(&s, level);
            if width(&comp) <= *remaining {
                spans.push(Span::styled("…/".to_string(), style.dim));
                spans.push(Span::styled(comp, style.path));
                return true;
            }
        }
        return false;
    }

    spans.push(Span::styled("…/".to_string(), style.dim));
    spans.push(Span::styled(s, style.path));

    true
}

fn split_path(path: &Path) -> (&OsStr, Vec<&OsStr>) {
    let filename = path.file_name().unwrap_or_else(|| "".as_ref());

    let mut dirs = Vec::new();
    if let Some(parent) = path.parent() {
        for comp in parent.components() {
            if let Component::Normal(os) = comp {
                dirs.push(os);
            }
        }
    }
    (filename, dirs)
}

fn os_to_str(os: &OsStr) -> Cow<'_, str> {
    os.to_string_lossy()
}

pub fn width(s: &str) -> usize {
    UnicodeWidthStr::width(s)
}

fn compress_segment(seg: &str, level: usize) -> String {
    match level {
        1 => truncate(seg, 8),
        2 => truncate(seg, 5),
        _ => initialism(seg),
    }
}

fn truncate(seg: &str, max_len: usize) -> String {
    if seg.chars().count() <= max_len {
        return seg.to_string();
    }

    let mut out = String::with_capacity(max_len);

    for (i, ch) in seg.chars().enumerate() {
        if i >= max_len - 1 {
            break;
        }
        out.push(ch);
    }

    out.push('…');
    out
}

fn initialism(seg: &str) -> String {
    let mut out = String::new();

    for part in seg.split(['_', '-', '.']) {
        if let Some(c) = part.chars().next() {
            out.push(c);
        }
    }

    if out.is_empty() {
        seg.chars().next().unwrap_or('?').to_string()
    } else {
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;
    use ratatui::style::Style;

    /// Convenience: default style config for tests
    fn default_cfg() -> StyleConfig {
        StyleConfig {
            filename: Style::default(),
            path: Style::default(),
            dim: Style::default(),
            separator: Style::default(),
        }
    }

    /// Convert a Line into a simple String for assertions
    fn line_to_string(line: &ratatui::text::Line) -> String {
        // ratatui::text::Line has `spans` field holding Vec<Span>
        line.spans.iter().map(|span| span.content.clone()).collect()
    }

    // ---------- Root-level files ----------
    #[test]
    fn test_root_file() {
        let path = Path::new("main.rs");
        let line = format_path_line(path, 40);
        assert_eq!(line_to_string(&line), "main.rs");
    }

    #[test]
    fn test_root_file_with_label() {
        let path = Path::new("Cargo.toml");
        let cfg = default_cfg();
        let mut spans = format_path_spans(path, 20, cfg);
        spans.push(ratatui::text::Span::raw(" (unstaged)"));
        let s: String = spans.iter().map(|sp| sp.content.clone()).collect();
        assert_eq!(s, "Cargo.toml (unstaged)");
    }

    // ---------- Single-directory files ----------
    #[test]
    fn test_single_directory_short() {
        let path = Path::new("src/main.rs");
        let line = format_path_line(path, 40);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("src"));
    }

    #[test]
    fn test_single_long_directory_elision() {
        let path = Path::new("very_long_directory_name/main.rs");
        let line = format_path_line(path, 15);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("…") || s.contains("vldn") || s.contains("very_lo…"));
        assert!(width(&s) <= 15);
    }

    // ---------- Multiple directories ----------
    #[test]
    fn test_multiple_directories_wide() {
        let path = Path::new("src/compiler/parser/ast/main.rs");
        let line = format_path_line(path, 80);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("src/compiler/parser/ast"));
    }

    #[test]
    fn test_multiple_directories_medium() {
        let path = Path::new("src/compiler/parser/ast/main.rs");
        let line = format_path_line(path, 30);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("…"));
    }

    #[test]
    fn test_multiple_directories_narrow() {
        let path = Path::new("src/compiler/parser/ast/main.rs");
        let line = format_path_line(path, 15);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("…"));
    }

    #[test]
    fn test_smart_directory_compression_multiple_dirs() {
        // Deep path with one very long directory
        let path = Path::new("src/compiler/very_long_directory_name/parser/ast/main.rs");
        let line = format_path_line(path, 30);
        let s = line_to_string(&line);
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("…") || s.contains("vldn") || s.contains("very_lo…"));
        assert!(width(&s) <= 30);
    }

    // ---------- Narrow width edge ----------
    #[test]
    fn test_filename_only_when_too_narrow() {
        let path = Path::new("src/compiler/parser/ast/main.rs");
        let line = format_path_line(path, 5);
        let s = line_to_string(&line);
        assert_eq!(s, "main.rs");
    }

    // ---------- Unicode filenames ----------
    #[test]
    fn test_unicode_filename() {
        let path = Path::new("src/解析/メイン.rs");
        let line = format_path_line(path, 40);
        let s = line_to_string(&line);
        assert!(s.contains("メイン.rs"));
        assert!(s.contains("解析") || s.contains("…"));
    }

    // ---------- Labels / decorations ----------
    #[test]
    fn test_path_with_label_simulation() {
        let path = Path::new("src/compiler/parser/main.rs");
        let cfg = default_cfg();
        let mut spans = format_path_spans(path, 20, cfg);
        spans.push(ratatui::text::Span::raw(" (staged)"));
        let s: String = spans.iter().map(|sp| sp.content.clone()).collect();
        assert!(s.starts_with("main.rs"));
        assert!(s.contains("…") || s.contains("parser"));
        assert!(s.contains("(staged)"));
    }
}
