use super::code_actions::{code_actions_from_response, select_code_action};
use super::parsers::{
    diagnostics_from_response, references_from_response, rename_plan_from_response,
    resolved_targets_from_response, workspace_symbols_from_response,
};
use super::position::utf16_character_for_byte_column;
use super::session::LspSession;
use super::type_hierarchy::{
    method_suffix_to_hierarchy, type_hierarchy_node_from_item, type_hierarchy_to_map,
};
use super::types::{
    LspCodeActionRefactorPlan, LspCodeActionRefactorResult, LspCodeActionRequest, LspDiagnostic,
    LspDiagnosticRequest, LspReference, LspRenamePlan, LspRenamePlanRequest, LspRenameRequest,
    LspRequest, LspResolveTargetRequest, LspResolvedTarget, LspTypeHierarchyNode,
    LspTypeHierarchyRequest, LspWorkspaceEditTransaction, LspWorkspaceSymbol,
    LspWorkspaceSymbolRequest,
};
use super::workspace_edit::{
    apply_workspace_edit_transaction, workspace_edit_transaction_from_edit,
    workspace_edit_transaction_from_response,
};
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use std::collections::HashMap;

impl LspSession {
    pub(super) fn find_references(&mut self, request: &LspRequest) -> Result<Vec<LspReference>> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, source) = self.prepare_document(&absolute_path)?;
        let character = utf16_character_for_byte_column(&source, request.line, request.column);

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/references",
            json!({
                "textDocument":{"uri":uri_string},
                "position":{"line":request.line.saturating_sub(1),"character":character},
                "context":{"includeDeclaration":true}
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        references_from_response(&self.project, response, request.max_results)
    }

    pub(super) fn get_diagnostics(
        &mut self,
        request: &LspDiagnosticRequest,
    ) -> Result<Vec<LspDiagnostic>> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, _source) = self.prepare_document(&absolute_path)?;

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/diagnostic",
            json!({
                "textDocument":{"uri":uri_string}
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        diagnostics_from_response(&self.project, response, request.max_results)
    }

    pub(super) fn search_workspace_symbols(
        &mut self,
        request: &LspWorkspaceSymbolRequest,
    ) -> Result<Vec<LspWorkspaceSymbol>> {
        let id = self.next_id();
        self.send_request(
            id,
            "workspace/symbol",
            json!({
                "query": request.query
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        workspace_symbols_from_response(&self.project, response, request.max_results)
    }

    pub(super) fn get_type_hierarchy(
        &mut self,
        request: &LspTypeHierarchyRequest,
    ) -> Result<HashMap<String, Value>> {
        let workspace_symbols = self.search_workspace_symbols(&LspWorkspaceSymbolRequest {
            command: request.command.clone(),
            args: request.args.clone(),
            query: request.query.clone(),
            max_results: 20,
        })?;
        let seed = workspace_symbols
            .into_iter()
            .find(|symbol| match &request.relative_path {
                Some(path) => &symbol.file_path == path,
                None => true,
            })
            .with_context(|| format!("No workspace symbol found for '{}'", request.query))?;

        let absolute_path = self.project.resolve(&seed.file_path)?;
        let (uri_string, _source) = self.prepare_document(&absolute_path)?;

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/prepareTypeHierarchy",
            json!({
                "textDocument":{"uri":uri_string},
                "position":{"line":seed.line.saturating_sub(1),"character":seed.column.saturating_sub(1)}
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        let items = response
            .get("result")
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default();
        let root_item = items
            .into_iter()
            .next()
            .context("LSP prepareTypeHierarchy returned no items")?;

        let root = self.build_type_hierarchy_node(
            &root_item,
            request.depth,
            request.hierarchy_type.as_str(),
        )?;
        Ok(type_hierarchy_to_map(&root))
    }

    pub(super) fn resolve_symbol_target(
        &mut self,
        request: &LspResolveTargetRequest,
    ) -> Result<Vec<LspResolvedTarget>> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, source) = self.prepare_document(&absolute_path)?;
        let character = utf16_character_for_byte_column(&source, request.line, request.column);
        let method = match request.target.as_str() {
            "declaration" => "textDocument/declaration",
            "definition" => "textDocument/definition",
            "implementation" => "textDocument/implementation",
            "type_definition" => "textDocument/typeDefinition",
            other => bail!("unsupported LSP target: {other}"),
        };

        let id = self.next_id();
        self.send_request(
            id,
            method,
            json!({
                "textDocument":{"uri":uri_string},
                "position":{"line":request.line.saturating_sub(1),"character":character}
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        resolved_targets_from_response(
            &self.project,
            response,
            &request.target,
            method,
            request.max_results,
        )
    }

    pub(super) fn get_rename_plan(
        &mut self,
        request: &LspRenamePlanRequest,
    ) -> Result<LspRenamePlan> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, source) = self.prepare_document(&absolute_path)?;
        let character = utf16_character_for_byte_column(&source, request.line, request.column);

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/prepareRename",
            json!({
                "textDocument":{"uri":uri_string},
                "position":{"line":request.line.saturating_sub(1),"character":character}
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        rename_plan_from_response(
            &self.project,
            &request.file_path,
            &source,
            response,
            request.new_name.clone(),
        )
    }

    pub(super) fn rename_symbol(
        &mut self,
        request: &LspRenameRequest,
    ) -> Result<crate::rename::RenameResult> {
        let transaction = self.rename_symbol_transaction(request)?;
        let edits = transaction.edits.clone();
        let modified_files = transaction.modified_files;
        let total_replacements = transaction.edit_count;

        if !request.dry_run {
            apply_workspace_edit_transaction(&self.project, &transaction)?;
        }

        Ok(crate::rename::RenameResult {
            success: true,
            message: format!(
                "{} {} LSP replacement(s) in {} file(s)",
                if request.dry_run {
                    "Would make"
                } else {
                    "Made"
                },
                total_replacements,
                modified_files
            ),
            modified_files,
            total_replacements,
            edits,
        })
    }

    pub(super) fn rename_symbol_transaction(
        &mut self,
        request: &LspRenameRequest,
    ) -> Result<LspWorkspaceEditTransaction> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, source) = self.prepare_document(&absolute_path)?;
        let character = utf16_character_for_byte_column(&source, request.line, request.column);
        let _plan = self.get_rename_plan(&LspRenamePlanRequest {
            command: request.command.clone(),
            args: request.args.clone(),
            file_path: request.file_path.clone(),
            line: request.line,
            column: request.column,
            new_name: Some(request.new_name.clone()),
        })?;

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/rename",
            json!({
                "textDocument":{"uri":uri_string},
                "position":{"line":request.line.saturating_sub(1),"character":character},
                "newName": request.new_name.clone(),
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        workspace_edit_transaction_from_response(&self.project, response)
    }

    pub(super) fn code_action_refactor(
        &mut self,
        request: &LspCodeActionRequest,
    ) -> Result<LspCodeActionRefactorResult> {
        let plan = self.code_action_refactor_plan(request)?;
        if !request.dry_run {
            apply_workspace_edit_transaction(&self.project, &plan.transaction)?;
        }

        Ok(LspCodeActionRefactorResult {
            success: true,
            message: format!(
                "{} {} LSP codeAction edit(s) in {} file(s)",
                if request.dry_run {
                    "Would apply"
                } else {
                    "Applied"
                },
                plan.transaction.edit_count,
                plan.transaction.modified_files
            ),
            operation: plan.operation,
            action_title: plan.action_title,
            action_kind: plan.action_kind,
            resolved_via: plan.resolved_via,
            applied: !request.dry_run,
            transaction: plan.transaction,
        })
    }

    pub(super) fn code_action_refactor_plan(
        &mut self,
        request: &LspCodeActionRequest,
    ) -> Result<LspCodeActionRefactorPlan> {
        let absolute_path = self.project.resolve(&request.file_path)?;
        let (uri_string, source) = self.prepare_document(&absolute_path)?;
        let start_character =
            utf16_character_for_byte_column(&source, request.start_line, request.start_column);
        let end_character =
            utf16_character_for_byte_column(&source, request.end_line, request.end_column);

        let id = self.next_id();
        self.send_request(
            id,
            "textDocument/codeAction",
            json!({
                "textDocument":{"uri":uri_string},
                "range":{
                    "start":{
                        "line":request.start_line.saturating_sub(1),
                        "character":start_character
                    },
                    "end":{
                        "line":request.end_line.saturating_sub(1),
                        "character":end_character
                    }
                },
                "context":{
                    "diagnostics":[],
                    "only": request.only
                }
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        let actions = code_actions_from_response(response, &request.only)?;
        let action = select_code_action(&actions, request.action_id.as_deref())?;
        if let Some(reason) = &action.disabled_reason {
            bail!("unsupported_semantic_refactor: selected LSP codeAction is disabled: {reason}");
        }

        let (edit, resolved_via) = if let Some(edit) = action.edit.clone() {
            (edit, "textDocument/codeAction".to_owned())
        } else {
            let id = self.next_id();
            self.send_request(id, "codeAction/resolve", action.raw.clone())?;
            let response = self.read_response_for_id(id)?;
            let Some(result) = response.get("result") else {
                bail!("unsupported_semantic_refactor: codeAction/resolve returned no result");
            };
            if let Some(edit) = result.get("edit").cloned() {
                (edit, "codeAction/resolve".to_owned())
            } else if action.command.is_some() || result.get("command").is_some() {
                bail!(
                    "unsupported_semantic_refactor: LSP codeAction returned command without inspectable WorkspaceEdit"
                );
            } else {
                bail!("unsupported_semantic_refactor: LSP codeAction returned no WorkspaceEdit");
            }
        };

        let transaction = workspace_edit_transaction_from_edit(&self.project, &edit)?;
        if transaction.edit_count == 0 && transaction.resource_ops.is_empty() {
            bail!("unsupported_semantic_refactor: LSP codeAction WorkspaceEdit is empty");
        }

        Ok(LspCodeActionRefactorPlan {
            operation: request.operation.clone(),
            action_title: action.title,
            action_kind: action.kind,
            resolved_via,
            transaction,
        })
    }

    fn build_type_hierarchy_node(
        &mut self,
        item: &Value,
        depth: usize,
        hierarchy_type: &str,
    ) -> Result<LspTypeHierarchyNode> {
        let mut node = type_hierarchy_node_from_item(item)?;

        if depth == 0 {
            return Ok(node);
        }

        let next_depth = depth.saturating_sub(1);
        if hierarchy_type == "super" || hierarchy_type == "both" {
            node.supertypes = self.fetch_type_hierarchy_branch(item, "supertypes", next_depth)?;
        }
        if hierarchy_type == "sub" || hierarchy_type == "both" {
            node.subtypes = self.fetch_type_hierarchy_branch(item, "subtypes", next_depth)?;
        }
        Ok(node)
    }

    fn fetch_type_hierarchy_branch(
        &mut self,
        item: &Value,
        method_suffix: &str,
        depth: usize,
    ) -> Result<Vec<LspTypeHierarchyNode>> {
        let id = self.next_id();
        self.send_request(
            id,
            &format!("typeHierarchy/{method_suffix}"),
            json!({
                "item": item
            }),
        )?;
        let response = self.read_response_for_id(id)?;
        let Some(items) = response.get("result").and_then(Value::as_array) else {
            return Ok(Vec::new());
        };

        let mut nodes = Vec::new();
        for child in items {
            nodes.push(self.build_type_hierarchy_node(
                child,
                depth,
                method_suffix_to_hierarchy(method_suffix),
            )?);
        }
        Ok(nodes)
    }
}
