//! Line range viewing for view command.

use super::report::ViewReport;
use crate::parsers;
use crate::tree::{ViewNode, ViewNodeKind};
use std::collections::HashSet;

/// Parse a line target like "file.rs:30" or "file.rs:30-55".
/// Returns (file_path, start, end) where end is None for single line.
pub fn parse_line_target(target: &str) -> Option<(String, usize, Option<usize>)> {
    let colon_pos = target.rfind(':')?;
    let (path, range) = target.split_at(colon_pos);
    let range = &range[1..];

    if let Some((start_str, end_str)) = range.split_once('-') {
        let start: usize = start_str.parse().ok()?;
        let end: usize = end_str.parse().ok()?;
        if start == 0 || end == 0 || start > end {
            return None;
        }
        return Some((path.to_string(), start, Some(end)));
    }

    let line: usize = range.parse().ok()?;
    if line == 0 {
        return None;
    }
    Some((path.to_string(), line, None))
}

/// Build a line range view for the service layer.
pub fn build_view_line_range_service(
    file_path: &str,
    start: usize,
    end: usize,
    root: &std::path::Path,
    docstring_mode: crate::tree::DocstringDisplay,
) -> Result<ViewReport, String> {
    let matches = crate::path_resolve::resolve_unified_all(file_path, root);
    let resolved = match matches.len() {
        0 => return Err(format!("File not found: {}", file_path)),
        // normalize-syntax-allow: rust/unwrap-in-impl - match arm guards exactly 1 match, so next() is always Some
        1 => matches.into_iter().next().unwrap(),
        _ => {
            return Err(format!(
                "Multiple matches for '{}' - be more specific",
                file_path
            ));
        }
    };

    if resolved.is_directory {
        return Err(format!(
            "Cannot use line range with directory: {}",
            file_path
        ));
    }

    let full_path = root.join(&resolved.file_path);
    let display_path = resolved.file_path.clone();

    let content =
        std::fs::read_to_string(&full_path).map_err(|e| format!("Failed to read file: {}", e))?;

    let lines: Vec<&str> = content.lines().collect();
    let total_lines = lines.len();

    if start > total_lines {
        return Err(format!(
            "Line {} is past end of file ({} lines)",
            start, total_lines
        ));
    }

    let actual_start = start.min(total_lines);
    let actual_end = end.min(total_lines);
    let range_start = actual_start.saturating_sub(1);
    let range_end = actual_end.min(lines.len());

    let grammar =
        normalize_languages::support_for_path(&full_path).map(|s| s.grammar_name().to_string());
    let source: String = if docstring_mode != crate::tree::DocstringDisplay::Full {
        if let Some(ref g) = grammar {
            let doc_lines = find_doc_comment_lines(&content, g, actual_start, actual_end);
            lines[range_start..range_end]
                .iter()
                .enumerate()
                .filter(|(i, _)| !doc_lines.contains(&(actual_start + i)))
                .map(|(_, line)| *line)
                .collect::<Vec<_>>()
                .join("\n")
        } else {
            lines[range_start..range_end].join("\n")
        }
    } else {
        lines[range_start..range_end].join("\n")
    };

    // Build a synthetic "symbol" node so the renderer dispatches to line-range mode.
    // We use a Symbol kind with "line_range" as the kind string.
    let target = format!("{}:{}-{}", display_path, actual_start, actual_end);
    let node = ViewNode {
        name: target.clone(),
        kind: ViewNodeKind::Symbol("line_range".to_string()),
        path: display_path,
        children: Vec::new(),
        signature: None,
        docstring: None,
        line_range: Some((actual_start, actual_end)),
        grammar: grammar.clone(),
    };

    Ok(ViewReport {
        target,
        node,
        source: Some(source),
        imports: Vec::new(),
        exports: Vec::new(),
        parent_signatures: Vec::new(),
        line_range: Some((actual_start, actual_end)),
        grammar,
        warnings: Vec::new(),
        summary: None,
        dir_context: None,
    })
}

/// Find line numbers that contain doc comments within a range.
fn find_doc_comment_lines(
    content: &str,
    grammar: &str,
    start_line: usize,
    end_line: usize,
) -> HashSet<usize> {
    let mut doc_lines = HashSet::new();

    if let Some(tree) = parsers::parse_with_grammar(grammar, content) {
        let mut cursor = tree.walk();
        collect_doc_comment_lines(&mut cursor, start_line, end_line, &mut doc_lines);
    }

    doc_lines
}

/// Recursively collect doc comment line numbers.
fn collect_doc_comment_lines(
    cursor: &mut tree_sitter::TreeCursor,
    start_line: usize,
    end_line: usize,
    doc_lines: &mut HashSet<usize>,
) {
    loop {
        let node = cursor.node();
        let kind = node.kind();

        let is_comment = kind == "comment"
            || kind == "line_comment"
            || kind == "block_comment"
            || kind == "doc_comment"
            || kind.contains("comment");

        if is_comment {
            let node_start = node.start_position().row + 1;
            let end_pos = node.end_position();
            let node_end = if end_pos.column == 0 {
                end_pos.row
            } else {
                end_pos.row + 1
            };

            for line in node_start..=node_end {
                if line >= start_line && line <= end_line {
                    doc_lines.insert(line);
                }
            }
        }

        if cursor.goto_first_child() {
            collect_doc_comment_lines(cursor, start_line, end_line, doc_lines);
            cursor.goto_parent();
        }

        if !cursor.goto_next_sibling() {
            break;
        }
    }
}
