defmodule Raxol.Agent.Action.Schema do
  @moduledoc """
  Lightweight schema validation for Action input/output.

  No external dependencies -- pure pattern matching on keyword list specs.

  ## Field Specs

      schema = [
        path: [type: :string, required: true, description: "File path"],
        timeout: [type: :integer, default: 5000],
        format: [type: :string, enum: ["json", "text"], default: "text"]
      ]

  Supported types: `:string`, `:integer`, `:float`, `:boolean`, `:atom`,
  `:map`, `:list`, `{:list, type}`.
  """

  @type field_spec :: keyword()
  @type schema :: [{atom(), field_spec()}]

  @doc """
  Validate params against a schema.

  Returns `{:ok, validated_params}` with defaults applied,
  or `{:error, [{field, reason}]}` on failure.
  """
  @spec validate(map(), schema()) :: {:ok, map()} | {:error, [{atom(), String.t()}]}
  def validate(params, schema) when is_map(params) and is_list(schema) do
    {result, errors} =
      Enum.reduce(schema, {params, []}, fn {field, spec}, {acc, errs} ->
        validate_field(field, spec, acc, errs)
      end)

    case errors do
      [] -> {:ok, result}
      _ -> {:error, Enum.reverse(errors)}
    end
  end

  @doc """
  Convert a schema to a JSON Schema-compatible tool definition.

  Produces the OpenAI/Anthropic function calling format:

      %{
        "type" => "function",
        "function" => %{
          "name" => name,
          "description" => description,
          "parameters" => %{
            "type" => "object",
            "properties" => %{...},
            "required" => [...]
          }
        }
      }
  """
  @spec to_json_schema(schema(), String.t(), String.t()) :: map()
  def to_json_schema(schema, name, description \\ "") do
    {properties, required} =
      Enum.reduce(schema, {%{}, []}, fn {field, spec}, {props, req} ->
        prop = field_to_json_schema(spec)
        props = Map.put(props, to_string(field), prop)

        req =
          if Keyword.get(spec, :required, false),
            do: [to_string(field) | req],
            else: req

        {props, req}
      end)

    %{
      "type" => "function",
      "function" => %{
        "name" => name,
        "description" => description,
        "parameters" => %{
          "type" => "object",
          "properties" => properties,
          "required" => Enum.reverse(required)
        }
      }
    }
  end

  # -- Field validation --------------------------------------------------------

  defp validate_field(field, spec, params, errors) do
    required = Keyword.get(spec, :required, false)
    default = Keyword.get(spec, :default)
    type = Keyword.get(spec, :type)
    enum_values = Keyword.get(spec, :enum)

    case Map.fetch(params, field) do
      {:ok, value} ->
        with :ok <- check_type(field, type, value),
             :ok <- check_enum(field, enum_values, value) do
          {params, errors}
        else
          {:error, err} -> {params, [err | errors]}
        end

      :error when required ->
        {params, [{field, "is required"} | errors]}

      :error when not is_nil(default) ->
        {Map.put(params, field, default), errors}

      :error ->
        {params, errors}
    end
  end

  defp check_type(_field, nil, _value), do: :ok
  defp check_type(_field, :string, value) when is_binary(value), do: :ok
  defp check_type(_field, :integer, value) when is_integer(value), do: :ok
  defp check_type(_field, :float, value) when is_float(value), do: :ok
  defp check_type(_field, :float, value) when is_integer(value), do: :ok
  defp check_type(_field, :boolean, value) when is_boolean(value), do: :ok
  defp check_type(_field, :atom, value) when is_atom(value), do: :ok
  defp check_type(_field, :map, value) when is_map(value), do: :ok
  defp check_type(_field, :list, value) when is_list(value), do: :ok

  defp check_type(field, {:list, inner_type}, value) when is_list(value) do
    if Enum.all?(value, &type_matches?(inner_type, &1)),
      do: :ok,
      else: {:error, {field, "must be a list of #{inner_type}"}}
  end

  defp check_type(field, type, _value) do
    {:error, {field, "must be of type #{inspect(type)}"}}
  end

  defp check_enum(_field, nil, _value), do: :ok

  defp check_enum(field, enum_values, value) do
    if value in enum_values,
      do: :ok,
      else: {:error, {field, "must be one of #{inspect(enum_values)}"}}
  end

  defp type_matches?(:string, v), do: is_binary(v)
  defp type_matches?(:integer, v), do: is_integer(v)
  defp type_matches?(:float, v), do: is_float(v) or is_integer(v)
  defp type_matches?(:boolean, v), do: is_boolean(v)
  defp type_matches?(:atom, v), do: is_atom(v)
  defp type_matches?(:map, v), do: is_map(v)
  defp type_matches?(:list, v), do: is_list(v)
  defp type_matches?(_, _), do: false

  # -- JSON Schema conversion --------------------------------------------------

  defp field_to_json_schema(spec) do
    type = Keyword.get(spec, :type)
    desc = Keyword.get(spec, :description)
    enum_values = Keyword.get(spec, :enum)

    prop = %{"type" => type_to_json(type)}
    prop = if desc, do: Map.put(prop, "description", desc), else: prop
    prop = if enum_values, do: Map.put(prop, "enum", enum_values), else: prop
    prop
  end

  defp type_to_json(:string), do: "string"
  defp type_to_json(:integer), do: "integer"
  defp type_to_json(:float), do: "number"
  defp type_to_json(:boolean), do: "boolean"
  defp type_to_json(:atom), do: "string"
  defp type_to_json(:map), do: "object"
  defp type_to_json(:list), do: "array"
  defp type_to_json({:list, _}), do: "array"
  defp type_to_json(nil), do: "string"
end
