defmodule Phantom.Request do
  @moduledoc "Standard requests and responses for the MCP protocol"
  defstruct [:id, :type, :method, :params, :response, :spec]

  @opaque t :: %__MODULE__{
            id: String.t(),
            type: String.t(),
            method: String.t(),
            params: map(),
            response: map(),
            spec: Phantom.ResourceTemplate.t() | Phantom.Tool.t() | Phantom.Prompt.t()
          }

  @connection -32000
  @resource_not_found -32002
  @invalid_request -32600
  @method_not_found -32601
  @invalid_params -32602
  @internal_error -32603
  @parse_error -32700

  import Phantom.Utils
  alias Phantom.Session

  @doc "Invalid request"
  def invalid(message \\ nil) do
    %{code: @invalid_request, message: message || "Invalid request"}
  end

  @doc "Invalid request due to bad parameters"
  def invalid_params(data), do: %{code: @invalid_params, message: "Invalid Params", data: data}
  def invalid_params, do: %{code: @invalid_params, message: "Invalid Params"}

  @doc "Invalid request due to parsing error"
  def parse_error(message \\ nil) do
    %{code: @parse_error, message: message || "Parsing error"}
  end

  @doc "Invalid request due to no streaming connection being available"
  def closed(message \\ nil) do
    %{code: @connection, message: message || "Connection closed"}
  end

  @doc "Server encountered an issue"
  def internal_error(message \\ nil) do
    %{code: @internal_error, message: message || "Internal server error"}
  end

  @doc "The method is not implemented or found"
  def not_found(message \\ nil),
    do: %{code: @method_not_found, message: message || "Method not found"}

  @doc "The resource is not found"
  def resource_not_found(data),
    do: %{code: @resource_not_found, data: data, message: "Resource not found"}

  @doc false
  def build(nil), do: nil

  def build(%{"jsonrpc" => "2.0", "method" => method} = request)
      when is_binary(method) do
    {:ok,
     struct!(__MODULE__,
       params: request["params"] || %{},
       method: method,
       id: request["id"]
     )}
  end

  def build(%{"jsonrpc" => "2.0", "result" => result} = response)
      when is_map(result) do
    {:ok,
     struct!(__MODULE__,
       response: result,
       id: response["id"]
     )}
  end

  def build(request) do
    {:error, struct!(__MODULE__, id: request["id"], response: error(request["id"], invalid()))}
  end

  @doc false
  def to_json(%__MODULE__{} = request) do
    %{
      "jsonrpc" => "2.0",
      "method" => request.method,
      "id" => request.id,
      "params" => request.params
    }
  end

  @doc "Ping request"
  def ping() do
    %{jsonrpc: "2.0", method: "ping", id: UUIDv7.generate()}
  end

  @doc "An empty response"
  def empty() do
    %{jsonrpc: "2.0", result: ""}
  end

  @doc false
  def result(%__MODULE__{} = request, type, result) do
    %{request | type: type, response: %{id: request.id, jsonrpc: "2.0", result: result}}
  end

  @doc "Response error"
  def error(id \\ nil, error) do
    %{jsonrpc: "2.0", error: error, id: id}
  end

  @doc false
  def completion_response({:reply, results, session}, _session) do
    {:reply, completion_response(results), session}
  end

  def completion_response({:error, error}, session) do
    {:error, error, session}
  end

  def completion_response({:noreply, session}, _session) do
    {:noreply, session}
  end

  def completion_response(results) when is_list(results) do
    %{
      completion: %{
        values: Enum.take(List.wrap(results), 100),
        hasMore: length(results) > 100
      }
    }
  end

  def completion_response(%{} = results) do
    %{
      completion:
        remove_nils(%{
          values: Enum.take(List.wrap(results[:values]), 100),
          total: results[:total],
          hasMore: results[:has_more] || false
        })
    }
  end

  @doc false
  def resource_response({:error, reason}, _uri, session) do
    {:error, reason, session}
  end

  def resource_response({:noreply, _} = result, _uri, _session), do: result

  def resource_response({:error, _reason, %Session{}} = result, _uri, _session) do
    result
  end

  def resource_response(nil, uri, session) do
    {:error, resource_not_found(%{uri: uri}), session}
  end

  def resource_response({:reply, nil, %Session{} = session}, uri, _session) do
    {:error, resource_not_found(%{uri: uri}), session}
  end

  def resource_response({:reply, results, %Session{} = session}, _uri, _session) do
    {:reply, Phantom.Resource.response(results), session}
  end

  @doc "Resource updated notification"
  def resource_updated(content) do
    %{jsonrpc: "2.0", method: "notifications/resources/updated", params: content}
  end

  @doc "Tools List updated notification"
  def tools_updated do
    %{jsonrpc: "2.0", method: "notifications/tools/list_changed"}
  end

  @doc "Prompts List updated notification"
  def prompts_updated do
    %{jsonrpc: "2.0", method: "notifications/prompts/list_changed"}
  end

  @doc "Resources List updated notification"
  def resources_updated do
    %{jsonrpc: "2.0", method: "notifications/resources/list_changed"}
  end

  @doc "A generic notifiation"
  def notify(content) do
    %{jsonrpc: "2.0", method: "notifications/message", params: content}
  end

  @doc "Progress notifiation"
  def notify_progress(progress_token, progress, total) do
    %{
      jsonrpc: "2.0",
      method: "notifications/progress",
      params:
        remove_nils(%{
          progressToken: progress_token,
          progress: progress,
          total: total
        })
    }
  end
end
