defmodule Cloak.Ecto.Migrator.CursorStream do
  @moduledoc false
  # Returns a stream of primary key values from a database table associated
  # with an Ecto.Schema.
  #
  # By default, uses the primary key as the cursor. Users can specify a
  # custom list of cursor fields using the Cloak.CustomCursor behaviour.

  import Ecto.Query

  def new(repo, schema, limit) do
    Stream.resource(
      fn ->
        [primary_key | _] = schema.__schema__(:primary_key)
        cursor_fields = fields_for_cursor(schema, primary_key)

        select_fields =
          [primary_key]
          |> Enum.concat(cursor_fields)
          |> Enum.uniq()

        query =
          schema
          |> select(^select_fields)
          |> order_by(^cursor_fields)

        cursor_record =
          query
          |> first()
          |> repo.one()

        %{
          results: [Map.get(cursor_record, primary_key)],
          repo: repo,
          schema: schema,
          query: query,
          cursor: Map.take(cursor_record, cursor_fields),
          cursor_fields: cursor_fields,
          primary_key: primary_key,
          limit: limit
        }
      end,
      &next/1,
      fn _config -> :ok end
    )
  end

  defp next(%{cursor: nil} = config) do
    {:halt, config}
  end

  defp next(%{results: [head | tail]} = config) do
    {[head], %{config | results: tail}}
  end

  defp next(%{results: []} = config) do
    results =
      config
      |> query_by_cursor()
      |> limit(^config.limit)
      |> config.repo.all()

    case length(results) do
      0 ->
        {:halt, config}

      _ ->
        new_cursor =
          results
          |> List.last()
          |> Map.take(config.cursor_fields)

        [head | tail] = Enum.map(results, &Map.get(&1, config.primary_key))
        {[head], %{config | cursor: new_cursor, results: tail}}
    end
  end

  defp fields_for_cursor(schema, primary_key) do
    if function_exported?(schema, :__cloak_cursor_fields__, 0) do
      schema.__cloak_cursor_fields__
    else
      [primary_key]
    end
  end

  defp query_by_cursor(config) do
    Enum.reduce(config.cursor_fields, config.query, fn field, query ->
      cursor_filter(query, field, config.schema.__schema__(:type, field), config.cursor[field])
    end)
  end

  defp cursor_filter(query, field, type, value)
       when type in [:utc_datetime, :naive_datetime, :date, :time] do
    where(query, [s], field(s, ^field) >= ^value)
  end

  defp cursor_filter(query, field, _type, value) do
    where(query, [s], field(s, ^field) > ^value)
  end
end
