lib/impl/wrapper.ex

defmodule PatreonEx.Impl.Wrapper do

  @scopes MapSet.new([
    "campaigns",
    "campaigns.members",
    "campaigns.members[email]",
    "campaigns.members.address",
    "identity",
    "identity.memberships",
    "identity[email]"
  ])



  defp base_url(),     do: "www.patreon.com"
  # defp redirect_uri(), do: Patreon.Config.redirect_uri()
  # defp client_id,      do: Patreon.Config.client_id()
  # defp secret,         do: Patreon.Config.secret()

  defp http(host, method, path, query, headers, body \\ "") do
    {:ok, conn} = Mint.HTTP.connect(:https, host, 443)

    query_string =
      cond do
        query == %{} -> ""
        TRUE -> "?" <> URI.encode_query(query)
      end

    path = path <> query_string

    {:ok, conn, ref} =
      Mint.HTTP.request(
        conn,
        method,
        path,
        headers,
        body
      )

    receive_resp(conn, ref, nil, nil, false)
  end

  defp receive_resp(conn, ref, status, data, done?) do
    receive do
      message ->
        {:ok, conn, responses} = Mint.HTTP.stream(conn, message)

        {new_status, new_data, done?} =
          Enum.reduce(responses, {status, data, done?}, fn
            {:status, ^ref, new_status}, {_old_status, data, done?} -> {new_status, data, done?}
            {:headers, ^ref, _headers}, acc -> acc
            {:data, ^ref, binary}, {status, nil, done?} -> {status, binary, done?}
            {:data, ^ref, binary}, {status, data, done?} -> {status, data <> binary, done?}
            {:done, ^ref}, {status, data, _done?} -> {status, data, true}
          end)

        cond do
          done? and new_status == 200 -> {:ok, new_data}
          done? -> {:error, {new_status, new_data}}
          !done? -> receive_resp(conn, ref, new_status, new_data, done?)
        end
    end
  end


  defp authorize_query(scope, true, redirect_uri, client_id) do
    state = random_string()

    %{
      response_type: "code",
      redirect_uri: redirect_uri,
      scope: Enum.join(scope, " "),
      state: state,
      client_id: client_id
    }
  end

  defp authorize_query(_scope, false, redirect_uri, client_id) do
    state = random_string()

    %{
      response_type: "code",
      redirect_uri: redirect_uri,
      state: state,
      client_id: client_id
    }
  end

  def authorize_url(scope, redirect_uri, client_id) when is_list(scope) do
    scope = MapSet.new(scope)

    cond do
      MapSet.subset?(scope, @scopes) ->
        {
          :ok,
          base_url()
            <> "/oauth2/authorize?"
            <> URI.encode_query(authorize_query(scope, scope !== MapSet.new(), redirect_uri, client_id))
        }
      true ->
        {:error, "Invalid scope. Valid scopes are: #{Enum.join(@scopes, ", ")}"}
    end
  end

  def authorize_url(redirect_uri, client_id) do
    base_url()
    <> "/oauth2/authorize?"
    <> URI.encode_query(authorize_query(@scopes, true, redirect_uri, client_id))
  end

  defp random_string() do
    binary = <<
      System.system_time(:nanosecond)::64,
      :erlang.phash2({node(), self()})::16,
      :erlang.unique_integer()::16
    >>

    binary
    |> Base.url_encode64()
    |> String.replace(["/", "+"], "-")
  end

  defp validate_query(validation_code, redirect_uri, client_id, client_secret) do
    %{
      code: validation_code,
      grant_type: "authorization_code",
      client_id: client_id,
      client_secret: client_secret,
      redirect_uri:  redirect_uri,
    }
  end

  def validate_code(validation_code, redirect_uri, client_id, client_secret) do
    resp = http(
      base_url(),
      "POST",
      "/api/oauth2/token",
      validate_query(validation_code, redirect_uri, client_id, client_secret),
      ["Content-Type: application/x-www-form-urlencoded"]
    )

    case resp do
      {:ok, info} -> {:ok, Jason.decode!(info)}
      {:error, _reason} = err -> err
    end
  end

  def get_user(token, params) do
    resp = http(
      base_url(),
      "GET",
      "/api/oauth2/v2/identity",
      params, # %{"fields[user]" => Enum.join(fields, ",")},
      [{"Authorization", "Bearer #{token}"}]
    )

    case resp do
      {:ok, info} -> {:ok, Jason.decode!(info)}
      {:error, _reason} = err -> err
    end
  end

  def get_campaigns(token, params) do
    resp = http(
      base_url(),
      "GET",
      "/api/oauth2/v2/campaigns",
      params, # %{"fields[campaign]" => Enum.join(fields, ",")},#
      [{"Authorization", "Bearer #{token}"}]
    )

    case resp do
      {:ok, info} -> {:ok, Jason.decode!(info)}
      {:error, _reason} = err -> err
    end
  end
end