//! LSP server implementation for Wasot.

use super::document::{DocumentIndex, LinkTargetType, Position, ResolvedTarget};
use crate::consumers::wasot_reconstructor::WasotReconstructor;
use crate::emitters::wasot_file_emitter::WasotFileEmitter;
use crate::events::run;
use crate::parsers::wasot::parser::MetaLine;
use dashmap::DashMap;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};

/// Semantic token types used by the Wasot LSP.
const TOKEN_TYPES: &[SemanticTokenType] = &[
    SemanticTokenType::NAMESPACE, // 0: dots (nesting level)
    SemanticTokenType::CLASS,     // 1: block type name
    SemanticTokenType::STRING,    // 2: quoted strings
    SemanticTokenType::VARIABLE,  // 3: unquoted arguments
    SemanticTokenType::PARAMETER, // 4: key names in key=value
    SemanticTokenType::OPERATOR,  // 5: equals sign
    SemanticTokenType::DECORATOR, // 6: ID (colon prefix)
    SemanticTokenType::COMMENT,   // 7: inline text separator (..)
    SemanticTokenType::KEYWORD,   // 8: special markers (#, /)
];

/// Semantic token modifiers (none used currently).
const TOKEN_MODIFIERS: &[SemanticTokenModifier] = &[];

/// The Wasot language server.
pub struct WasotLanguageServer {
    /// Client for sending messages back to the editor.
    client: Client,
    /// Document indexes keyed by URI.
    documents: Arc<DashMap<String, DocumentIndex>>,
    /// Workspace root path (for resolving external document references).
    workspace_root: Arc<std::sync::RwLock<Option<PathBuf>>>,
}

impl WasotLanguageServer {
    /// Create a new language server instance.
    #[must_use]
    pub fn new(client: Client) -> Self {
        Self {
            client,
            documents: Arc::new(DashMap::new()),
            workspace_root: Arc::new(std::sync::RwLock::new(None)),
        }
    }

    /// Index a document and store it.
    fn index_document(&self, uri: &str, content: &str) {
        let index = DocumentIndex::from_content(content);
        self.documents.insert(uri.to_string(), index);
    }

    /// Get the document index for a URI.
    fn get_document(
        &self,
        uri: &str,
    ) -> Option<dashmap::mapref::one::Ref<'_, String, DocumentIndex>> {
        self.documents.get(uri)
    }

    /// Convert an LSP position to our internal Position.
    fn to_internal_position(pos: &tower_lsp::lsp_types::Position) -> Position {
        Position::new(pos.line, pos.character)
    }

    /// Convert our internal Position to an LSP position.
    fn to_lsp_position(pos: &Position) -> tower_lsp::lsp_types::Position {
        tower_lsp::lsp_types::Position::new(pos.line, pos.character)
    }

    /// Convert our internal Range to an LSP range.
    fn to_lsp_range(range: &super::document::Range) -> tower_lsp::lsp_types::Range {
        tower_lsp::lsp_types::Range::new(
            Self::to_lsp_position(&range.start),
            Self::to_lsp_position(&range.end),
        )
    }

    /// Convert a byte offset to a character offset in a string.
    fn byte_offset_to_char_offset(s: &str, byte_offset: usize) -> u32 {
        s[..byte_offset].chars().count() as u32
    }

    /// Find substring starting from a character offset, return character offset.
    fn find_char_offset_from(haystack: &str, needle: &str, start_char: u32) -> Option<u32> {
        // Convert start_char to byte offset
        let start_byte: usize = haystack
            .char_indices()
            .nth(start_char as usize)
            .map(|(i, _)| i)
            .unwrap_or(haystack.len());

        if start_byte >= haystack.len() {
            return None;
        }

        haystack[start_byte..].find(needle).map(|byte_pos| {
            start_char + Self::byte_offset_to_char_offset(&haystack[start_byte..], byte_pos)
        })
    }

    /// Try to resolve an external document reference to a file path.
    fn resolve_external_document(&self, document_id: &str) -> Option<Url> {
        let workspace_root = self.workspace_root.read().ok()?;
        let root = workspace_root.as_ref()?;

        // Try common extensions
        for ext in &["wasot", "wst", ""] {
            let filename = if ext.is_empty() {
                document_id.to_string()
            } else {
                format!("{document_id}.{ext}")
            };
            let path = root.join(&filename);
            if path.exists() {
                return Url::from_file_path(&path).ok();
            }
        }

        None
    }
}

#[tower_lsp::async_trait]
impl LanguageServer for WasotLanguageServer {
    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
        // Store workspace root
        if let Some(root_uri) = params.root_uri {
            if let Ok(path) = root_uri.to_file_path() {
                if let Ok(mut workspace_root) = self.workspace_root.write() {
                    *workspace_root = Some(path);
                }
            }
        }

        Ok(InitializeResult {
            server_info: Some(ServerInfo {
                name: "wasot-lsp".to_string(),
                version: Some(env!("CARGO_PKG_VERSION").to_string()),
            }),
            capabilities: ServerCapabilities {
                text_document_sync: Some(TextDocumentSyncCapability::Options(
                    TextDocumentSyncOptions {
                        open_close: Some(true),
                        change: Some(TextDocumentSyncKind::FULL),
                        will_save: None,
                        will_save_wait_until: Some(true),
                        save: Some(TextDocumentSyncSaveOptions::Supported(true)),
                    },
                )),
                definition_provider: Some(OneOf::Left(true)),
                hover_provider: Some(HoverProviderCapability::Simple(true)),
                document_link_provider: Some(DocumentLinkOptions {
                    resolve_provider: Some(true),
                    work_done_progress_options: Default::default(),
                }),
                semantic_tokens_provider: Some(
                    SemanticTokensServerCapabilities::SemanticTokensOptions(
                        SemanticTokensOptions {
                            legend: SemanticTokensLegend {
                                token_types: TOKEN_TYPES.to_vec(),
                                token_modifiers: TOKEN_MODIFIERS.to_vec(),
                            },
                            full: Some(SemanticTokensFullOptions::Bool(true)),
                            range: None,
                            work_done_progress_options: Default::default(),
                        },
                    ),
                ),
                completion_provider: Some(CompletionOptions {
                    trigger_characters: Some(vec![":".to_string()]),
                    resolve_provider: Some(false),
                    ..Default::default()
                }),
                ..Default::default()
            },
            offset_encoding: None,
        })
    }

    async fn initialized(&self, _: InitializedParams) {
        self.client
            .log_message(MessageType::INFO, "Wasot LSP server initialized")
            .await;
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }

    async fn did_open(&self, params: DidOpenTextDocumentParams) {
        let uri = params.text_document.uri.to_string();
        let content = &params.text_document.text;
        self.index_document(&uri, content);

        self.client
            .log_message(MessageType::INFO, format!("Opened document: {uri}"))
            .await;
    }

    async fn did_change(&self, params: DidChangeTextDocumentParams) {
        let uri = params.text_document.uri.to_string();

        // We're using FULL sync, so we get the entire document
        if let Some(change) = params.content_changes.into_iter().next() {
            self.index_document(&uri, &change.text);
        }
    }

    async fn did_close(&self, params: DidCloseTextDocumentParams) {
        let uri = params.text_document.uri.to_string();
        self.documents.remove(&uri);
    }

    async fn will_save_wait_until(
        &self,
        params: WillSaveTextDocumentParams,
    ) -> Result<Option<Vec<TextEdit>>> {
        let uri = params.text_document.uri.to_string();

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

        let content = doc.content().to_string();
        let max_id = doc.max_numeric_id();
        drop(doc); // Release the lock before processing

        // Run the pretty printer with auto-generated IDs using a shared buffer
        let buffer = Arc::new(Mutex::new(Vec::<u8>::new()));
        let buffer_clone = Arc::clone(&buffer);

        // Create a wrapper that writes to the shared buffer
        struct SharedWriter(Arc<Mutex<Vec<u8>>>);
        impl Write for SharedWriter {
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                self.0.lock().unwrap().extend_from_slice(buf);
                Ok(buf.len())
            }
            fn flush(&mut self) -> std::io::Result<()> {
                Ok(())
            }
        }

        // Start auto-ID from max + 1 to avoid conflicts with existing IDs
        let starting_id = max_id.map_or(1, |id| id + 1);
        let emitter = WasotFileEmitter::from_string(content.clone())
            .with_auto_generate_ids(true)
            .with_starting_auto_id(starting_id);
        let reconstructor = WasotReconstructor::new_with_writer(SharedWriter(buffer_clone));

        let result = run(emitter, reconstructor);

        match result {
            Ok(()) => {
                let output = {
                    let buf = buffer.lock().unwrap();
                    String::from_utf8_lossy(&buf).to_string()
                };

                // Only return edit if content changed
                if output != content {
                    let line_count = content.lines().count() as u32;
                    let last_line_len = content.lines().last().map(|l| l.len()).unwrap_or(0) as u32;

                    Ok(Some(vec![TextEdit {
                        range: Range {
                            start: tower_lsp::lsp_types::Position::new(0, 0),
                            end: tower_lsp::lsp_types::Position::new(line_count, last_line_len),
                        },
                        new_text: output,
                    }]))
                } else {
                    Ok(None)
                }
            }
            Err(e) => {
                self.client
                    .log_message(MessageType::ERROR, format!("Pretty print failed: {e}"))
                    .await;
                Ok(None)
            }
        }
    }

    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
        let uri = params.text_document_position.text_document.uri.to_string();
        let position = params.text_document_position.position;

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

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

        // Get the current line
        let line_num = position.line as usize;
        if line_num >= lines.len() {
            return Ok(None);
        }
        let line = lines[line_num];
        let char_pos = position.character as usize;

        // Check if we're inside a link (( ... )) and just typed :
        let before_cursor = if char_pos <= line.len() {
            &line[..char_pos]
        } else {
            line
        };

        // Look for (( before cursor without closing ))
        let in_link = if let Some(link_start) = before_cursor.rfind("((") {
            // Check there's no )) between (( and cursor
            !before_cursor[link_start..].contains("))")
        } else {
            false
        };

        if !in_link {
            return Ok(None);
        }

        // Check if cursor is right after a colon
        if !before_cursor.ends_with(':') {
            return Ok(None);
        }

        // Get the text between (( and : (this is the link display text, if any)
        let link_start = before_cursor.rfind("((").unwrap();
        let link_text = before_cursor[link_start + 2..before_cursor.len() - 1].trim();

        // Collect all IDs from the document as completion items
        let mut items: Vec<CompletionItem> = Vec::new();

        for indexed_id in doc.all_ids() {
            let id = &indexed_id.id;
            let block_type = &indexed_id.block_type;

            // If link text is empty, we'll insert the ID as the link text too
            let insert_text = if link_text.is_empty() {
                // Replace the entire link with display text and target
                format!("{id} :{id}")
            } else {
                // Just insert the ID
                id.clone()
            };

            // Calculate the range to replace
            let (start_char, new_text) = if link_text.is_empty() {
                // Replace from after (( to include the colon we just typed
                (link_start + 2, insert_text)
            } else {
                // Just replace the colon with :id
                (char_pos - 1, format!(":{id}"))
            };

            items.push(CompletionItem {
                label: id.clone(),
                kind: Some(CompletionItemKind::REFERENCE),
                detail: Some(block_type.to_string()),
                insert_text: Some(new_text.clone()),
                text_edit: Some(CompletionTextEdit::Edit(TextEdit {
                    range: Range {
                        start: tower_lsp::lsp_types::Position::new(
                            position.line,
                            start_char as u32,
                        ),
                        end: position,
                    },
                    new_text,
                })),
                ..Default::default()
            });
        }

        if items.is_empty() {
            Ok(None)
        } else {
            Ok(Some(CompletionResponse::Array(items)))
        }
    }

    async fn goto_definition(
        &self,
        params: GotoDefinitionParams,
    ) -> Result<Option<GotoDefinitionResponse>> {
        let uri = params
            .text_document_position_params
            .text_document
            .uri
            .to_string();
        let position = Self::to_internal_position(&params.text_document_position_params.position);

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

        // Check if there's a link at this position
        let Some(link) = doc.link_at_position(position) else {
            return Ok(None);
        };

        // Resolve the link
        match doc.resolve_link(link) {
            ResolvedTarget::SameDocument(range) => {
                let lsp_range = Self::to_lsp_range(&range);
                Ok(Some(GotoDefinitionResponse::Scalar(Location::new(
                    params.text_document_position_params.text_document.uri,
                    lsp_range,
                ))))
            }
            ResolvedTarget::OtherDocument { document, id } => {
                // Try to resolve the external document
                if let Some(target_uri) = self.resolve_external_document(&document) {
                    // Read the external document to find the ID
                    if let Some(id) = id {
                        if let Ok(content) =
                            std::fs::read_to_string(target_uri.to_file_path().unwrap_or_default())
                        {
                            let external_index = DocumentIndex::from_content(&content);
                            if let Some(indexed_id) = external_index.get_id(&id) {
                                return Ok(Some(GotoDefinitionResponse::Scalar(Location::new(
                                    target_uri,
                                    Self::to_lsp_range(&indexed_id.range),
                                ))));
                            }
                        }
                    }
                    // If no ID or ID not found, just go to the start of the document
                    Ok(Some(GotoDefinitionResponse::Scalar(Location::new(
                        target_uri,
                        tower_lsp::lsp_types::Range::new(
                            tower_lsp::lsp_types::Position::new(0, 0),
                            tower_lsp::lsp_types::Position::new(0, 0),
                        ),
                    ))))
                } else {
                    Ok(None)
                }
            }
            ResolvedTarget::Uri(uri) => {
                // For URIs, we could potentially open them in a browser
                // For now, return None as we can't navigate to URLs in the editor
                self.client
                    .log_message(MessageType::INFO, format!("Link to URI: {uri}"))
                    .await;
                Ok(None)
            }
            ResolvedTarget::NotFound => Ok(None),
        }
    }

    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
        let uri = params
            .text_document_position_params
            .text_document
            .uri
            .to_string();
        let position = Self::to_internal_position(&params.text_document_position_params.position);

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

        // Check if there's a link at this position
        if let Some(link) = doc.link_at_position(position) {
            let hover_text = match &link.target_type {
                LinkTargetType::LocalId => {
                    let target = link.target.as_deref().unwrap_or("(none)");
                    if let Some(indexed_id) = doc.get_id(target) {
                        format!(
                            "**Link to:** `{}` ({})\n\nDefined at line {}",
                            target,
                            indexed_id.block_type,
                            indexed_id.range.start.line + 1
                        )
                    } else {
                        format!("**Link to:** `{}` (not found)", target)
                    }
                }
                LinkTargetType::ExternalId { document, id } => {
                    format!("**External link:** `{}.{}`", document, id)
                }
                LinkTargetType::Uri => {
                    let uri = link.target.as_deref().unwrap_or("(none)");
                    format!("**URL:** {}", uri)
                }
                LinkTargetType::Footnote => {
                    let marker = link.target.as_deref().unwrap_or("(none)");
                    format!("**Footnote:** {}", marker)
                }
            };

            return Ok(Some(Hover {
                contents: HoverContents::Markup(MarkupContent {
                    kind: MarkupKind::Markdown,
                    value: hover_text,
                }),
                range: Some(Self::to_lsp_range(&link.range)),
            }));
        }

        // Check if we're hovering over an ID definition
        for indexed_id in doc.all_ids() {
            if indexed_id.range.contains(position) {
                let hover_text = format!(
                    "**ID:** `{}`\n\n**Type:** {}",
                    indexed_id.id, indexed_id.block_type
                );
                return Ok(Some(Hover {
                    contents: HoverContents::Markup(MarkupContent {
                        kind: MarkupKind::Markdown,
                        value: hover_text,
                    }),
                    range: Some(Self::to_lsp_range(&indexed_id.range)),
                }));
            }
        }

        Ok(None)
    }

    async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
        let uri = params.text_document.uri.to_string();

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

        let mut links = Vec::new();

        // Add document links for all indexed links
        for (index, indexed_link) in doc.all_links().iter().enumerate() {
            if let Some(target) = &indexed_link.target {
                let tooltip = match &indexed_link.target_type {
                    LinkTargetType::LocalId => format!("Go to: {target}"),
                    LinkTargetType::ExternalId { document, id } => {
                        format!("Go to: {document}.{id}")
                    }
                    LinkTargetType::Uri => format!("Open: {target}"),
                    LinkTargetType::Footnote => format!("Footnote: {target}"),
                };

                // Store URI and link index in data for resolution
                let data = serde_json::json!({
                    "uri": uri,
                    "index": index
                });

                links.push(DocumentLink {
                    range: Self::to_lsp_range(&indexed_link.range),
                    target: None, // Resolved later
                    tooltip: Some(tooltip),
                    data: Some(data),
                });
            }
        }

        Ok(Some(links))
    }

    async fn document_link_resolve(&self, mut link: DocumentLink) -> Result<DocumentLink> {
        // Extract stored data
        let Some(data) = &link.data else {
            return Ok(link);
        };

        let uri = data.get("uri").and_then(|v| v.as_str());
        let index = data
            .get("index")
            .and_then(|v| v.as_u64())
            .map(|i| i as usize);

        let (Some(uri), Some(index)) = (uri, index) else {
            return Ok(link);
        };

        let Some(doc) = self.get_document(uri) else {
            return Ok(link);
        };

        let all_links = doc.all_links();
        let Some(indexed_link) = all_links.get(index) else {
            return Ok(link);
        };

        // Resolve the target based on link type
        match &indexed_link.target_type {
            LinkTargetType::LocalId => {
                if let Some(target_id) = &indexed_link.target {
                    if doc.get_id(target_id).is_some() {
                        // For local IDs, use the same document URI with a fragment
                        if let Ok(mut target_url) = Url::parse(uri) {
                            target_url.set_fragment(Some(target_id));
                            link.target = Some(target_url);
                        }
                    }
                }
            }
            LinkTargetType::ExternalId { document, id } => {
                if let Some(target_uri) = self.resolve_external_document(document) {
                    let mut target_url = target_uri;
                    if !id.is_empty() {
                        target_url.set_fragment(Some(id));
                    }
                    link.target = Some(target_url);
                }
            }
            LinkTargetType::Uri => {
                if let Some(uri_str) = &indexed_link.target {
                    if let Ok(url) = Url::parse(uri_str) {
                        link.target = Some(url);
                    }
                }
            }
            LinkTargetType::Footnote => {
                // Footnotes don't have external targets
            }
        }

        Ok(link)
    }

    async fn semantic_tokens_full(
        &self,
        params: SemanticTokensParams,
    ) -> Result<Option<SemanticTokensResult>> {
        let uri = params.text_document.uri.to_string();

        let Some(doc) = self.get_document(&uri) else {
            return Ok(None);
        };

        let content = doc.content();

        // Collect tokens with absolute positions first
        struct AbsoluteToken {
            line: u32,
            start: u32,
            length: u32,
            token_type: u32,
        }

        let mut abs_tokens: Vec<AbsoluteToken> = Vec::new();

        for (line_num, line) in content.lines().enumerate() {
            let line_num = line_num as u32;

            // Only process meta-lines (starting with .)
            if !line.starts_with('.') {
                continue;
            }

            // Try to parse the meta-line
            if let Some(meta) = MetaLine::parse(line, line_num as usize) {
                let mut char_offset: u32 = 0;

                // Token for dots (nesting level)
                let dots_len = meta.level as u32;
                if dots_len > 0 {
                    abs_tokens.push(AbsoluteToken {
                        line: line_num,
                        start: 0,
                        length: dots_len,
                        token_type: 0, // NAMESPACE
                    });
                    char_offset = dots_len;
                }

                // Token for block type
                let type_name = &meta.type_name;
                if !type_name.is_empty() {
                    let token_type = if type_name == "#" || type_name == "/" {
                        8 // KEYWORD
                    } else {
                        1 // CLASS
                    };

                    let type_len = type_name.chars().count() as u32;
                    abs_tokens.push(AbsoluteToken {
                        line: line_num,
                        start: char_offset,
                        length: type_len,
                        token_type,
                    });
                    char_offset += type_len;
                }

                // Process arguments
                for arg in &meta.arguments.args {
                    if let Some(arg_start) =
                        Self::find_char_offset_from(line, &arg.content, char_offset)
                    {
                        let token_type = if arg.quoted { 2 } else { 3 };
                        let length = if arg.quoted {
                            arg.content.chars().count() as u32 + 2
                        } else {
                            arg.content.chars().count() as u32
                        };

                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: if arg.quoted {
                                arg_start.saturating_sub(1)
                            } else {
                                arg_start
                            },
                            length,
                            token_type,
                        });
                        char_offset = arg_start + arg.content.chars().count() as u32;
                        if arg.quoted {
                            char_offset += 1; // closing quote
                        }
                    }
                }

                // Process kwargs
                for (key, value) in &meta.arguments.kwargs {
                    if let Some(key_start) =
                        Self::find_char_offset_from(line, &key.content, char_offset)
                    {
                        let key_len = key.content.chars().count() as u32;
                        let actual_key_start = if key.quoted {
                            key_start.saturating_sub(1)
                        } else {
                            key_start
                        };
                        let actual_key_len = if key.quoted { key_len + 2 } else { key_len };

                        // Key token
                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: actual_key_start,
                            length: actual_key_len,
                            token_type: 4, // PARAMETER
                        });

                        // Equals token
                        let eq_pos = actual_key_start + actual_key_len;
                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: eq_pos,
                            length: 1,
                            token_type: 5, // OPERATOR
                        });

                        // Value token
                        let val_start = eq_pos + 1;
                        let token_type = if value.quoted { 2 } else { 3 };
                        let val_len = if value.quoted {
                            value.content.chars().count() as u32 + 2
                        } else {
                            value.content.chars().count() as u32
                        };

                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: val_start,
                            length: val_len,
                            token_type,
                        });

                        char_offset = val_start + val_len;
                    }
                }

                // Token for ID if present
                if !meta.id.value.is_empty() {
                    let id_pattern = format!(":{}", meta.id.value);
                    if let Some(byte_pos) = line.rfind(&id_pattern) {
                        let id_start = Self::byte_offset_to_char_offset(line, byte_pos);
                        let id_len = id_pattern.chars().count() as u32;

                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: id_start,
                            length: id_len,
                            token_type: 6, // DECORATOR
                        });
                    }
                }

                // Token for inline text separator if present
                if !meta.inline_text.content.is_empty() {
                    if let Some(sep_char_pos) = Self::find_char_offset_from(line, " .. ", dots_len)
                    {
                        let sep_start = sep_char_pos + 1; // +1 for space before ..
                        abs_tokens.push(AbsoluteToken {
                            line: line_num,
                            start: sep_start,
                            length: 2,
                            token_type: 7, // COMMENT
                        });
                    }
                }
            }
        }

        // Sort tokens by position (line, then start)
        abs_tokens.sort_by(|a, b| a.line.cmp(&b.line).then(a.start.cmp(&b.start)));

        // Convert to relative positions
        let mut tokens: Vec<SemanticToken> = Vec::new();
        let mut prev_line: u32 = 0;
        let mut prev_char: u32 = 0;

        for t in abs_tokens {
            let delta_line = t.line - prev_line;
            let delta_start = if delta_line == 0 {
                t.start.saturating_sub(prev_char)
            } else {
                t.start
            };

            tokens.push(SemanticToken {
                delta_line,
                delta_start,
                length: t.length,
                token_type: t.token_type,
                token_modifiers_bitset: 0,
            });

            prev_line = t.line;
            prev_char = t.start;
        }

        Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
            result_id: None,
            data: tokens,
        })))
    }
}

/// Start the LSP server on stdin/stdout.
pub async fn start_lsp_server() {
    let stdin = tokio::io::stdin();
    let stdout = tokio::io::stdout();

    let (service, socket) = LspService::new(WasotLanguageServer::new);
    Server::new(stdin, stdout, socket).serve(service).await;
}

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

    // ==================== Unit Tests for Helper Functions ====================

    #[test]
    fn test_to_internal_position() {
        let lsp_pos = tower_lsp::lsp_types::Position::new(10, 5);
        let internal_pos = WasotLanguageServer::to_internal_position(&lsp_pos);
        assert_eq!(internal_pos.line, 10);
        assert_eq!(internal_pos.character, 5);
    }

    #[test]
    fn test_to_lsp_position() {
        let internal_pos = Position::new(10, 5);
        let lsp_pos = WasotLanguageServer::to_lsp_position(&internal_pos);
        assert_eq!(lsp_pos.line, 10);
        assert_eq!(lsp_pos.character, 5);
    }

    #[test]
    fn test_to_lsp_range() {
        let internal_range =
            super::super::document::Range::new(Position::new(1, 5), Position::new(1, 15));
        let lsp_range = WasotLanguageServer::to_lsp_range(&internal_range);
        assert_eq!(lsp_range.start.line, 1);
        assert_eq!(lsp_range.start.character, 5);
        assert_eq!(lsp_range.end.line, 1);
        assert_eq!(lsp_range.end.character, 15);
    }

    // ==================== Integration-like Tests ====================

    #[tokio::test]
    async fn test_document_indexing_integration() {
        // Create a mock client (we can't test the full server easily without a mock)
        // But we can test the document indexing and resolution logic
        let content = r#".document :main-doc
.section :intro
Introduction text with ((link to other :conclusion))

.section :conclusion
This is the conclusion with ((back link :intro))
"#;

        let index = DocumentIndex::from_content(content);

        // Check IDs are indexed
        assert!(index.get_id("main-doc").is_some());
        assert!(index.get_id("intro").is_some());
        assert!(index.get_id("conclusion").is_some());

        // Check links are indexed
        let link1 = index.link_at_position(Position::new(2, 30)).unwrap();
        assert_eq!(link1.target, Some("conclusion".to_string()));

        let link2 = index.link_at_position(Position::new(5, 35)).unwrap();
        assert_eq!(link2.target, Some("intro".to_string()));

        // Check link resolution
        let resolved1 = index.resolve_link(link1);
        match resolved1 {
            ResolvedTarget::SameDocument(range) => {
                assert_eq!(range.start.line, 4); // .section :conclusion is on line 4
            }
            _ => panic!("Expected SameDocument resolution"),
        }

        let resolved2 = index.resolve_link(link2);
        match resolved2 {
            ResolvedTarget::SameDocument(range) => {
                assert_eq!(range.start.line, 1); // .section :intro is on line 1
            }
            _ => panic!("Expected SameDocument resolution"),
        }
    }

    #[tokio::test]
    async fn test_external_link_resolution() {
        let content = "See ((other doc :readme.getting-started))";
        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::OtherDocument { document, id } => {
                assert_eq!(document, "readme");
                assert_eq!(id, Some("getting-started".to_string()));
            }
            _ => panic!("Expected OtherDocument resolution"),
        }
    }

    #[tokio::test]
    async fn test_uri_link_handling() {
        let content = "Visit ((our website :https://wasot.dev))";
        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://wasot.dev");
            }
            _ => panic!("Expected Uri resolution"),
        }
    }

    #[test]
    fn test_complex_document_with_nested_blocks() {
        let content = r#".article :main
.. Title of Article

..section :intro
Introduction paragraph.

...paragraph :para1
First nested paragraph with ((reference :para2)).

...paragraph :para2
Second nested paragraph.

..section :conclusion
Conclusion with ((back to intro :intro)).
"#;

        let index = DocumentIndex::from_content(content);

        // Check all IDs
        assert!(index.get_id("main").is_some());
        assert!(index.get_id("intro").is_some());
        assert!(index.get_id("para1").is_some());
        assert!(index.get_id("para2").is_some());
        assert!(index.get_id("conclusion").is_some());

        // Check cross-references resolve correctly
        // Link from para1 to para2 (line 7: "First nested paragraph with ((reference :para2)).")
        let link_to_para2 = index.link_at_position(Position::new(7, 30)).unwrap();
        assert_eq!(link_to_para2.target, Some("para2".to_string()));

        let resolved = index.resolve_link(link_to_para2);
        match resolved {
            ResolvedTarget::SameDocument(range) => {
                assert_eq!(range.start.line, 9); // ...paragraph :para2 is on line 9
            }
            _ => panic!("Expected SameDocument resolution"),
        }
    }
}
