defmodule Berrypod.Pagination do
  @moduledoc """
  Offset-based pagination for Ecto queries.

  Returns a `%Berrypod.Pagination{}` struct with items, page metadata,
  and total count. Context modules pipe their filtered/ordered queries
  into `paginate/2` instead of `Repo.all/0`.
  """

  import Ecto.Query

  alias Berrypod.Repo

  @enforce_keys [:items, :page, :per_page, :total_count, :total_pages]
  defstruct [:items, :page, :per_page, :total_count, :total_pages]

  @type t :: %__MODULE__{
          items: [any()],
          page: pos_integer(),
          per_page: pos_integer(),
          total_count: non_neg_integer(),
          total_pages: non_neg_integer()
        }

  @doc """
  Paginates an Ecto query.

  Options:
    * `:page` - current page (default 1, clamped to valid range)
    * `:per_page` - items per page (default 25)
  """
  def paginate(query, opts \\ []) do
    page = max(opts[:page] || 1, 1)
    per_page = opts[:per_page] || 25

    count_query =
      query
      |> exclude(:preload)
      |> exclude(:select)
      |> exclude(:order_by)

    total_count = Repo.aggregate(count_query, :count)
    total_pages = max(ceil(total_count / per_page), 1)
    page = min(page, total_pages)

    items =
      query
      |> limit(^per_page)
      |> offset(^((page - 1) * per_page))
      |> Repo.all()

    %__MODULE__{
      items: items,
      page: page,
      per_page: per_page,
      total_count: total_count,
      total_pages: total_pages
    }
  end

  @doc """
  Returns "Showing X\u2013Y of Z" for display, or "No results" when empty.
  """
  def showing_text(%__MODULE__{total_count: 0}), do: "No results"

  def showing_text(%__MODULE__{} = p) do
    first = (p.page - 1) * p.per_page + 1
    last = min(p.page * p.per_page, p.total_count)
    "Showing #{first}\u2013#{last} of #{p.total_count}"
  end

  @doc """
  Computes page numbers with `:ellipsis` gaps for display.

  Always shows first, last, and a window around the current page.
  Returns a flat list like `[1, :ellipsis, 4, 5, 6, :ellipsis, 20]`.
  """
  def page_numbers(%__MODULE__{total_pages: total}) when total <= 7 do
    Enum.to_list(1..total)
  end

  def page_numbers(%__MODULE__{page: current, total_pages: total}) do
    window =
      [current - 1, current, current + 1]
      |> Enum.filter(&(&1 >= 1 and &1 <= total))

    pages = Enum.uniq([1] ++ window ++ [total]) |> Enum.sort()

    # Insert :ellipsis between non-consecutive numbers
    pages
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.flat_map(fn [a, b] ->
      if b - a > 1, do: [a, :ellipsis], else: [a]
    end)
    |> Kernel.++([List.last(pages)])
  end

  @doc """
  Parses a page number from request params. Always returns >= 1.
  """
  def parse_page(params, key \\ "page") do
    case params[key] do
      nil ->
        1

      val when is_binary(val) ->
        case Integer.parse(val) do
          {n, _} when n > 0 -> n
          _ -> 1
        end

      val when is_integer(val) and val > 0 ->
        val

      _ ->
        1
    end
  end
end
