# Creating Custom Nodes

All custom nodes must implement the `Jido.BehaviorTree.Node` behaviour and use the Zoi struct pattern.

## Basic Structure

```elixir
defmodule MyApp.Nodes.MyCustomNode do
  @moduledoc "A custom behavior tree node"

  alias Jido.BehaviorTree.Node

  @schema Zoi.struct(
    __MODULE__,
    %{
      my_field: Zoi.string(description: "Custom field") |> Zoi.default("default"),
      internal_state: Zoi.any(description: "Internal state") |> Zoi.optional()
    },
    coerce: true
  )

  @type t :: unquote(Zoi.type_spec(@schema))
  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  def schema, do: @schema

  @behaviour Jido.BehaviorTree.Node

  def new(my_field \\ "default") do
    %__MODULE__{my_field: my_field, internal_state: nil}
  end

  @impl true
  def tick(%__MODULE__{} = state, tick) do
    {:success, state}
  end

  @impl true
  def halt(%__MODULE__{} = state) do
    %{state | internal_state: nil}
  end
end
```

## The Node Behaviour

The `Jido.BehaviorTree.Node` behaviour requires two callbacks:

### `tick/2`

Called each time the node is executed.

```elixir
@callback tick(node :: t(), tick :: Tick.t()) ::
  {Jido.BehaviorTree.Status.t(), t()}
```

**Parameters:**
- `node` - The current node state
- `tick` - The tick context containing blackboard, tree info, etc.

**Return Values:**
- `{:success, updated_node}` - Node completed successfully
- `{:failure, updated_node}` - Node failed
- `{:running, updated_node}` - Node is still executing
- `{{:error, reason}, updated_node}` - An execution error occurred

### `tick_with_context/2` (optional, recommended)

If your node mutates tick context (blackboard, directives, agent state), implement:

```elixir
@spec tick_with_context(t(), Jido.BehaviorTree.Tick.t()) ::
  {Jido.BehaviorTree.Status.t(), t(), Jido.BehaviorTree.Tick.t()}
```

This callback is used by `Tree.tick_with_context/2` and strategy/agent integrations
to persist context changes through execution.

### `halt/1`

Called when execution is interrupted (e.g., parent node stops ticking this branch).

```elixir
@callback halt(node :: t()) :: t()
```

Use this to clean up any running state.

## Using Zoi for Schemas

Zoi provides type-safe struct definitions with validation and coercion.

### Common Zoi Types

```elixir
# Basic types
Zoi.string()
Zoi.integer()
Zoi.boolean()
Zoi.atom()
Zoi.any()

# With constraints
Zoi.integer() |> Zoi.min(0)
Zoi.string() |> Zoi.min_length(1)

# Optional and defaults
Zoi.integer() |> Zoi.optional()
Zoi.integer() |> Zoi.default(0)

# Complex types
Zoi.list(Zoi.string())
Zoi.map(%{key: Zoi.string()})
```

### Schema Definition Pattern

```elixir
@schema Zoi.struct(
  __MODULE__,
  %{
    # Required field with description
    name: Zoi.string(description: "Node name"),
    
    # Optional field
    timeout: Zoi.integer(description: "Timeout in ms") |> Zoi.optional(),
    
    # Field with default
    retries: Zoi.integer(description: "Retry count") |> Zoi.default(3),
    
    # Child node
    child: Zoi.any(description: "Child node") |> Zoi.optional()
  },
  coerce: true
)

@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
```

## Example: Condition Node

A node that evaluates a condition function:

```elixir
defmodule MyApp.Nodes.Condition do
  @moduledoc "Evaluates a condition and returns success or failure"

  @schema Zoi.struct(
    __MODULE__,
    %{
      condition_fn: Zoi.any(description: "Function that returns boolean")
    },
    coerce: true
  )

  @type t :: unquote(Zoi.type_spec(@schema))
  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  def schema, do: @schema

  @behaviour Jido.BehaviorTree.Node

  def new(condition_fn) when is_function(condition_fn, 1) do
    %__MODULE__{condition_fn: condition_fn}
  end

  @impl true
  def tick(%__MODULE__{condition_fn: condition_fn} = state, tick) do
    if condition_fn.(tick.blackboard) do
      {:success, state}
    else
      {:failure, state}
    end
  end

  @impl true
  def halt(state), do: state
end
```

Usage:

```elixir
condition = Condition.new(fn blackboard ->
  blackboard[:health] > 50
end)
```

## Example: Async Node

A node that handles async operations:

```elixir
defmodule MyApp.Nodes.AsyncTask do
  @moduledoc "Runs an async task and waits for completion"

  @schema Zoi.struct(
    __MODULE__,
    %{
      task_fn: Zoi.any(description: "Function to run asynchronously"),
      task_ref: Zoi.any(description: "Running task reference") |> Zoi.optional()
    },
    coerce: true
  )

  @type t :: unquote(Zoi.type_spec(@schema))
  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  def schema, do: @schema

  @behaviour Jido.BehaviorTree.Node

  def new(task_fn) when is_function(task_fn, 0) do
    %__MODULE__{task_fn: task_fn, task_ref: nil}
  end

  @impl true
  def tick(%__MODULE__{task_ref: nil, task_fn: task_fn} = state, _tick) do
    task = Task.async(task_fn)
    {:running, %{state | task_ref: task}}
  end

  def tick(%__MODULE__{task_ref: task} = state, _tick) do
    case Task.yield(task, 0) do
      {:ok, {:ok, _result}} ->
        {:success, %{state | task_ref: nil}}

      {:ok, {:error, _reason}} ->
        {:failure, %{state | task_ref: nil}}

      nil ->
        {:running, state}
    end
  end

  @impl true
  def halt(%__MODULE__{task_ref: nil} = state), do: state

  def halt(%__MODULE__{task_ref: task} = state) do
    Task.shutdown(task, :brutal_kill)
    %{state | task_ref: nil}
  end
end
```
