//! Rename refactoring provider
//!
//! Provides semantic, workspace-wide symbol renaming using NameTable

use std::collections::HashMap;

use tower_lsp::lsp_types::{
    Position,
    Range,
    RenameParams,
    TextEdit,
    Url,
    WorkspaceEdit,
};

use super::document::Document;
use crate::{
    position::PositionTracker,
    resolve::find_all_references,
};

/// Perform a workspace-wide semantic rename operation
pub fn get_rename_edits(
    documents: &HashMap<Url, Document>,
    params: &RenameParams,
    uri: &Url,
) -> Option<WorkspaceEdit> {
    let doc = documents.get(uri)?;
    let position = params.text_document_position.position;

    // Convert LSP position to byte offset
    let offset = position_to_offset(doc, position.line as usize, position.character as usize)?;

    // Get the word at the cursor
    let old_name = doc.word_at_offset(offset)?;

    // Look up the symbol in the name table - this validates it's a real symbol
    let entry = doc.name_table.resolve_name(&old_name)?;

    // Get the symbol's kind for semantic matching
    let symbol_kind = entry.kind;

    // Collect all ASTs and build file_index -> URL mapping
    let mut file_asts = Vec::new();
    let mut file_url_map: HashMap<usize, Url> = HashMap::new();
    let mut url_file_map: HashMap<Url, usize> = HashMap::new();
    let mut url_positions_map: HashMap<Url, PositionTracker> = HashMap::new();

    for (doc_uri, document) in documents {
        if let Some(ref ast) = document.ast {
            let file_index = file_asts.len();
            file_asts.push(ast.clone());
            file_url_map.insert(file_index, doc_uri.clone());
            url_file_map.insert(doc_uri.clone(), file_index);
            url_positions_map.insert(doc_uri.clone(), document.positions.clone());
        }
    }

    // Find all semantic references using the language layer
    let references = find_all_references(&file_asts, &old_name, symbol_kind);

    // Convert references to TextEdits grouped by file
    let mut all_changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();

    for reference in references {
        // Get the URL for this file
        if let Some(url) = file_url_map.get(&reference.file_index) {
            if let Some(mut positions) = url_positions_map.get(url).cloned() {
                // Convert byte offsets to line/col positions
                let (start_line, start_col) = positions.offset_to_position(reference.span.start);
                let (end_line, end_col) = positions.offset_to_position(reference.span.end);

                let edit = TextEdit {
                    range: Range {
                        start: Position {
                            line: start_line as u32,
                            character: start_col as u32,
                        },
                        end: Position {
                            line: end_line as u32,
                            character: end_col as u32,
                        },
                    },
                    new_text: params.new_name.clone(),
                };

                all_changes.entry(url.clone()).or_default().push(edit);
            }
        }
    }

    if all_changes.is_empty() {
        return None;
    }

    Some(WorkspaceEdit {
        changes: Some(all_changes),
        document_changes: None,
        change_annotations: None,
    })
}

/// Prepare rename - check if rename is valid at this position
pub fn prepare_rename(doc: &Document, position: Position) -> Option<Range> {
    // Convert LSP position to byte offset
    let offset = position_to_offset(doc, position.line as usize, position.character as usize)?;

    // Get the word at the cursor
    let word = doc.word_at_offset(offset)?;

    // Check if this is a valid symbol in the name table
    let entry = doc.name_table.resolve_name(&word)?;

    // Return the range of the symbol at its definition
    let mut positions = doc.positions.clone();
    let (start_line, start_col) = positions.offset_to_position(entry.span.start);
    let (end_line, end_col) = positions.offset_to_position(entry.span.end);

    Some(Range {
        start: Position {
            line: start_line as u32,
            character: start_col as u32,
        },
        end: Position {
            line: end_line as u32,
            character: end_col as u32,
        },
    })
}

/// Convert LSP position to byte offset
fn position_to_offset(doc: &Document, line: usize, character: usize) -> Option<usize> {
    let line_start = doc.positions.line_offset(line)?;
    Some(line_start + character)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lsp::document::Document;

    fn make_documents(files: Vec<(&str, &str)>) -> HashMap<Url, Document> {
        files
            .into_iter()
            .map(|(uri_str, content)| {
                let uri = Url::parse(uri_str).unwrap();
                let doc = Document::new(content.to_string());
                (uri, doc)
            })
            .collect()
    }

    #[test]
    fn test_rename_character_single_file() {
        let source = r#"
character Alice {}
character Bob { friend: Alice }
"#;
        let uri = Url::parse("file:///test.sb").unwrap();
        let documents = make_documents(vec![("file:///test.sb", source)]);

        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 11,
                },
            },
            new_name: "Alicia".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_some());

        let workspace_edit = result.unwrap();
        let changes = workspace_edit.changes.unwrap();
        let edits = changes.get(&uri).unwrap();

        // Should find 2 occurrences: definition and reference
        assert_eq!(edits.len(), 2);
        assert!(edits.iter().all(|e| e.new_text == "Alicia"));
    }

    #[test]
    fn test_rename_across_multiple_files() {
        let file1 = "character Alice {}";
        let file2 = "character Bob { friend: Alice }";
        let file3 = "character Charlie { mentor: Alice }";

        let documents = make_documents(vec![
            ("file:///file1.sb", file1),
            ("file:///file2.sb", file2),
            ("file:///file3.sb", file3),
        ]);

        let uri = Url::parse("file:///file1.sb").unwrap();
        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 11,
                },
            },
            new_name: "Alicia".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_some());

        let changes = result.unwrap().changes.unwrap();

        // Should have edits in all 3 files
        assert_eq!(changes.len(), 3);

        // File 1: definition
        assert_eq!(
            changes
                .get(&Url::parse("file:///file1.sb").unwrap())
                .unwrap()
                .len(),
            1
        );
        // File 2: one reference
        assert_eq!(
            changes
                .get(&Url::parse("file:///file2.sb").unwrap())
                .unwrap()
                .len(),
            1
        );
        // File 3: one reference
        assert_eq!(
            changes
                .get(&Url::parse("file:///file3.sb").unwrap())
                .unwrap()
                .len(),
            1
        );
    }

    #[test]
    fn test_rename_template() {
        let source = r#"
template Person {}
character Alice from Person {}
"#;
        let uri = Url::parse("file:///test.sb").unwrap();
        let documents = make_documents(vec![("file:///test.sb", source)]);

        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 10,
                },
            },
            new_name: "Human".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_some());

        let edits = result.unwrap().changes.unwrap().get(&uri).unwrap().clone();
        assert_eq!(edits.len(), 2); // Definition + usage
    }

    #[test]
    fn test_rename_not_found() {
        let source = "character Alice {}";
        let uri = Url::parse("file:///test.sb").unwrap();
        let documents = make_documents(vec![("file:///test.sb", source)]);

        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 0,
                }, // On "character" keyword
            },
            new_name: "NewName".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_none());
    }

    #[test]
    fn test_prepare_rename_valid() {
        let source = "character Alice {}";
        let doc = Document::new(source.to_string());

        let result = prepare_rename(
            &doc,
            Position {
                line: 0,
                character: 11,
            },
        );
        assert!(result.is_some());

        let range = result.unwrap();
        assert_eq!(range.start.line, 0);
        assert_eq!(range.end.line, 0);
    }

    #[test]
    fn test_prepare_rename_invalid() {
        let source = "character Alice {}";
        let doc = Document::new(source.to_string());

        // Position on keyword "character"
        let result = prepare_rename(
            &doc,
            Position {
                line: 0,
                character: 0,
            },
        );
        assert!(result.is_none());
    }

    #[test]
    fn test_rename_respects_symbol_kind() {
        // Test that we don't rename unrelated symbols with the same name in different
        // contexts
        let source = r#"
character Alice {}
template Person {}
character Bob { friend: Alice }
character Charlie from Person {}
"#;
        let uri = Url::parse("file:///test.sb").unwrap();
        let documents = make_documents(vec![("file:///test.sb", source)]);

        // Try to rename the character Alice
        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 11,
                },
            },
            new_name: "Alicia".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_some());

        let edits = result.unwrap().changes.unwrap().get(&uri).unwrap().clone();

        // Should rename the character definition and its field reference, but not the
        // template
        assert_eq!(edits.len(), 2);
    }

    #[test]
    fn test_rename_with_word_boundaries() {
        let source = r#"
character Alice {}
character AliceJr {}
character NotAlice {}
"#;
        let uri = Url::parse("file:///test.sb").unwrap();
        let documents = make_documents(vec![("file:///test.sb", source)]);

        let params = RenameParams {
            text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
                text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 11,
                },
            },
            new_name: "Alicia".to_string(),
            work_done_progress_params: Default::default(),
        };

        let result = get_rename_edits(&documents, &params, &uri);
        assert!(result.is_some());

        let edits = result.unwrap().changes.unwrap().get(&uri).unwrap().clone();

        // Should only rename exact "Alice", not "AliceJr" or "NotAlice"
        assert_eq!(edits.len(), 1);
    }
}
