//! Document ID index for resolving external document references
//!
//! This module provides a global index that maps document IDs to file paths,
//! loaded from `.wasot-ids` files.

use std::path::Path;
use std::sync::OnceLock;

/// Global document ID index, initialized once at startup
static DOCUMENT_ID_INDEX: OnceLock<DocumentIdIndex> = OnceLock::new();

/// An index mapping document IDs to their file paths.
/// Uses a sorted Vec for memory efficiency and binary search for lookups.
#[derive(Debug, Default)]
pub struct DocumentIdIndex {
    /// Sorted by document ID for binary search
    entries: Vec<(String, String)>,
}

impl DocumentIdIndex {
    /// Create an empty index
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }

    /// Load index from a file. Returns empty index if file doesn't exist.
    /// Format: `<document-id> <path>` per line, assumed sorted by ID.
    pub fn load_from_file(path: &Path) -> Self {
        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(_) => return Self::new(), // File doesn't exist or can't be read
        };

        Self::parse(&content)
    }

    /// Parse index from string content
    pub fn parse(content: &str) -> Self {
        let mut entries: Vec<(String, String)> = content
            .lines()
            .filter_map(|line| {
                let line = line.trim();
                if line.is_empty() {
                    return None;
                }
                // Split on first space
                let (id, path) = line.split_once(' ')?;
                Some((id.to_string(), path.to_string()))
            })
            .collect();

        // Ensure sorted (file should already be sorted, but verify)
        entries.sort_by(|a, b| a.0.cmp(&b.0));

        Self { entries }
    }

    /// Look up a document path by ID using binary search
    pub fn get(&self, document_id: &str) -> Option<&str> {
        self.entries
            .binary_search_by(|(id, _)| id.as_str().cmp(document_id))
            .ok()
            .map(|idx| self.entries[idx].1.as_str())
    }

    /// Check if the index is empty
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Get the number of entries
    pub fn len(&self) -> usize {
        self.entries.len()
    }
}

/// Initialize the global document ID index from `.wasot-ids` in the current directory.
/// This should be called once at startup. If already initialized, does nothing.
pub fn init_global_index() {
    DOCUMENT_ID_INDEX.get_or_init(|| DocumentIdIndex::load_from_file(Path::new(".wasot-ids")));
}

/// Initialize the global index with an empty index (disables path resolution).
/// Use this when the `--no-id-index` flag is set.
pub fn init_empty_index() {
    DOCUMENT_ID_INDEX.get_or_init(DocumentIdIndex::new);
}

/// Look up a document path by ID from the global index.
/// Returns None if the index is not initialized or the ID is not found.
pub fn lookup_document_path(document_id: &str) -> Option<&'static str> {
    DOCUMENT_ID_INDEX.get()?.get(document_id)
}

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

    #[test]
    fn test_parse_empty() {
        let index = DocumentIdIndex::parse("");
        assert!(index.is_empty());
    }

    #[test]
    fn test_parse_single_entry() {
        let index = DocumentIdIndex::parse("doc-id path/to/file.wasot");
        assert_eq!(index.len(), 1);
        assert_eq!(index.get("doc-id"), Some("path/to/file.wasot"));
    }

    #[test]
    fn test_parse_multiple_entries() {
        let content = "aaa first.wasot\nbbb second.wasot\nccc third.wasot";
        let index = DocumentIdIndex::parse(content);
        assert_eq!(index.len(), 3);
        assert_eq!(index.get("aaa"), Some("first.wasot"));
        assert_eq!(index.get("bbb"), Some("second.wasot"));
        assert_eq!(index.get("ccc"), Some("third.wasot"));
    }

    #[test]
    fn test_lookup_not_found() {
        let index = DocumentIdIndex::parse("doc-id path.wasot");
        assert_eq!(index.get("nonexistent"), None);
    }

    #[test]
    fn test_binary_search_efficiency() {
        // Even if input is unsorted, the index sorts it
        let content = "zzz last.wasot\naaa first.wasot\nmmm middle.wasot";
        let index = DocumentIdIndex::parse(content);
        assert_eq!(index.get("aaa"), Some("first.wasot"));
        assert_eq!(index.get("mmm"), Some("middle.wasot"));
        assert_eq!(index.get("zzz"), Some("last.wasot"));
    }

    #[test]
    fn test_empty_lines_ignored() {
        let content = "doc-id path.wasot\n\n  \nanother other.wasot";
        let index = DocumentIdIndex::parse(content);
        assert_eq!(index.len(), 2);
    }

    #[test]
    fn test_path_with_spaces() {
        // Path can contain spaces - only first space separates ID from path
        let content = "doc-id path with spaces/file.wasot";
        let index = DocumentIdIndex::parse(content);
        assert_eq!(index.get("doc-id"), Some("path with spaces/file.wasot"));
    }
}
