//! Document indexing for the Wasot LSP server.
//!
//! This module provides functionality to index Wasot documents for:
//! - Tracking block IDs and their positions
//! - Tracking links and their positions
//! - Resolving go-to-definition targets

use crate::parsers::wasot::parser::TargetType;
use crate::parsers::wasot::{detect_links, is_meta_line, parse_link, MetaLine};
use std::collections::HashMap;

/// Represents a position in a document (0-indexed line and character).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
    pub line: u32,
    pub character: u32,
}

impl Position {
    #[must_use]
    pub fn new(line: u32, character: u32) -> Self {
        Self { line, character }
    }
}

/// Represents a range in a document.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Range {
    pub start: Position,
    pub end: Position,
}

impl Range {
    #[must_use]
    pub fn new(start: Position, end: Position) -> Self {
        Self { start, end }
    }

    /// Check if a position is within this range.
    #[must_use]
    pub fn contains(&self, pos: Position) -> bool {
        if pos.line < self.start.line || pos.line > self.end.line {
            return false;
        }
        if pos.line == self.start.line && pos.character < self.start.character {
            return false;
        }
        if pos.line == self.end.line && pos.character > self.end.character {
            return false;
        }
        true
    }
}

/// Information about an indexed ID in the document.
#[derive(Debug, Clone)]
pub struct IndexedId {
    pub id: String,
    pub range: Range,
    pub block_type: String,
}

/// Information about an indexed link in the document.
#[derive(Debug, Clone)]
pub struct IndexedLink {
    pub range: Range,
    pub target: Option<String>,
    pub target_type: LinkTargetType,
    pub name: Option<String>,
}

/// Type of link target for resolution.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkTargetType {
    /// Local ID reference within the same document.
    LocalId,
    /// Reference to an ID in another document (document.id format).
    ExternalId { document: String, id: String },
    /// URI (http://, https://, etc.).
    Uri,
    /// Footnote reference (*1, *2, etc.).
    Footnote,
}

/// Result of resolving a link target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedTarget {
    /// Target resolved to a position in the same document.
    SameDocument(Range),
    /// Target resolved to another document (with optional ID).
    OtherDocument {
        document: String,
        id: Option<String>,
    },
    /// Target is a URI.
    Uri(String),
    /// Target could not be resolved.
    NotFound,
}

/// Index of a single Wasot document.
#[derive(Debug, Default)]
pub struct DocumentIndex {
    /// Map from ID to its definition location.
    ids: HashMap<String, IndexedId>,
    /// All links in the document with their positions.
    links: Vec<IndexedLink>,
    /// Document content (for re-parsing on changes).
    content: String,
}

impl DocumentIndex {
    /// Create a new empty document index.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Index a document from its content.
    #[must_use]
    pub fn from_content(content: &str) -> Self {
        let mut index = Self::new();
        index.content = content.to_string();
        index.index_content(content);
        index
    }

    /// Re-index the document with new content.
    pub fn update(&mut self, content: &str) {
        self.ids.clear();
        self.links.clear();
        self.content = content.to_string();
        self.index_content(content);
    }

    /// Get an ID definition by name.
    #[must_use]
    pub fn get_id(&self, id: &str) -> Option<&IndexedId> {
        self.ids.get(id)
    }

    /// Get all IDs in the document.
    #[must_use]
    pub fn all_ids(&self) -> Vec<&IndexedId> {
        self.ids.values().collect()
    }

    /// Get the maximum numeric (base36) ID value in the document.
    ///
    /// Returns `None` if there are no numeric IDs.
    #[must_use]
    pub fn max_numeric_id(&self) -> Option<u64> {
        self.ids
            .values()
            .filter_map(|indexed_id| crate::base36::base36_to_u64(&indexed_id.id).ok())
            .max()
    }

    /// Find a link at the given position.
    #[must_use]
    pub fn link_at_position(&self, pos: Position) -> Option<&IndexedLink> {
        self.links.iter().find(|link| link.range.contains(pos))
    }

    /// Get all links in the document.
    #[must_use]
    pub fn all_links(&self) -> &[IndexedLink] {
        &self.links
    }

    /// Get the document content.
    #[must_use]
    pub fn content(&self) -> &str {
        &self.content
    }

    /// Resolve a link target to a location.
    #[must_use]
    pub fn resolve_link(&self, link: &IndexedLink) -> ResolvedTarget {
        match &link.target_type {
            LinkTargetType::LocalId => {
                if let Some(target) = &link.target {
                    if let Some(indexed_id) = self.ids.get(target) {
                        return ResolvedTarget::SameDocument(indexed_id.range);
                    }
                }
                ResolvedTarget::NotFound
            }
            LinkTargetType::ExternalId { document, id } => ResolvedTarget::OtherDocument {
                document: document.clone(),
                id: Some(id.clone()),
            },
            LinkTargetType::Uri => {
                if let Some(target) = &link.target {
                    ResolvedTarget::Uri(target.clone())
                } else {
                    ResolvedTarget::NotFound
                }
            }
            LinkTargetType::Footnote => {
                // Footnotes could be resolved to their definitions if needed
                ResolvedTarget::NotFound
            }
        }
    }

    /// Index the document content.
    fn index_content(&mut self, content: &str) {
        let lines: Vec<&str> = content.lines().collect();

        for (line_num, line) in lines.iter().enumerate() {
            // Index meta-lines for IDs
            if is_meta_line(line) {
                self.index_meta_line(line, line_num);
            }

            // Index links in any line
            self.index_links_in_line(line, line_num);
        }
    }

    /// Index a meta-line for ID definitions.
    fn index_meta_line(&mut self, line: &str, line_num: usize) {
        if let Some(meta_line) = MetaLine::parse(line, line_num) {
            if !meta_line.id.value.is_empty() {
                // Find the position of the ID in the line
                let id_str = format!(":{}", meta_line.id.value);
                if let Some(id_pos) = line.find(&id_str) {
                    let start = Position::new(line_num as u32, id_pos as u32);
                    let end = Position::new(line_num as u32, (id_pos + id_str.len()) as u32);

                    self.ids.insert(
                        meta_line.id.value.clone(),
                        IndexedId {
                            id: meta_line.id.value,
                            range: Range::new(start, end),
                            block_type: meta_line.type_name,
                        },
                    );
                }
            }
        }
    }

    /// Index links in a line of text.
    fn index_links_in_line(&mut self, line: &str, line_num: usize) {
        let link_spans = detect_links(line);

        for span in link_spans {
            let link = parse_link(&span.content, span.start_index, span.length);

            let target_type = match &link.target_type {
                TargetType::Id => LinkTargetType::LocalId,
                TargetType::IdInDocument(doc_ref) => LinkTargetType::ExternalId {
                    document: doc_ref.document_id.value.clone(),
                    id: doc_ref.local_id.value.clone(),
                },
                TargetType::Uri => LinkTargetType::Uri,
                TargetType::FootNote => LinkTargetType::Footnote,
            };

            // Determine the target string
            let target = link.target.clone().or_else(|| {
                // If no explicit target, use the name as the target for local IDs
                if matches!(target_type, LinkTargetType::LocalId) {
                    link.name.as_ref().map(|n| n.content.clone())
                } else {
                    None
                }
            });

            let start = Position::new(line_num as u32, span.start_index as u32);
            let end = Position::new(line_num as u32, (span.start_index + span.length) as u32);

            self.links.push(IndexedLink {
                range: Range::new(start, end),
                target,
                target_type,
                name: link.name.map(|n| n.content),
            });
        }
    }
}

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

    // ==================== Position and Range Tests ====================

    #[test]
    fn test_position_creation() {
        let pos = Position::new(10, 5);
        assert_eq!(pos.line, 10);
        assert_eq!(pos.character, 5);
    }

    #[test]
    fn test_range_creation() {
        let start = Position::new(1, 0);
        let end = Position::new(1, 10);
        let range = Range::new(start, end);
        assert_eq!(range.start, start);
        assert_eq!(range.end, end);
    }

    #[test]
    fn test_range_contains_position_within() {
        let range = Range::new(Position::new(1, 5), Position::new(1, 15));

        // Position within range
        assert!(range.contains(Position::new(1, 5)));
        assert!(range.contains(Position::new(1, 10)));
        assert!(range.contains(Position::new(1, 15)));
    }

    #[test]
    fn test_range_contains_position_outside() {
        let range = Range::new(Position::new(1, 5), Position::new(1, 15));

        // Position before range
        assert!(!range.contains(Position::new(1, 4)));
        // Position after range
        assert!(!range.contains(Position::new(1, 16)));
        // Wrong line
        assert!(!range.contains(Position::new(0, 10)));
        assert!(!range.contains(Position::new(2, 10)));
    }

    #[test]
    fn test_range_contains_multiline() {
        let range = Range::new(Position::new(1, 5), Position::new(3, 10));

        // Start line checks
        assert!(range.contains(Position::new(1, 5)));
        assert!(range.contains(Position::new(1, 100))); // After start char on start line
        assert!(!range.contains(Position::new(1, 4))); // Before start char

        // Middle line - any character is valid
        assert!(range.contains(Position::new(2, 0)));
        assert!(range.contains(Position::new(2, 100)));

        // End line checks
        assert!(range.contains(Position::new(3, 0))); // Before end char
        assert!(range.contains(Position::new(3, 10))); // At end char
        assert!(!range.contains(Position::new(3, 11))); // After end char
    }

    // ==================== Document Indexing Tests ====================

    #[test]
    fn test_empty_document() {
        let index = DocumentIndex::from_content("");
        assert!(index.all_ids().is_empty());
        assert!(index.link_at_position(Position::new(0, 0)).is_none());
    }

    #[test]
    fn test_index_single_id() {
        let content = ".paragraph :my-id\nSome content";
        let index = DocumentIndex::from_content(content);

        let ids = index.all_ids();
        assert_eq!(ids.len(), 1);

        let id = index.get_id("my-id").unwrap();
        assert_eq!(id.id, "my-id");
        assert_eq!(id.block_type, "paragraph");
        assert_eq!(id.range.start.line, 0);
    }

    #[test]
    fn test_index_multiple_ids() {
        let content = ".div :container\n..p :first-para\nContent\n..p :second-para\nMore content";
        let index = DocumentIndex::from_content(content);

        assert_eq!(index.all_ids().len(), 3);
        assert!(index.get_id("container").is_some());
        assert!(index.get_id("first-para").is_some());
        assert!(index.get_id("second-para").is_some());
    }

    #[test]
    fn test_index_id_position() {
        let content = ".block :test-id";
        let index = DocumentIndex::from_content(content);

        let id = index.get_id("test-id").unwrap();
        // :test-id starts at position 7
        assert_eq!(id.range.start.character, 7);
        assert_eq!(id.range.end.character, 15); // :test-id is 8 chars
    }

    // ==================== Link Indexing Tests ====================

    #[test]
    fn test_index_simple_link() {
        let content = "Text with ((link name)) here";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 12)).unwrap();
        assert_eq!(link.name, Some("link name".to_string()));
    }

    #[test]
    fn test_index_link_with_target() {
        let content = "Click ((here :my-target)) to go";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 10)).unwrap();
        assert_eq!(link.target, Some("my-target".to_string()));
        assert_eq!(link.target_type, LinkTargetType::LocalId);
    }

    #[test]
    fn test_index_link_with_uri() {
        let content = "Visit ((website :https://example.com))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 10)).unwrap();
        assert_eq!(link.target, Some("https://example.com".to_string()));
        assert_eq!(link.target_type, LinkTargetType::Uri);
    }

    #[test]
    fn test_index_link_with_external_id() {
        let content = "See ((other doc :readme.intro))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 10)).unwrap();
        assert_eq!(
            link.target_type,
            LinkTargetType::ExternalId {
                document: "readme".to_string(),
                id: "intro".to_string()
            }
        );
    }

    #[test]
    fn test_index_footnote_link() {
        let content = "See reference (( :*1))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 16)).unwrap();
        assert_eq!(link.target_type, LinkTargetType::Footnote);
    }

    #[test]
    fn test_multiple_links_on_line() {
        let content = "Go ((here :first)) or ((there :second))";
        let index = DocumentIndex::from_content(content);

        let link1 = index.link_at_position(Position::new(0, 5)).unwrap();
        assert_eq!(link1.target, Some("first".to_string()));

        let link2 = index.link_at_position(Position::new(0, 25)).unwrap();
        assert_eq!(link2.target, Some("second".to_string()));
    }

    #[test]
    fn test_link_not_at_position() {
        let content = "No links here";
        let index = DocumentIndex::from_content(content);

        assert!(index.link_at_position(Position::new(0, 5)).is_none());
    }

    // ==================== Link Resolution Tests ====================

    #[test]
    fn test_resolve_local_link() {
        let content = ".section :target-id\nContent\nGo to ((section :target-id))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(2, 10)).unwrap();
        let resolved = index.resolve_link(link);

        match resolved {
            ResolvedTarget::SameDocument(range) => {
                assert_eq!(range.start.line, 0);
            }
            _ => panic!("Expected SameDocument resolution"),
        }
    }

    #[test]
    fn test_resolve_missing_local_link() {
        let content = "Go to ((nonexistent :missing-id))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 10)).unwrap();
        let resolved = index.resolve_link(link);

        assert_eq!(resolved, ResolvedTarget::NotFound);
    }

    #[test]
    fn test_resolve_external_link() {
        let content = "See ((other :readme.section))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 8)).unwrap();
        let resolved = index.resolve_link(link);

        match resolved {
            ResolvedTarget::OtherDocument { document, id } => {
                assert_eq!(document, "readme");
                assert_eq!(id, Some("section".to_string()));
            }
            _ => panic!("Expected OtherDocument resolution"),
        }
    }

    #[test]
    fn test_resolve_uri_link() {
        let content = "Visit ((site :https://example.com))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 10)).unwrap();
        let resolved = index.resolve_link(link);

        match resolved {
            ResolvedTarget::Uri(uri) => {
                assert_eq!(uri, "https://example.com");
            }
            _ => panic!("Expected Uri resolution"),
        }
    }

    // ==================== Document Update Tests ====================

    #[test]
    fn test_document_update() {
        let mut index = DocumentIndex::from_content(".block :old-id");
        assert!(index.get_id("old-id").is_some());
        assert!(index.get_id("new-id").is_none());

        index.update(".block :new-id");
        assert!(index.get_id("old-id").is_none());
        assert!(index.get_id("new-id").is_some());
    }

    // ==================== Edge Cases ====================

    #[test]
    fn test_id_with_special_characters() {
        let content = ".block :my-id_123";
        let index = DocumentIndex::from_content(content);

        assert!(index.get_id("my-id_123").is_some());
    }

    #[test]
    fn test_link_with_empty_name() {
        let content = "Link (( :target))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 8)).unwrap();
        assert!(link.name.is_none());
        assert_eq!(link.target, Some("target".to_string()));
    }

    #[test]
    fn test_unicode_content() {
        let content = ".paragraph :日本語id\n((リンク :target))";
        let index = DocumentIndex::from_content(content);

        assert!(index.get_id("日本語id").is_some());
    }

    #[test]
    fn test_link_uses_name_as_target_when_no_explicit_target() {
        let content = "Click ((my-id)) to go";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(0, 8)).unwrap();
        // When no explicit target, the name becomes the target for local ID resolution
        assert_eq!(link.target, Some("my-id".to_string()));
        assert_eq!(link.target_type, LinkTargetType::LocalId);
    }

    #[test]
    fn test_link_resolution_with_name_as_target() {
        let content = ".section :intro\nGo to ((intro))";
        let index = DocumentIndex::from_content(content);

        let link = index.link_at_position(Position::new(1, 8)).unwrap();
        let resolved = index.resolve_link(link);

        match resolved {
            ResolvedTarget::SameDocument(range) => {
                assert_eq!(range.start.line, 0);
            }
            _ => panic!("Expected SameDocument resolution, got {:?}", resolved),
        }
    }
}
