lib/blockchain/blockchain_api.ex

defmodule HeliumElixir.BlockchainApi do
  use HTTPoison.Base
  alias HTTPoison.Response
  alias HeliumElixir.BlockchainApi.BlockchainRequestConfig
  alias HeliumElixir.BlockchainApi.TakeMinConfig

  def post_json(
        %BlockchainRequestConfig{headers: headers, base_url: base_url, path: path},
        body \\ %{}
      ) do
    case post(base_url <> path, headers, Jason.encode!(body)) do
      {:ok, %Response{body: body}} ->
        {:ok, body |> Jason.decode!()}

      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end

  def get_json(
        %BlockchainRequestConfig{headers: headers, base_url: base_url, path: path},
        params \\ %{}
      ) do
    case get(base_url <> path, headers, params: params, follow_redirect: true) do
      {:ok, %Response{body: body, status_code: status_code}} ->
        case status_code do
          200 ->
            response = body |> Jason.decode!()
            {:ok, response}

          400 ->
            IO.puts("Cursor invalid")
            {:error, :cursor_invalid}

          404 ->
            IO.puts("404")
            {:error, :four_oh_four}

          503 ->
            IO.puts("Too busy")
            {:error, :too_busy}

          _ ->
            IO.puts("Unknown error status code: #{status_code}")
            IO.inspect(body)
            {:error, String.to_atom("unknown_#{status_code}")}
        end

      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end

  @doc """
  Pages the given endpoint until `min_count` is reached.
  The data returned will be at least the amount requested unless no more items exist.

  Returns `{:ok, %{"data" => data, "cursor" => cursor}}`.
  """
  def take_min(
        %BlockchainRequestConfig{} = config,
        %TakeMinConfig{min_count: min_count, params: params, acc: acc, cursor: cursor}
      ) do
    case get_json(config, Map.merge(params, %{cursor: cursor})) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        next_data = acc ++ data
        acc_count = length(next_data)

        if(acc_count >= min_count || is_nil(next_cursor)) do
          {:ok, %{"data" => next_data, "cursor" => next_cursor}}
        else
          take_min(config, %TakeMinConfig{
            min_count: min_count,
            params: params,
            acc: next_data,
            cursor: next_cursor
          })
        end

      {:ok, %{"data" => data}} ->
        {:ok, %{"data" => acc ++ data, "cursor" => ""}}

      {:error, error} ->
        len = length(acc)

        case len do
          0 ->
            {:error, error}

          _ ->
            {:ok, %{"data" => acc, "cursor" => cursor}}
        end

      _ ->
        {:error}
    end
  end

  # paginates through all entries of a provided url
  def paginate_all(%BlockchainRequestConfig{} = config, params \\ %{}, acc \\ [], cursor \\ nil) do
    case get_json(config, Map.merge(params, %{cursor: cursor})) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        paginate_all(config, params, acc ++ data, next_cursor)

      {:ok, %{"data" => data}} ->
        {:ok, acc ++ data}

      {:error, error} ->
        {:error, error}
    end
  end

  def each_page(%BlockchainRequestConfig{} = config, fun, cursor \\ nil) do
    case get_json(config, %{cursor: cursor}) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        fun.(data)
        each_page(config, fun, next_cursor)

      {:ok, %{"data" => data}} ->
        fun.(data)
        :ok

      {:error, error} ->
        {:error, error}
    end
  end

  # paginates through all entries of a provided url, while fun returns true
  def paginate_while(
        %BlockchainRequestConfig{} = config,
        fun,
        params \\ %{},
        acc \\ [],
        cursor \\ nil
      ) do
    case get_json(config, Map.merge(params, %{cursor: cursor})) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        filtered_data = data |> Enum.take_while(fn d -> fun.(d) end)

        if length(filtered_data) == length(data) do
          IO.puts("keep going")
          paginate_while(config, fun, params, acc ++ filtered_data, next_cursor)
        else
          IO.puts("fun returned subset, we're done")
          {:ok, acc ++ filtered_data}
        end

      {:ok, %{"data" => data}} ->
        filtered_data = data |> Enum.take_while(fn d -> fun.(d) end)
        {:ok, acc ++ filtered_data}

      {:error, error} ->
        {:error, error}
    end
  end

  # returns the first entry that fun evaluates true for
  def paginate_find(%BlockchainRequestConfig{} = config, fun, params \\ %{}, cursor \\ nil) do
    case get_json(config, Map.merge(params, %{cursor: cursor})) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        case Enum.find(data, fun) do
          nil ->
            paginate_find(config, fun, params, next_cursor)

          found ->
            {:ok, found}
        end

      {:ok, %{"data" => data}} ->
        case Enum.find(data, fun) do
          nil ->
            {:error, :not_found}

          found ->
            {:ok, found}
        end

      {:error, error} ->
        {:error, error}
    end
  end
end