defprotocol Funx.Appendable do
  @moduledoc """
  A protocol for combining values in a generic, extensible way.

  The `Appendable` protocol defines how two values of the same type can be combined. It is
  used throughout Funx in functions like `traverse_a/2` and `wither_a/2` to accumulate
  intermediate results without coupling logic to a specific type.

  This protocol enables functions to remain flexible and composable when reducing,
  aggregating, or accumulating values across a wide variety of domains.

  ## Required functions

  * `coerce/1` – Normalizes an input value into a form suitable for aggregation.
  * `append/2` – Combines two values of the same type into one.

  ### Default – Flat list aggregation

  A fallback implementation is provided for all types that do not define a specific
  `Appendable` instance. This default uses list concatenation as a universal aggregation
  strategy: all inputs are coerced into lists (if not already), and combined using `++`.

  When using the default aggregation strategy, values are collected in a plain list:

  ```elixir
  validate_positive = fn x ->
    Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
  end

  validate_even = fn x ->
    Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
  end

  Funx.Monad.Either.validate(4, [validate_positive, validate_even])
  #=> %Funx.Monad.Either.Right{right: 4}

  Funx.Monad.Either.validate(3, [validate_positive, validate_even])
  #=> %Funx.Monad.Either.Left{left: ["Value must be even: 3"]}

  Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
  #=> %Funx.Monad.Either.Left{left: ["Value must be positive: -3", "Value must be even: -3"]}
  ```

  ### Structured aggregation with `ValidationError`

  You can also use a custom struct to hold errors. This example uses `ValidationError`:

  ```elixir
  alias Funx.Errors.ValidationError

  validate_positive = fn x ->
    Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
    |> Funx.Monad.Either.map_left(&ValidationError.new/1)
  end

  validate_even = fn x ->
    Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
    |> Funx.Monad.Either.map_left(&ValidationError.new/1)
  end

  Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
  #=> %Funx.Monad.Either.Left{
  #     left: %ValidationError{
  #       errors: ["Value must be positive: -3", "Value must be even: -3"]
  #     }
  #   }
  ```

  """

  @fallback_to_any true

  @doc """
  Normalizes a single input value into a form suitable for accumulation.
  """
  def coerce(term)

  @doc """
  Combines two values into a single result.

  Implementations must ensure the operation is associative within their type. For types
  that require disambiguation or structural control, define a custom implementation.
  """
  def append(accumulator, coerced)
end

defimpl Funx.Appendable, for: Any do
  @spec coerce(term()) :: list()
  def coerce(value) when is_list(value), do: value
  def coerce(value), do: [value]

  @spec append(term(), term()) :: list()
  def append(acc, coerced), do: coerce(acc) ++ coerce(coerced)
end
