defmodule Sidecar.GRPC.Generators.GRPCWithCustomOptions do
  # ref: https://github.com/elixir-protobuf/protobuf/blob/main/lib/protobuf/protoc/generator/grpc.ex
  @moduledoc """
  Plugin to Generate gRPC code from protbuf service definitions, this plugin outputs any extension options
  on service methods as a param to [`GRPC.Service.rpc/4`](https://github.com/drowzy/grpc/blob/grpc_transcoding/lib/grpc/service.ex#L56)
  """

  @behaviour ProtobufGenerate.Plugin

  alias Protobuf.Protoc.Generator.Util

  @impl true
  def template do
    """
    <%= if @render do %>
      defmodule <%= @module %>.Service do
        use GRPC.Service, name: <%= inspect(@service_name) %>, protoc_gen_elixir_version: "<%= @version %>"

        <%= if @descriptor_fun_body do %>
        def descriptor do
          # credo:disable-for-next-line
          <%= @descriptor_fun_body %>
        end
        <% end %>

        <%= for {method_name, input, output, options} <- @methods do %>
        rpc :<%= method_name %>, <%= input %>, <%= output %>, <%= options %>
        <% end %>
      end

      defmodule <%= @module %>.Stub do
        use GRPC.Stub, service: <%= @module %>.Service
      end
    <% end %>
    """
  end

  @impl true
  def generate(ctx, %Google.Protobuf.FileDescriptorProto{service: [_ | _] = svcs} = desc) do
    svcs =
      Enum.filter(svcs, fn svc ->
        Map.get(svc.options || %{}, :__pb_extensions__, %{})
        |> Map.get({Spawn.Actors.PbExtension, :actor})
      end)

    do_generate(ctx, svcs, desc)
  end

  def generate(_ctx, _opts), do: {"unknown", [render: false]}

  defp do_generate(_ctx, [], _desc), do: {"unknown", [render: false]}

  defp do_generate(ctx, svcs, desc) do
    for svc <- svcs do
      mod_name = Util.mod_name(ctx, [Macro.camelize(svc.name)])
      name = Util.prepend_package_prefix(ctx.package, svc.name)

      descriptor_fun_body =
        if ctx.gen_descriptors? do
          Util.descriptor_fun_body(desc)
        else
          nil
        end

      methods =
        for m <- svc.method do
          input = service_arg(Util.type_from_type_name(ctx, m.input_type), m.client_streaming)
          output = service_arg(Util.type_from_type_name(ctx, m.output_type), m.server_streaming)

          options =
            m.options
            |> opts()
            |> inspect(limit: :infinity)

          {m.name, input, output, options}
        end

      {mod_name,
       [
         render: true,
         module: mod_name,
         service_name: name,
         methods: methods,
         descriptor_fun_body: descriptor_fun_body,
         version: Util.version()
       ]}
    end
  end

  defp service_arg(type, _streaming? = true), do: "stream(#{type})"
  defp service_arg(type, _streaming?), do: type

  defp opts(nil), do: %{}

  defp opts(%Google.Protobuf.MethodOptions{__pb_extensions__: extensions})
       when extensions == %{} do
    %{}
  end

  defp opts(%Google.Protobuf.MethodOptions{__pb_extensions__: extensions}) do
    for {{type, field}, value} <- extensions, into: %{} do
      {field, %{type: type, value: value}}
    end
  end
end
