--- Grep search bridge and renderer.
--- Wraps the Rust `live_grep` FFI function with file-based pagination state tracking.
--- Also provides renderer for live grep results with file grouping.
local M = {
  supports_cursor_rerender = true,
}

local fuzzy = require('fff.fuzzy')
local file_renderer = require('fff.picker_ui.file_renderer')
local tresitter_highlight = require('fff.treesitter_hl')

-- ===== Search Bridge =====

---@class fff.grep.SearchResult
---@field items table[] Array of grep match items
---@field total_matched number Total matches found in this call
---@field total_files_searched number Files actually searched in this call
---@field total_files number Total indexed files
---@field filtered_file_count number Total searchable files after filtering
---@field next_file_offset number File offset to pass for the next page (0 = no more results)
---@field regex_fallback_error string|nil Error message if regex compilation failed and search fell back to literal

local last_result = nil

--- Perform a grep search.
---@param query string The search query (may contain file constraints like *.rs)
---@param file_offset? number Index into sorted file list to start from (default 0)
---@param page_size? number Max matches to collect (default 50)
---@param config? table Grep configuration overrides
---@param grep_mode? string Search mode: "plain" (default), "regex", or "fuzzy"
---@return fff.grep.SearchResult
function M.search(query, file_offset, page_size, config, grep_mode)
  local conf = config or {}
  last_result = fuzzy.live_grep(
    query or '',
    file_offset or 0,
    page_size or 50,
    conf.max_file_size,
    conf.max_matches_per_file,
    conf.smart_case,
    grep_mode or 'plain',
    conf.time_budget_ms,
    conf.trim_whitespace
  )
  return last_result
end

--- Get metadata from the last search result.
---@return { total_matched: number, total_files_searched: number, total_files: number, next_file_offset: number }
function M.get_search_metadata()
  if not last_result then
    return { total_matched = 0, total_files_searched = 0, total_files = 0, next_file_offset = 0 }
  end
  return {
    total_matched = last_result.total_matched or 0,
    total_files_searched = last_result.total_files_searched or 0,
    total_files = last_result.total_files or 0,
    next_file_offset = last_result.next_file_offset or 0,
  }
end

-- ===== Renderer =====

--- Build the file group header line using the same layout as file_renderer.
--- Delegates to file_renderer.render_line (with combo disabled).
---@param item FileItem Grep match
---@param ctx table Render context
---@return string The header line string
local function build_group_header(item, ctx)
  local lines = file_renderer.render_line(item, ctx)
  return lines[1]
end

--- Apply highlights for a file group header line using file_renderer.
---@param item FileItem Grep match item
---@param ctx ListRenderContext Render context
---@param buf number Buffer handle
---@param ns_id number Namespace id
---@param row number 0-based row in buffer (header line)
local function apply_group_header_highlights(item, ctx, buf, ns_id, row)
  local line_content = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1] or ''
  local saved_cursor = ctx.cursor
  ctx.cursor = -1
  file_renderer.apply_highlights(item, ctx, 0, buf, ns_id, row + 1, line_content)
  ctx.cursor = saved_cursor
end

--- Format a grep match location string.
---@param item table Grep match item
---@param ctx table Render context
---@return string
local function format_location(item, ctx)
  local fmt = (ctx.config and ctx.config.grep and ctx.config.grep.location_format) or ':%d:%d'
  local ok, str = pcall(string.format, fmt, item.line_number or 0, (item.col or 0) + 1)
  if not ok then str = string.format(':%d:%d', item.line_number or 0, (item.col or 0) + 1) end
  return str
end

local BINARY_PLACEHOLDER = '<binary content>'

--- Render a single grep match line.
---@param item table Grep match item
---@param ctx table Render context
---@return string
local function render_match_line(item, ctx)
  local location = format_location(item, ctx)
  local separator = '  '
  local raw_content = item.line_content
  if type(raw_content) ~= 'string' then raw_content = raw_content and tostring(raw_content) or '' end
  local content = raw_content
  if item.is_binary_content then content = BINARY_PLACEHOLDER end

  local indent = ' '
  local prefix_display_w = #indent + #location + #separator
  local available = ctx.win_width - prefix_display_w - 2
  local content_display_w = vim.fn.strdisplaywidth(content)

  if content_display_w > available and available > 3 then
    local nchars = vim.fn.strchars(content)
    local lo, hi = 0, nchars
    while lo < hi do
      local mid = math.floor((lo + hi + 1) / 2)
      if vim.fn.strdisplaywidth(vim.fn.strcharpart(content, 0, mid)) <= available - 1 then
        lo = mid
      else
        hi = mid - 1
      end
    end
    content = vim.fn.strcharpart(content, 0, lo) .. '…'
  end

  local line = indent .. location .. separator .. content
  local padding = math.max(0, ctx.win_width - vim.fn.strdisplaywidth(line) + 5)

  item._match_indent = #indent
  item._content_offset = prefix_display_w
  item._trimmed_content = content

  return line .. string.rep(' ', padding)
end

--- Apply highlights for a grouped match line.
---@param item table Grep match item
---@param item_idx number 1-based item index
---@param buf number Buffer handle
---@param ns_id number Namespace id
---@param row number 0-based row in buffer
---@param line_content string The rendered line text
local function apply_match_highlights(item, item_idx, buf, ns_id, row, line_content, ctx)
  local config = ctx.config
  local is_cursor = item_idx == ctx.cursor
  local indent = item._match_indent or 1

  if is_cursor then
    vim.api.nvim_buf_set_extmark(buf, ns_id, row, 0, {
      end_col = 0,
      end_row = row + 1,
      hl_group = config.hl.cursor,
      hl_eol = true,
      priority = 100,
    })
  end

  local location_str = format_location(item, ctx)
  local loc_start = indent
  local loc_end = loc_start + #location_str
  if loc_end <= #line_content then
    pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, loc_start, {
      end_col = loc_end,
      hl_group = config.hl.grep_line_number or 'LineNr',
      priority = 150,
    })
  end

  local sep_start = loc_end
  local sep_end = sep_start + 2
  if sep_end <= #line_content then
    pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, sep_start, {
      end_col = sep_end,
      hl_group = 'Comment',
      priority = 150,
    })
  end

  local content_start = sep_end

  if item.is_binary_content then
    local content_end = content_start + #BINARY_PLACEHOLDER
    if content_end <= #line_content then
      pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, content_start, {
        end_col = content_end,
        hl_group = 'Comment',
        priority = 150,
      })
    end
  elseif item._trimmed_content and item.name then
    ctx._ts_lang_cache = ctx._ts_lang_cache or {}
    local lang = ctx._ts_lang_cache[item.name]
    if lang == nil then
      lang = tresitter_highlight.lang_from_filename(item.name) or false
      ctx._ts_lang_cache[item.name] = lang
    end

    if lang then
      local highlights = tresitter_highlight.get_line_highlights(item._trimmed_content, lang)
      for _, hl in ipairs(highlights) do
        local hl_start = content_start + hl.col
        local hl_end = content_start + hl.end_col
        if hl_start < #line_content and hl_end <= #line_content then
          pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, hl_start, {
            end_col = hl_end,
            hl_group = hl.hl_group,
            priority = 120,
          })
        end
      end
    end
  end

  if item.match_ranges and not item.is_binary_content then
    for _, range in ipairs(item.match_ranges) do
      local raw_start = range[1] or 0
      local raw_end = range[2] or 0

      if raw_end > 0 then
        raw_start = math.max(0, raw_start)
        local hl_start = content_start + raw_start
        local hl_end = content_start + raw_end
        if hl_start < #line_content and hl_end <= #line_content then
          pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, hl_start, {
            end_col = hl_end,
            hl_group = config.hl.grep_match or 'IncSearch',
            priority = 200,
          })
        end
      end
    end
  end

  if ctx.selected_items then
    local key = string.format('%s:%d:%d', item.relative_path, item.line_number or 0, item.col or 0)
    if ctx.selected_items[key] then
      vim.api.nvim_buf_set_extmark(buf, ns_id, row, 0, {
        sign_text = '▊',
        sign_hl_group = config.hl.selected or 'FFFSelected',
        priority = 1001,
      })
    end
  end
end

--- Render a single item's lines (called by list_renderer).
--- Returns 2 lines [header, match] for the first match of a file group,
--- or 1 line [match] for subsequent matches in the same file.
---@param item FileItem Grep match item
---@param ctx table Render context
---@param item_idx number 1-based item index in ctx.items
---@return string[]
function M.render_line(item, ctx, item_idx)
  -- First rendered item in this pass always gets header — fixes missing header
  -- when paginating backward in multi-page grep results (ctx is fresh per render).
  local is_first_visible = (item_idx == ctx.iter_start)
  local is_new_group = is_first_visible or (item.relative_path ~= ctx.grep_last_file)
  ctx.grep_last_file = item.relative_path

  local match_line = render_match_line(item, ctx)

  if is_new_group then
    ---@diagnostic disable-next-line: inject-field
    item._has_group_header = true
    local header_line = build_group_header(item, ctx)
    return { header_line, match_line }
  else
    ---@diagnostic disable-next-line: inject-field
    item._has_group_header = false
    return { match_line }
  end
end

--- Apply highlights for rendered lines (called by list_renderer).
---@param item FileItem Grep match item
---@param ctx ListRenderContext Render context
---@param item_idx number 1-based item index
---@param buf number Buffer handle
---@param ns_id number Namespace id
---@param line_idx number 1-based line index of the match line
---@param line_content string The rendered match line text
function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_content)
  local row = line_idx - 1

  apply_match_highlights(item, item_idx, buf, ns_id, row, line_content, ctx)

  ---@diagnostic disable-next-line: undefined-field
  if item._has_group_header then apply_group_header_highlights(item, ctx, buf, ns_id, row - 1) end
end

return M
