use lsp_server::ErrorCode;
use lsp_types::CodeActionResolveRequest;
use lsp_types::{self as types};

use ruff_linter::codes::Rule;

use crate::PositionEncoding;
use crate::edit::WorkspaceEditTracker;
use crate::fix::Fixes;
use crate::server::Result;
use crate::server::SupportedCodeAction;
use crate::server::api::LSPResult;
use crate::session::Client;
use crate::session::{DocumentQuery, DocumentSnapshot, ResolvedClientCapabilities, Session};

pub(crate) struct CodeActionResolve;

impl super::RequestHandler for CodeActionResolve {
    type RequestType = CodeActionResolveRequest;
}

impl super::BackgroundRequestHandler for CodeActionResolve {
    type Snapshot = std::result::Result<DocumentSnapshot, String>;

    fn snapshot(session: &Session, action: &types::CodeAction) -> Self::Snapshot {
        let data = action
            .data
            .clone()
            .ok_or_else(|| "it doesn't contain Ruff's document URI payload".to_string())?;

        let uri: lsp_types::Uri = serde_json::from_value(data)
            .map_err(|err| format!("its Ruff document URI payload is invalid: {err}"))?;

        session
            .take_snapshot(uri.clone())
            .ok_or_else(|| format!("document `{uri}` isn't open"))
    }

    fn run_with_snapshot(
        snapshot: Self::Snapshot,
        _client: &Client,
        mut action: types::CodeAction,
    ) -> Result<types::CodeAction> {
        let snapshot = match snapshot {
            Ok(snapshot) => snapshot,
            Err(err) => {
                tracing::warn!("Returning code action unchanged because {err}.");
                return Ok(action);
            }
        };

        let query = snapshot.query();

        let code_actions = SupportedCodeAction::from_kind(
            action
                .kind
                .clone()
                .ok_or(anyhow::anyhow!("No kind was given for code action"))
                .with_failure_code(ErrorCode::InvalidParams)?,
        )
        .collect::<Vec<_>>();

        // Ensure that the code action maps to _exactly one_ supported code action
        let [action_kind] = code_actions.as_slice() else {
            return Err(anyhow::anyhow!(
                "Code action resolver did not expect code action kind {:?}",
                action.kind.as_ref().unwrap()
            ))
            .with_failure_code(ErrorCode::InvalidParams);
        };

        match action_kind {
            SupportedCodeAction::SourceFixAll | SupportedCodeAction::SourceOrganizeImports
                if snapshot.is_notebook_cell() =>
            {
                // This should never occur because we ignore generating these code actions for a
                // notebook cell in the `textDocument/codeAction` request handler.
                return Err(anyhow::anyhow!(
                    "Code action resolver cannot resolve {:?} for a notebook cell",
                    action_kind.to_kind().as_str()
                ))
                .with_failure_code(ErrorCode::InvalidParams);
            }
            _ => {}
        }

        action.edit = match action_kind {
            SupportedCodeAction::SourceFixAll | SupportedCodeAction::NotebookSourceFixAll => Some(
                resolve_edit_for_fix_all(
                    query,
                    snapshot.resolved_client_capabilities(),
                    snapshot.encoding(),
                )
                .with_failure_code(ErrorCode::InternalError)?,
            ),
            SupportedCodeAction::SourceOrganizeImports
            | SupportedCodeAction::NotebookSourceOrganizeImports => Some(
                resolve_edit_for_organize_imports(
                    query,
                    snapshot.resolved_client_capabilities(),
                    snapshot.encoding(),
                )
                .with_failure_code(ErrorCode::InternalError)?,
            ),
            SupportedCodeAction::QuickFix => {
                // The client may ask us to resolve a code action, as it has no way of knowing
                // whether e.g. `command` field will be filled out by the resolution callback.
                return Ok(action);
            }
        };

        Ok(action)
    }
}

pub(super) fn resolve_edit_for_fix_all(
    query: &DocumentQuery,
    client_capabilities: &ResolvedClientCapabilities,
    encoding: PositionEncoding,
) -> crate::Result<types::WorkspaceEdit> {
    let mut tracker = WorkspaceEditTracker::new(client_capabilities);
    tracker.set_fixes_for_document(fix_all_edit(query, encoding)?, query.version())?;
    Ok(tracker.into_workspace_edit())
}

pub(super) fn fix_all_edit(
    query: &DocumentQuery,
    encoding: PositionEncoding,
) -> crate::Result<Fixes> {
    crate::fix::fix_all(query, &query.settings().linter, encoding)
}

pub(super) fn resolve_edit_for_organize_imports(
    query: &DocumentQuery,
    client_capabilities: &ResolvedClientCapabilities,
    encoding: PositionEncoding,
) -> crate::Result<types::WorkspaceEdit> {
    let mut tracker = WorkspaceEditTracker::new(client_capabilities);
    tracker.set_fixes_for_document(organize_imports_edit(query, encoding)?, query.version())?;
    Ok(tracker.into_workspace_edit())
}

pub(super) fn organize_imports_edit(
    query: &DocumentQuery,
    encoding: PositionEncoding,
) -> crate::Result<Fixes> {
    let mut linter_settings = query.settings().linter.clone();
    linter_settings.rules = [
        Rule::UnsortedImports,       // I001
        Rule::MissingRequiredImport, // I002
        // Note: ModuleImportNotAtTopOfFile's fixes are unsafe. We include them
        // here in order to match isort's behaviour and what we believe
        // developers want. Since the fixes are unsafe, we're relying on this
        // edit action not performing them unless the user has opted-in to these
        // fixes in their settings (i.e: `extend-safe-fixes` in the
        // `pyproject.toml` or similar).
        Rule::ModuleImportNotAtTopOfFile, // E402
    ]
    .into_iter()
    .collect();

    crate::fix::fix_all(query, &linter_settings, encoding)
}
