local make_builtin = require("null-ls.helpers").make_builtin
local u = require("null-ls.utils")
local methods = require("null-ls.methods")
local h = require("cspell.helpers")
local make_add_to_json = require("cspell.code_actions.make_add_to_json")
local make_add_to_dictionary_action = require("cspell.code_actions.make_add_to_dictionary_action")

local CODE_ACTION = methods.internal.CODE_ACTION

--- Filter diagnostics generated by the cspell built-in
---@param bufnr number
---@param lnum number
---@param cursor_col number
---@return table<number, Diagnostic>
local cspell_diagnostics = function(bufnr, lnum, cursor_col)
    local diagnostics = {}
    for _, diagnostic in ipairs(vim.diagnostic.get(bufnr, { lnum = lnum })) do
        if diagnostic.source == "cspell" and cursor_col >= diagnostic.col and cursor_col < diagnostic.end_col then
            table.insert(diagnostics, diagnostic)
        end
    end
    return diagnostics
end

---@param code_action_config CSpellSourceConfig
---@param params GeneratorParams
---@param cspell_json_path string
local get_config_info = function(code_action_config, params, cspell_json_path)
    -- In theory, the call to async_get_config_info in the diagnostics source
    -- should already have been loaded, that's why we're defaulting reading the
    -- config synchronously here.
    if code_action_config.read_config_synchronously then
        return h.sync_get_config_info(params, cspell_json_path)
    end

    return h.async_get_config_info(params, cspell_json_path)
end

return make_builtin({
    name = "cspell",
    meta = {
        url = "https://github.com/streetsidesoftware/cspell",
        description = "Injects actions to fix typos found by `cspell`.",
        notes = {
            "This source depends on the `cspell` built-in diagnostics source, so make sure to register it, too.",
        },
        usage = [[
          local cspell = require('cspell')
          local sources = { cspell.diagnostics, cspell.code_actions }
        ]],
    },
    method = CODE_ACTION,
    filetypes = {},
    generator = {
        ---@param params GeneratorParams
        ---@return table<number, CodeAction>
        fn = function(params)
            params.cwd = params.cwd or u.get_root()

            ---@type CSpellSourceConfig
            local code_action_config =
                vim.tbl_extend("force", { read_config_synchronously = true }, params:get_config())

            ---@type table<number|string, string>
            local cspell_config_paths = {}

            local cspell_config_directories = code_action_config.cspell_config_dirs or {}
            table.insert(cspell_config_directories, params.cwd)

            for _, cspell_config_directory in pairs(cspell_config_directories) do
                local cspell_config_path = h.get_config_path(params, cspell_config_directory)
                if cspell_config_path == nil then
                    cspell_config_path = h.generate_cspell_config_path(params, cspell_config_directory)
                end
                cspell_config_paths[cspell_config_directory] = cspell_config_path
            end

            local default_cspell_config = get_config_info(code_action_config, params, cspell_config_paths[params.cwd])

            ---@type table<number, CodeAction>
            local actions = {}

            local diagnostics = cspell_diagnostics(params.bufnr, params.row - 1, params.col)
            if vim.tbl_isempty(diagnostics) then
                return actions
            end

            for _, diagnostic in ipairs(diagnostics) do
                -- replace word with a suggestion
                for _, suggestion in ipairs(diagnostic.user_data.suggestions) do
                    local kind
                    -- CSpell marks fixes with a trailing asterisk
                    if suggestion:match("*$") then
                        kind = "Fix"
                        suggestion = string.sub(suggestion, 1, -2)
                    else
                        kind = "Suggestion"
                    end

                    table.insert(actions, {
                        title = string.format("%s: %s", kind, suggestion),
                        action = function()
                            h.set_word(diagnostic, suggestion)

                            local on_success = code_action_config.on_success
                            local on_use_suggestion = code_action_config.on_use_suggestion

                            if on_success then
                                vim.notify_once(
                                    "The on_success callback is deprecated, use on_use_suggestion instead",
                                    vim.log.levels.INFO,
                                    { title = "cspell.nvim" }
                                )
                                on_success(
                                    default_cspell_config and default_cspell_config.path,
                                    params,
                                    "use_suggestion"
                                )
                            end

                            if on_use_suggestion then
                                ---@type UseSuggestionSuccess
                                local payload = {
                                    misspelled_word = diagnostic.user_data.misspelled,
                                    suggestion = suggestion,
                                    cspell_config_path = default_cspell_config and default_cspell_config.path,
                                    generator_params = params,
                                }
                                on_use_suggestion(payload)
                            end
                        end,
                    })
                end

                local word = h.get_word(diagnostic)
                local dictionary_cspell_configs = {}

                for _, cspell_config_path in pairs(cspell_config_paths) do
                    -- add word to "words" in cspell.json
                    table.insert(
                        actions,
                        make_add_to_json({
                            diagnostic = diagnostic,
                            word = word,
                            params = params,
                            cspell_config_path = cspell_config_path,
                        })
                    )
                    local cspell_config = get_config_info(code_action_config, params, cspell_config_path)
                    if cspell_config and cspell_config.config.dictionaryDefinitions then
                        dictionary_cspell_configs[cspell_config_path] = cspell_config
                    end
                end

                -- add word to a custom dictionary
                for _, cspell_config in pairs(dictionary_cspell_configs) do
                    for _, dictionary in ipairs(cspell_config.config.dictionaryDefinitions) do
                        if dictionary ~= nil and dictionary.addWords then
                            table.insert(
                                actions,
                                make_add_to_dictionary_action({
                                    diagnostic = diagnostic,
                                    word = word,
                                    params = params,
                                    cspell = cspell_config,
                                    dictionary = dictionary,
                                })
                            )
                        end
                    end
                end
            end

            return actions
        end,
    },
})
