defmodule ConduitMcp.Component do
  @moduledoc """
  Behaviour for defining individual MCP tools, resources, and prompts as separate modules.

  Each component is a standalone module with its own schema and execution logic.
  Components are registered with a `ConduitMcp.Endpoint` which aggregates them
  into a full MCP server.

  ## Component Types

  - `:tool` — An MCP tool with input parameters and execution logic
  - `:resource` — An MCP resource identified by a URI template
  - `:prompt` — An MCP prompt with arguments that returns messages

  ## Tool Example

      defmodule MyApp.Echo do
        use ConduitMcp.Component, type: :tool, description: "Echoes text back"

        schema do
          field :text, :string, "The text to echo", required: true, max_length: 150
        end

        @impl true
        def execute(%{text: text}, _conn) do
          text(text)
        end
      end

  ## Resource Example

      defmodule MyApp.ReadUser do
        use ConduitMcp.Component,
          type: :resource,
          uri: "user://{id}",
          description: "Reads a user by ID",
          mime_type: "application/json"

        @impl true
        def execute(%{id: id}, _conn) do
          user = MyApp.Users.get!(id)
          {:ok, %{"contents" => [%{
            "uri" => "user://\#{id}",
            "mimeType" => "application/json",
            "text" => JSON.encode!(user)
          }]}}
        end
      end

  ## Prompt Example

      defmodule MyApp.CodeReview do
        use ConduitMcp.Component, type: :prompt, description: "Code review assistant"

        schema do
          field :code, :string, "Code to review", required: true
          field :language, :string, "Programming language", default: "elixir"
        end

        @impl true
        def execute(%{code: code, language: language}, _conn) do
          {:ok, %{"messages" => [
            system("You are a code reviewer for \#{language}"),
            user("Review this code:\\n\#{code}")
          ]}}
        end
      end

  ## Options

  - `:type` — Required. One of `:tool`, `:resource`, `:prompt`
  - `:name` — Optional. Defaults to last module segment, snake_cased
  - `:description` — Required for tools and prompts
  - `:uri` — Required for resources. URI template with `{param}` placeholders
  - `:mime_type` — Optional for resources
  - `:scope` — Optional OAuth scope string
  - `:annotations` — Optional keyword list of tool annotations (e.g., `destructive: true`)

  ## Generated Functions

  After compilation, each component module exports:

  - `__component_type__/0` — Returns `:tool`, `:resource`, or `:prompt`
  - `__component_name__/0` — Returns the string name
  - `__component_description__/0` — Returns the description
  - `__component_schema__/0` — Returns the JSON Schema map for MCP clients
  - `__validation_schema__/0` — Returns the NimbleOptions schema for runtime validation
  - `__component_opts__/0` — Returns raw component options
  """

  @doc """
  Called to execute the component's logic.

  For tools: receives validated params (atom-keyed) and the Plug.Conn.
  For resources: receives URI params (atom-keyed) extracted from the template and the Plug.Conn.
  For prompts: receives validated args (atom-keyed) and the Plug.Conn.

  Must return `{:ok, map()}` or `{:error, map()}`.
  """
  @callback execute(params :: map(), conn :: Plug.Conn.t()) ::
              {:ok, map()} | {:error, map()}

  @doc false
  @callback __component_type__() :: :tool | :resource | :prompt

  @doc false
  @callback __component_name__() :: String.t()

  @doc false
  @callback __component_description__() :: String.t() | nil

  @doc false
  @callback __component_schema__() :: map()

  @doc false
  @callback __validation_schema__() :: keyword()

  @doc false
  @callback __component_opts__() :: keyword()

  # All callbacks are generated by @before_compile, none need to be optional

  defmacro __using__(opts) do
    quote do
      @behaviour ConduitMcp.Component

      import ConduitMcp.Component.Schema
      import ConduitMcp.DSL.Helpers

      @component_type Keyword.fetch!(unquote(opts), :type)
      @component_name ConduitMcp.Component.__derive_name__(
                        __MODULE__,
                        Keyword.get(unquote(opts), :name)
                      )
      @component_description Keyword.get(unquote(opts), :description)
      @component_opts unquote(opts)
      @component_fields []

      @before_compile ConduitMcp.Component
    end
  end

  defmacro __before_compile__(env) do
    module = env.module
    type = Module.get_attribute(module, :component_type)
    name = Module.get_attribute(module, :component_name)
    description = Module.get_attribute(module, :component_description)
    opts = Module.get_attribute(module, :component_opts)
    fields = Module.get_attribute(module, :component_fields) || []

    # Compile-time validations
    validate_component!(type, name, description, opts, module)

    # Build schemas from fields at compile time
    json_schema = build_json_schema(type, name, description, fields, opts)
    validation_schema = build_validation_schema(fields)

    quote do
      @impl true
      def __component_type__, do: unquote(type)

      @impl true
      def __component_name__, do: unquote(name)

      @impl true
      def __component_description__, do: unquote(description)

      @impl true
      def __component_schema__ do
        unquote(Macro.escape(json_schema))
      end

      @impl true
      def __validation_schema__ do
        unquote(Macro.escape(validation_schema))
      end

      @impl true
      def __component_opts__, do: unquote(opts)
    end
  end

  @doc false
  def __derive_name__(module, nil) do
    module
    |> Module.split()
    |> List.last()
    |> Macro.underscore()
  end

  def __derive_name__(_module, name) when is_binary(name), do: name
  def __derive_name__(_module, name) when is_atom(name), do: Atom.to_string(name)

  # Compile-time validation

  defp validate_component!(type, _name, description, opts, module) do
    unless type in [:tool, :resource, :prompt] do
      raise CompileError,
        description:
          "#{inspect(module)}: invalid component type #{inspect(type)}. " <>
            "Must be :tool, :resource, or :prompt"
    end

    if type in [:tool, :prompt] and is_nil(description) do
      raise CompileError,
        description: "#{inspect(module)}: #{type} components require a :description option"
    end

    if type == :resource and is_nil(Keyword.get(opts, :uri)) do
      raise CompileError,
        description: "#{inspect(module)}: resource components require a :uri option"
    end
  end

  # Schema generation — reuses existing SchemaBuilder

  defp build_json_schema(:tool, name, description, fields, opts) do
    annotations = build_annotations(Keyword.get(opts, :annotations))
    meta = build_meta(Keyword.get(opts, :ui))

    tool_def = %{
      name: name,
      description: description,
      params: fields,
      annotations: annotations,
      meta: meta,
      title: Keyword.get(opts, :title),
      icons: Keyword.get(opts, :icons),
      output_schema: Keyword.get(opts, :output_schema),
      task_support: Keyword.get(opts, :task_support)
    }

    ConduitMcp.DSL.SchemaBuilder.build_tool_schema(tool_def)
  end

  defp build_json_schema(:prompt, name, description, fields, _opts) do
    prompt_def = %{
      name: name,
      description: description,
      args: fields
    }

    ConduitMcp.DSL.SchemaBuilder.build_prompt_schema(prompt_def)
  end

  defp build_json_schema(:resource, _name, description, _fields, opts) do
    uri = Keyword.fetch!(opts, :uri)
    mime_type = Keyword.get(opts, :mime_type)

    resource_def = %{
      uri: uri,
      description: description,
      mime_type: mime_type
    }

    ConduitMcp.DSL.SchemaBuilder.build_resource_schema(resource_def)
  end

  defp build_annotations(nil), do: nil
  defp build_annotations([]), do: nil

  defp build_annotations(annotations) when is_list(annotations) do
    Map.new(annotations, fn {k, v} -> {Atom.to_string(k), v} end)
  end

  defp build_meta(nil), do: nil

  defp build_meta(resource_uri) when is_binary(resource_uri),
    do: %{ui: %{resourceUri: resource_uri}, "ui/resourceUri": resource_uri}

  defp build_validation_schema(fields) do
    ConduitMcp.Validation.SchemaConverter.dsl_params_to_nimble_options(fields)
  end
end
