Skip to main content

lib/jido/chat/github/transport/req_client.ex

defmodule Jido.Chat.GitHub.Transport.ReqClient do
  @moduledoc "Req-backed GitHub REST transport."
  @behaviour Jido.Chat.GitHub.Transport

  @api_version "2026-03-10"

  @impl true
  def create_issue(owner, repo, title, body, opts) do
    request(:post, "/repos/#{owner}/#{repo}/issues", opts,
      json: issue_create_body(title, body, opts)
    )
  end

  @impl true
  def create_issue_comment(owner, repo, issue_number, body, opts) do
    request(:post, "/repos/#{owner}/#{repo}/issues/#{issue_number}/comments", opts,
      json: %{body: body}
    )
  end

  @impl true
  def update_issue_comment(owner, repo, comment_id, body, opts) do
    request(:patch, "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}", opts,
      json: %{body: body}
    )
  end

  @impl true
  def delete_issue_comment(owner, repo, comment_id, opts) do
    case request(:delete, "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}", opts) do
      {:ok, _} -> :ok
      {:error, _} = error -> error
    end
  end

  @impl true
  def get_issue(owner, repo, issue_number, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues/#{issue_number}", opts)
  end

  @impl true
  def list_issue_comments(owner, repo, issue_number, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues/#{issue_number}/comments", opts)
  end

  @impl true
  def get_issue_comment(owner, repo, comment_id, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}", opts)
  end

  @impl true
  def list_issues(owner, repo, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues", opts, params: issue_list_params(opts))
  end

  @impl true
  def create_issue_reaction(owner, repo, issue_number, content, opts) do
    request(:post, "/repos/#{owner}/#{repo}/issues/#{issue_number}/reactions", opts,
      json: %{content: content}
    )
  end

  @impl true
  def create_issue_comment_reaction(owner, repo, comment_id, content, opts) do
    request(:post, "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}/reactions", opts,
      json: %{content: content}
    )
  end

  @impl true
  def list_issue_reactions(owner, repo, issue_number, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues/#{issue_number}/reactions", opts)
  end

  @impl true
  def list_issue_comment_reactions(owner, repo, comment_id, opts) do
    request(:get, "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}/reactions", opts)
  end

  @impl true
  def delete_issue_reaction(owner, repo, issue_number, reaction_id, opts) do
    case request(
           :delete,
           "/repos/#{owner}/#{repo}/issues/#{issue_number}/reactions/#{reaction_id}",
           opts
         ) do
      {:ok, _} -> :ok
      {:error, _} = error -> error
    end
  end

  @impl true
  def delete_issue_comment_reaction(owner, repo, comment_id, reaction_id, opts) do
    case request(
           :delete,
           "/repos/#{owner}/#{repo}/issues/comments/#{comment_id}/reactions/#{reaction_id}",
           opts
         ) do
      {:ok, _} -> :ok
      {:error, _} = error -> error
    end
  end

  defp request(method, path, opts, req_opts \\ []) do
    token = Keyword.get(opts, :token) || System.get_env("GITHUB_TOKEN")
    base_url = Keyword.get(opts, :base_url, "https://api.github.com")

    headers = [
      {"accept", "application/vnd.github+json"},
      {"x-github-api-version", @api_version}
    ]

    headers =
      if token in [nil, ""], do: headers, else: [{"authorization", "Bearer #{token}"} | headers]

    [method: method, url: base_url <> path, headers: headers]
    |> Keyword.merge(req_opts)
    |> Req.request()
    |> case do
      {:ok, %{status: status, body: body}} when status in 200..299 -> {:ok, body || %{}}
      {:ok, %{status: status, body: body}} -> {:error, {:github_api_error, status, body}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp issue_list_params(opts) do
    []
    |> maybe_param(:state, Keyword.get(opts, :state, "open"))
    |> maybe_param(:per_page, Keyword.get(opts, :limit, Keyword.get(opts, :per_page)))
    |> maybe_param(:page, Keyword.get(opts, :page))
    |> maybe_param(:since, Keyword.get(opts, :since))
    |> maybe_param(:labels, Keyword.get(opts, :labels))
    |> maybe_param(:sort, Keyword.get(opts, :sort))
    |> maybe_param(:direction, Keyword.get(opts, :direction))
  end

  defp issue_create_body(title, body, opts) do
    %{title: title}
    |> maybe_json(:body, body)
    |> maybe_json(:labels, Keyword.get(opts, :labels))
    |> maybe_json(:assignees, Keyword.get(opts, :assignees))
    |> maybe_json(:milestone, Keyword.get(opts, :milestone))
  end

  defp maybe_json(body, _key, value) when value in [nil, "", []], do: body
  defp maybe_json(body, key, value), do: Map.put(body, key, value)

  defp maybe_param(params, _key, value) when value in [nil, ""], do: params
  defp maybe_param(params, key, value), do: Keyword.put(params, key, value)
end