use std::borrow::Cow;
use std::time::Instant;

use lsp_types::{
    CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList,
    CompletionParams, CompletionRequest, CompletionResponse, Documentation, InsertTextFormat,
    TextEdit, Uri,
};
use ruff_source_file::OneIndexed;
use ruff_text_size::Ranged;
use ty_ide::{CompletionCapabilities, CompletionInsertTextFormat, CompletionKind, completion};
use ty_project::ProjectDatabase;

use crate::document::{PositionExt, ToRangeExt};
use crate::server::api::traits::{
    BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::DocumentSnapshot;
use crate::session::client::Client;

pub(crate) struct CompletionRequestHandler;

impl RequestHandler for CompletionRequestHandler {
    type RequestType = CompletionRequest;
}

impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
    fn document_uri(params: &CompletionParams) -> Cow<'_, Uri> {
        Cow::Borrowed(&params.text_document_position_params.text_document.uri)
    }

    fn run_with_snapshot(
        db: &ProjectDatabase,
        snapshot: &DocumentSnapshot,
        _client: &Client,
        params: CompletionParams,
    ) -> crate::server::Result<Option<CompletionResponse>> {
        let start = Instant::now();

        if snapshot
            .workspace_settings()
            .is_language_services_disabled()
        {
            return Ok(None);
        }

        let Some(file) = snapshot.to_notebook_or_file(db) else {
            return Ok(None);
        };

        let Some(offset) = params.text_document_position_params.position.to_text_size(
            db,
            file,
            snapshot.uri(),
            snapshot.encoding(),
        ) else {
            return Ok(None);
        };
        let client_capabilities = snapshot.resolved_client_capabilities();
        let completions = completion(
            db,
            snapshot.workspace_settings().completions(),
            CompletionCapabilities::default()
                .snippets(client_capabilities.supports_completion_item_snippets()),
            file,
            offset,
        );
        if completions.is_empty() {
            return Ok(None);
        }

        // Safety: we just checked that completions is not empty.
        let max_index_len = OneIndexed::new(completions.len()).unwrap().digits().get();
        let items: Vec<CompletionItem> = completions
            .into_iter()
            .enumerate()
            .map(|(i, comp)| {
                let kind = comp.kind.map(ty_kind_to_lsp_kind);
                let type_display = comp.ty.map(|ty| ty.display(db).to_string());
                let import_edit = comp.import.as_ref().and_then(|edit| {
                    let range = edit
                        .range()
                        .to_lsp_range(db, file, snapshot.encoding())?
                        .local_range();
                    Some(TextEdit {
                        range,
                        new_text: edit.content().map(ToString::to_string).unwrap_or_default(),
                    })
                });

                let label = comp.label.to_string();
                let import_suffix = comp
                    .module_name
                    .and_then(|name| import_edit.is_some().then(|| format!(" (import {name})")));
                let (label, label_details) = if snapshot
                    .resolved_client_capabilities()
                    .supports_completion_item_label_details()
                {
                    let label_details = CompletionItemLabelDetails {
                        detail: import_suffix,
                        description: type_display.clone(),
                    };
                    (label, Some(label_details))
                } else {
                    let label = import_suffix
                        .map(|suffix| format!("{label}{suffix}"))
                        .unwrap_or(label);
                    (label, None)
                };

                let documentation = comp.documentation.map(|docstring| {
                    let (kind, value) = if snapshot
                        .resolved_client_capabilities()
                        .prefers_markdown_in_completion()
                    {
                        (lsp_types::MarkupKind::Markdown, docstring.render_markdown())
                    } else {
                        (
                            lsp_types::MarkupKind::PlainText,
                            docstring.render_plaintext(),
                        )
                    };

                    Documentation::MarkupContent(lsp_types::MarkupContent { kind, value })
                });
                let insert_text = comp.insert.map(String::from);
                let insert_text_format = match comp.insert_text_format {
                    CompletionInsertTextFormat::PlainText => None,
                    CompletionInsertTextFormat::Snippet => Some(InsertTextFormat::Snippet),
                };

                CompletionItem {
                    label,
                    kind,
                    sort_text: Some(format!("{i:-max_index_len$}")),
                    detail: type_display,
                    label_details,
                    insert_text,
                    insert_text_format,
                    additional_text_edits: import_edit.map(|edit| vec![edit]),
                    documentation,
                    ..Default::default()
                }
            })
            .collect();
        let len = items.len();
        let response = CompletionResponse::CompletionList(CompletionList {
            is_incomplete: true,
            items,
            item_defaults: None,
            apply_kind: None,
        });
        tracing::debug!(
            "Completions request returned {len} suggestions in {elapsed:?}",
            elapsed = Instant::now().duration_since(start)
        );
        Ok(Some(response))
    }
}

impl RetriableRequestHandler for CompletionRequestHandler {
    const RETRY_ON_CANCELLATION: bool = true;
}

fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind {
    // Gimme my dang globs in tight scopes!
    #[allow(clippy::enum_glob_use)]
    use self::CompletionKind::*;

    // ref https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
    match kind {
        Text => CompletionItemKind::Text,
        Method => CompletionItemKind::Method,
        Function => CompletionItemKind::Function,
        Constructor => CompletionItemKind::Constructor,
        Field => CompletionItemKind::Field,
        Variable => CompletionItemKind::Variable,
        Class => CompletionItemKind::Class,
        Interface => CompletionItemKind::Interface,
        Module => CompletionItemKind::Module,
        Property => CompletionItemKind::Property,
        Unit => CompletionItemKind::Unit,
        Value => CompletionItemKind::Value,
        Enum => CompletionItemKind::Enum,
        Keyword => CompletionItemKind::Keyword,
        Snippet => CompletionItemKind::Snippet,
        Color => CompletionItemKind::Color,
        File => CompletionItemKind::File,
        Reference => CompletionItemKind::Reference,
        Folder => CompletionItemKind::Folder,
        EnumMember => CompletionItemKind::EnumMember,
        Constant => CompletionItemKind::Constant,
        Struct => CompletionItemKind::Struct,
        Event => CompletionItemKind::Event,
        Operator => CompletionItemKind::Operator,
        TypeParameter => CompletionItemKind::TypeParameter,
    }
}
