local api = vim.api

local Utils = require("avante.utils")
local Sidebar = require("avante.sidebar")
local Selection = require("avante.selection")
local Config = require("avante.config")

---@class Avante
local M = {
  ---@type avante.Sidebar[] we use this to track chat command across tabs
  sidebars = {},
  ---@type avante.Selection[]
  selections = {},
  ---@type {sidebar?: avante.Sidebar, selection?: avante.Selection}
  current = { sidebar = nil, selection = nil },
}

M.did_setup = false

local H = {}

H.commands = function()
  local cmd = function(n, c, o)
    o = vim.tbl_extend("force", { nargs = 0 }, o or {})
    api.nvim_create_user_command("Avante" .. n, c, o)
  end

  cmd("Ask", function()
    M.ask()
  end, { desc = "avante: ask AI for code suggestions" })
  cmd("Edit", function()
    M.edit()
  end, { desc = "avante: edit selected block" })
  cmd("Refresh", function()
    M.refresh()
  end, { desc = "avante: refresh windows" })
end

H.keymaps = function()
  vim.keymap.set({ "n", "v" }, "<Plug>(AvanteAsk)", function()
    M.ask()
  end, { noremap = true })
  vim.keymap.set("v", "<Plug>(AvanteEdit)", function()
    M.edit()
  end, { noremap = true })
  vim.keymap.set("n", "<Plug>(AvanteRefresh)", function()
    M.refresh()
  end, { noremap = true })
  --- the following is kinda considered as internal mappings.
  vim.keymap.set("n", "<Plug>(AvanteToggleDebug)", function()
    M.toggle.debug()
  end)
  vim.keymap.set("n", "<Plug>(AvanteToggleHint)", function()
    M.toggle.hint()
  end)

  if Config.behaviour.auto_set_keymaps then
    Utils.safe_keymap_set({ "n", "v" }, Config.mappings.ask, function()
      M.ask()
    end, { desc = "avante: ask" })
    Utils.safe_keymap_set("v", Config.mappings.edit, function()
      M.edit()
    end, { desc = "avante: edit" })
    Utils.safe_keymap_set("n", Config.mappings.refresh, function()
      M.refresh()
    end, { desc = "avante: refresh" })
    Utils.safe_keymap_set("n", Config.mappings.toggle.debug, function()
      M.toggle.debug()
    end, { desc = "avante: toggle debug" })
    Utils.safe_keymap_set("n", Config.mappings.toggle.hint, function()
      M.toggle.hint()
    end, { desc = "avante: toggle hint" })
  end
end

---@class ApiCaller
---@operator call(...): any

H.api = function(fun)
  return setmetatable({ api = true }, {
    __call = function(...)
      return fun(...)
    end,
  }) --[[@as ApiCaller]]
end

H.signs = function()
  vim.fn.sign_define("AvanteInputPromptSign", { text = Config.windows.input.prefix })
end

H.augroup = api.nvim_create_augroup("avante_autocmds", { clear = true })

H.autocmds = function()
  api.nvim_create_autocmd("TabEnter", {
    group = H.augroup,
    pattern = "*",
    once = true,
    callback = function(ev)
      local tab = tonumber(ev.file)
      M._init(tab or api.nvim_get_current_tabpage())
      if Config.hints.enabled and not M.current.selection.did_setup then
        M.current.selection:setup_autocmds()
      end
    end,
  })

  api.nvim_create_autocmd("VimResized", {
    group = H.augroup,
    callback = function()
      local sidebar, _ = M._get()
      if not sidebar then
        return
      end
      if not sidebar:is_open() then
        return
      end
      sidebar:resize()
    end,
  })

  api.nvim_create_autocmd("TabClosed", {
    group = H.augroup,
    pattern = "*",
    callback = function(ev)
      local tab = tonumber(ev.file)
      local s = M.sidebars[tab]
      local sl = M.selections[tab]
      if s then
        s:reset()
      end
      if sl then
        sl:delete_autocmds()
      end
      if tab ~= nil then
        M.sidebars[tab] = nil
      end
    end,
  })

  vim.schedule(function()
    M._init(api.nvim_get_current_tabpage())
    if Config.hints.enabled then
      M.current.selection:setup_autocmds()
    end
  end)

  api.nvim_create_autocmd("ColorSchemePre", {
    group = H.augroup,
    callback = function()
      require("avante.highlights").setup()
    end,
  })

  -- automatically setup Avante filetype to markdown
  vim.treesitter.language.register("markdown", "Avante")
end

---@param current boolean? false to disable setting current, otherwise use this to track across tabs.
---@return avante.Sidebar, avante.Selection
function M._get(current)
  local tab = api.nvim_get_current_tabpage()
  local sidebar = M.sidebars[tab]
  local selection = M.selections[tab]
  if current ~= false then
    M.current.sidebar = sidebar
    M.current.selection = selection
  end
  return sidebar, selection
end

---@param id integer
function M._init(id)
  local sidebar = M.sidebars[id]
  local selection = M.selections[id]

  if not sidebar then
    sidebar = Sidebar:new(id)
    M.sidebars[id] = sidebar
  end
  if not selection then
    selection = Selection:new(id)
    M.selections[id] = selection
  end
  M.current = { sidebar = sidebar, selection = selection }
  return M
end

M.toggle = { api = true }

M.toggle.debug = H.api(Utils.toggle_wrap({
  name = "debug",
  get = function()
    return Config.debug
  end,
  set = function(state)
    Config.override({ debug = state })
  end,
}))

M.toggle.hint = H.api(Utils.toggle_wrap({
  name = "hint",
  get = function()
    return Config.hints.enabled
  end,
  set = function(state)
    Config.override({ hints = { enabled = state } })
  end,
}))

setmetatable(M.toggle, {
  __index = M.toggle,
  __call = function()
    local sidebar, _ = M._get()
    if not sidebar then
      M._init(api.nvim_get_current_tabpage())
      M.current.sidebar:open()
      return true
    end

    return sidebar:toggle()
  end,
})

M.ask = H.api(function()
  M.toggle()
end)

M.edit = H.api(function()
  local _, selection = M._get()
  if not selection then
    return
  end
  selection:create_editing_input()
end)

M.refresh = H.api(function()
  local sidebar, _ = M._get()
  if not sidebar then
    return
  end
  if not sidebar:is_open() then
    return
  end

  local curbuf = vim.api.nvim_get_current_buf()

  local focused = sidebar.result.bufnr == curbuf or sidebar.input.bufnr == curbuf
  if focused or not sidebar:is_open() then
    return
  end

  local listed = vim.api.nvim_get_option_value("buflisted", { buf = curbuf })

  if Utils.is_sidebar_buffer(curbuf) or not listed then
    return
  end

  local curwin = vim.api.nvim_get_current_win()

  sidebar:close()
  sidebar.code.winid = curwin
  sidebar.code.bufnr = curbuf
  sidebar:render()
end)

---@param opts? avante.Config
function M.setup(opts)
  if vim.fn.has("nvim-0.10") == 0 then
    vim.api.nvim_echo({
      { "Avante requires at least nvim-0.10", "ErrorMsg" },
      { "Please upgrade your neovim version", "WarningMsg" },
      { "Press any key to exit", "ErrorMsg" },
    }, true, {})
    vim.fn.getchar()
    vim.cmd([[quit]])
  end

  ---PERF: we can still allow running require("avante").setup() multiple times to override config if users wish to
  ---but most of the other functionality will only be called once from lazy.nvim
  Config.setup(opts)

  if M.did_setup then
    return
  end

  require("avante.history").setup()
  require("avante.highlights").setup()
  require("avante.diff").setup()
  require("avante.providers").setup()
  require("avante.clipboard").setup()

  -- setup helpers
  H.autocmds()
  H.commands()
  H.keymaps()
  H.signs()

  M.did_setup = true
end

return M