defmodule Funx.Monad.Either do
  @moduledoc """
  The `Funx.Monad.Either` module provides an implementation of the `Either` monad, a functional abstraction used to model computations that may fail.

  An `Either` represents one of two possibilities:

    - `Right(value)`: a successful result
    - `Left(error)`: a failure or error

  This pattern is commonly used in place of exceptions to handle errors explicitly and safely in functional pipelines.

  ### Constructors

    - `right/1`: Wraps a value in the `Right` branch.
    - `left/1`: Wraps a value in the `Left` branch.
    - `pure/1`: Alias for `right/1`.

  ### Refinement

    - `right?/1`: Returns `true` if the value is a `Right`.
    - `left?/1`: Returns `true` if the value is a `Left`.

  ### Fallback and Extraction

    - `get_or_else/2`: Returns the value from a `Right`, or a default if `Left`.
    - `or_else/2`: Returns the original `Right`, or invokes a fallback function if `Left`.
    - `map_left/2`: Transforms a `Left` using a function, leaving `Right` values unchanged.
    - `flip/1`: Swaps `Left` and `Right`, turning errors into successes and vice versa.
    - `filter_or_else/3`: Applies a predicate to the `Right` value; if false, returns a fallback `Left`.

  ### List Operations

    - `concat/1`: Removes all `Left` values and unwraps the `Right` values from a list.
    - `concat_map/2`: Applies a function and collects only `Right` results.
    - `sequence/1`: Converts a list of `Either` values into a single `Either` of list.
    - `traverse/2`: Applies a function to each element in a list and sequences the results.
    - `sequence_a/1`: Like `sequence/1`, but accumulates all errors from `Left` values.
    - `traverse_a/2`: Like `traverse/2`, but accumulates all `Left` values instead of short-circuiting.
    - `wither_a/2`: Like `traverse_a/2`, but filters out `Nothing` results and collects only `Just` values.

  ### Validation

    - `validate/2`: Applies multiple validators to a single input, collecting all errors.

  ### Lifting

    - `lift_predicate/3`: Turns a predicate into an `Either`, returning `Right` on `true` and `Left` on `false`.
    - `lift_maybe/2`: Converts a `Maybe` to an `Either` using a fallback value.
    - `lift_eq/1`: Lifts an equality function into the `Either` context.
    - `lift_ord/1`: Lifts an ordering function into the `Either` context.

    ## Transformation

    - `map_left/2` – Transforms the error inside a `Left`, leaving `Right` values untouched.

  ### Elixir Interoperability

    - `from_result/1`: Converts `{:ok, val}` or `{:error, err}` into an `Either`.
    - `to_result/1`: Converts an `Either` into a result tuple.
    - `from_try/1`: Runs a function and returns `Right` on success or `Left` on exception.
    - `to_try!/1`: Unwraps a `Right`, or raises an error from a `Left`.

  ## Protocols

  The `Left` and `Right` structs implement the following protocols, making the `Either` abstraction composable and extensible:

    - `Funx.Eq`: Enables equality comparisons between `Either` values.
    - `Funx.Foldable`: Implements `fold_l/3` and `fold_r/3` for reducing over contained values.
    - `Funx.Monad`: Provides `map/2`, `ap/2`, and `bind/2` for monadic composition.
    - `Funx.Ord`: Defines ordering behavior for comparing `Left` and `Right` values.

  Although these implementations are defined on each constructor (`Left` and `Right`), the behavior is consistent across the `Either` abstraction.

  This module helps you model failure explicitly, compose error-aware logic, and integrate cleanly with Elixir's functional idioms.
  """

  import Funx.Appendable, only: [append: 2, coerce: 1]
  import Funx.Monad, only: [map: 2]
  import Funx.Foldable, only: [fold_l: 3]

  alias Funx.Eq
  alias Funx.Ord
  alias Funx.Monad.{Either, Maybe}
  alias Either.{Left, Right}
  alias Maybe.{Just, Nothing}

  @type t(left, right) :: Left.t(left) | Right.t(right)

  @doc """
  Wraps a value in the `Right` monad.

  ## Examples

      iex> Funx.Monad.Either.right(5)
      %Funx.Monad.Either.Right{right: 5}
  """
  @spec right(any()) :: Right.t(any())
  def right(value), do: Right.pure(value)

  @doc """
  Wraps a value in the `Left` monad.

  ## Examples

      iex> Funx.Monad.Either.left("error")
      %Funx.Monad.Either.Left{left: "error"}
  """
  @spec left(any()) :: Left.t(any())
  def left(value), do: Left.pure(value)

  @doc """
  Alias for `right/1`.

  ## Examples

      iex> Funx.Monad.Either.pure(2)
      %Funx.Monad.Either.Right{right: 2}
  """
  @spec pure(any()) :: Right.t(any())
  def pure(value), do: right(value)

  @doc """
  Returns `true` if the `Either` is a `Left` value.

  ## Examples

      iex> Funx.Monad.Either.left?(Funx.Monad.Either.left("error"))
      true

      iex> Funx.Monad.Either.left?(Funx.Monad.Either.right(5))
      false
  """
  @spec left?(t(any(), any())) :: boolean()
  def left?(%Left{}), do: true
  def left?(_), do: false

  @doc """
  Returns `true` if the `Either` is a `Right` value.

  ## Examples

      iex> Funx.Monad.Either.right?(Funx.Monad.Either.right(5))
      true

      iex> Funx.Monad.Either.right?(Funx.Monad.Either.left("error"))
      false
  """
  @spec right?(t(any(), any())) :: boolean()
  def right?(%Right{}), do: true
  def right?(_), do: false

  @doc """
  Filters the value inside a `Right` using the given `predicate`. If the predicate returns `false`,
  a `Left` is returned using the `left_func`.

  ## Examples

      iex> Funx.Monad.Either.filter_or_else(Funx.Monad.Either.right(5), fn x -> x > 3 end, fn -> "error" end)
      %Funx.Monad.Either.Right{right: 5}

      iex> Funx.Monad.Either.filter_or_else(Funx.Monad.Either.right(2), fn x -> x > 3 end, fn -> "error" end)
      %Funx.Monad.Either.Left{left: "error"}
  """
  @spec filter_or_else(t(any(), any()), (any() -> boolean()), (-> any())) :: t(any(), any())
  def filter_or_else(either, predicate, left_func) do
    fold_l(
      either,
      fn value ->
        if predicate.(value) do
          either
        else
          left(left_func.())
        end
      end,
      fn _left_value -> either end
    )
  end

  @doc """
  Retrieves the value from a `Right`, returning the `default` value if `Left`.

  ## Examples

      iex> Funx.Monad.Either.get_or_else(Funx.Monad.Either.right(5), 0)
      5

      iex> Funx.Monad.Either.get_or_else(Funx.Monad.Either.left("error"), 0)
      0
  """
  @spec get_or_else(t(any(), any()), any()) :: any()
  def get_or_else(either, default) do
    fold_l(
      either,
      fn value -> value end,
      fn _left_value -> default end
    )
  end

  @doc """
  Returns the current `Right` value or invokes the `fallback_fun` if `Left`.

  Useful for recovering from a failure by providing an alternate computation.

  ## Examples

      iex> Funx.Monad.Either.or_else(Funx.Monad.Either.left("error"), fn -> Funx.Monad.Either.right(42) end)
      %Funx.Monad.Either.Right{right: 42}

      iex> Funx.Monad.Either.or_else(Funx.Monad.Either.right(10), fn -> Funx.Monad.Either.right(42) end)
      %Funx.Monad.Either.Right{right: 10}
  """
  @spec or_else(t(error, value), (-> t(error, value))) :: t(error, value)
        when error: term(), value: term()
  def or_else(%Left{}, fallback_fun) when is_function(fallback_fun, 0), do: fallback_fun.()
  def or_else(%Right{} = right, _fallback_fun), do: right

  @doc """
  Swaps the `Left` and `Right` branches of the `Either`.

  Turns a `Left` into a `Right` and vice versa, preserving the contained term.

  ## Examples

      iex> Funx.Monad.Either.flip(Funx.Monad.Either.left(:error))
      %Funx.Monad.Either.Right{right: :error}

      iex> Funx.Monad.Either.flip(Funx.Monad.Either.right(42))
      %Funx.Monad.Either.Left{left: 42}
  """
  @spec flip(t(left, right)) :: t(right, left)
        when left: term(), right: term()
  def flip(%Left{left: l}), do: %Right{right: l}
  def flip(%Right{right: r}), do: %Left{left: r}

  @doc """
  Lifts an equality function to compare `Either` values:
    - `Right` vs `Right`: Uses the custom equality function.
    - `Left` vs `Left`: Uses the custom equality function.
    - `Left` vs `Right` or vice versa: Always `false`.

  ## Examples

      iex> eq = Funx.Monad.Either.lift_eq(%{
      ...>   eq?: fn x, y -> x == y end,
      ...>   not_eq?: fn x, y -> x != y end
      ...> })
      iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.right(5))
      true
      iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.right(10))
      false
      iex> eq.eq?.(Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:a))
      true
      iex> eq.eq?.(Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:b))
      false
      iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.left(:a))
      false
  """
  @spec lift_eq(Eq.Utils.eq_t()) :: Eq.Utils.eq_map()
  def lift_eq(custom_eq) do
    custom_eq = Eq.Utils.to_eq_map(custom_eq)

    %{
      eq?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_eq.eq?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_eq.eq?.(v1, v2)
        %Left{}, %Right{} -> false
        %Right{}, %Left{} -> false
      end,
      not_eq?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_eq.not_eq?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_eq.not_eq?.(v1, v2)
        %Left{}, %Right{} -> true
        %Right{}, %Left{} -> true
      end
    }
  end

  @doc """
  Creates a custom ordering function for `Either` values using the provided `custom_ord`.

  The `custom_ord` must be a map with `:lt?`, `:le?`, `:gt?`, and `:ge?` functions. These are used to compare the internal `left` or `right` values.

  ## Examples

      iex> ord = Funx.Monad.Either.lift_ord(%{
      ...>   lt?: fn x, y -> x < y end,
      ...>   le?: fn x, y -> x <= y end,
      ...>   gt?: fn x, y -> x > y end,
      ...>   ge?: fn x, y -> x >= y end
      ...> })
      iex> ord.lt?.(Funx.Monad.Either.right(3), Funx.Monad.Either.right(5))
      true
      iex> ord.lt?.(Funx.Monad.Either.left(3), Funx.Monad.Either.right(5))
      true
      iex> ord.lt?.(Funx.Monad.Either.right(3), Funx.Monad.Either.left(5))
      false
      iex> ord.lt?.(Funx.Monad.Either.left(3), Funx.Monad.Either.left(5))
      true
  """
  @spec lift_ord(Ord.Utils.ord_t()) :: Ord.Utils.ord_map()
  def lift_ord(custom_ord) do
    custom_ord = Ord.Utils.to_ord_map(custom_ord)

    %{
      lt?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_ord.lt?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_ord.lt?.(v1, v2)
        %Left{}, %Right{} -> true
        %Right{}, %Left{} -> false
      end,
      le?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_ord.le?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_ord.le?.(v1, v2)
        %Left{}, %Right{} -> true
        %Right{}, %Left{} -> false
      end,
      gt?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_ord.gt?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_ord.gt?.(v1, v2)
        %Right{}, %Left{} -> true
        %Left{}, %Right{} -> false
      end,
      ge?: fn
        %Right{right: v1}, %Right{right: v2} -> custom_ord.ge?.(v1, v2)
        %Left{left: v1}, %Left{left: v2} -> custom_ord.ge?.(v1, v2)
        %Right{}, %Left{} -> true
        %Left{}, %Right{} -> false
      end
    }
  end

  @doc """
  Transforms the `Left` value using the given function if the `Either` is a `Left`.
  If the value is `Right`, it is returned unchanged.

  ## Examples

      iex> Funx.Monad.Either.map_left(Funx.Monad.Either.left("error"), fn e -> "wrapped: " <> e end)
      %Funx.Monad.Either.Left{left: "wrapped: error"}

      iex> Funx.Monad.Either.map_left(Funx.Monad.Either.right(42), fn _ -> "ignored" end)
      %Funx.Monad.Either.Right{right: 42}
  """
  @spec map_left(t(error, value), (error -> new_error)) :: t(new_error, value)
        when error: term(), new_error: term(), value: term()
  def map_left(%Left{left: error}, func) when is_function(func, 1), do: left(func.(error))
  def map_left(%Right{} = right, _func), do: right

  @doc """
  Removes `Left` values from a list of `Either` and returns a list of unwrapped `Right` values.

  Useful for discarding failed computations while keeping successful results.

  ## Examples

      iex> Funx.Monad.Either.concat([Funx.Monad.Either.right(1), Funx.Monad.Either.left(:error), Funx.Monad.Either.right(2)])
      [1, 2]

      iex> Funx.Monad.Either.concat([Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:b)])
      []

      iex> Funx.Monad.Either.concat([Funx.Monad.Either.right("a"), Funx.Monad.Either.right("b"), Funx.Monad.Either.right("c")])
      ["a", "b", "c"]
  """
  @spec concat([t(error, value)]) :: [value]
        when error: term(), value: any()
  def concat(list) when is_list(list) do
    list
    |> fold_l([], fn
      %Right{right: value}, acc -> [value | acc]
      %Left{}, acc -> acc
    end)
    |> :lists.reverse()
  end

  @doc """
  Applies the given function to each element in the list and collects the `Right` results, discarding any `Left`.

  This is useful when mapping a function that may fail and you only want the successful results.

  ## Examples

      iex> Funx.Monad.Either.concat_map([1, 2, 3], fn x -> if rem(x, 2) == 1, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left(:even) end)
      [1, 3]

      iex> Funx.Monad.Either.concat_map([2, 4], fn x -> if x > 3, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left(:too_small) end)
      [4]

      iex> Funx.Monad.Either.concat_map([], fn _ -> Funx.Monad.Either.left(:none) end)
      []
  """
  @spec concat_map([input], (input -> t(error, output))) :: [output]
        when input: any(), output: any(), error: any()
  def concat_map(list, func) when is_list(list) and is_function(func, 1) do
    fold_l(list, [], fn item, acc ->
      case func.(item) do
        %Right{right: value} -> [value | acc]
        %Left{} -> acc
      end
    end)
    |> :lists.reverse()
  end

  @doc """
  Sequences a list of `Either` values into an `Either` of a list.

  ## Examples

      iex> Funx.Monad.Either.sequence([Funx.Monad.Either.right(1), Funx.Monad.Either.right(2)])
      %Funx.Monad.Either.Right{right: [1, 2]}

      iex> Funx.Monad.Either.sequence([Funx.Monad.Either.right(1), Funx.Monad.Either.left("error")])
      %Funx.Monad.Either.Left{left: "error"}
  """
  @spec sequence([t(error, value)]) :: t(error, [value]) when error: term(), value: term()
  def sequence(list) when is_list(list), do: traverse(list, fn x -> x end)

  @doc """
  Traverses a list, applying the given function to each element and collecting the results in a single `Right`, or short-circuiting with the first `Left`.

  This is useful for validating or transforming a list of values where each step may fail.

  ## Examples

      iex> Funx.Monad.Either.traverse([1, 2, 3], &Funx.Monad.Either.right/1)
      %Funx.Monad.Either.Right{right: [1, 2, 3]}

      iex> Funx.Monad.Either.traverse([1, -2, 3], fn x -> if x > 0, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left("error") end)
      %Funx.Monad.Either.Left{left: "error"}
  """

  @spec traverse([a], (a -> t(error, b))) :: t(error, [b])
        when a: term(), b: term(), error: term()

  def traverse([], _func), do: pure([])

  def traverse(list, func) when is_list(list) and is_function(func, 1) do
    list
    |> Enum.reduce_while(pure([]), fn item, %Right{right: acc} ->
      case func.(item) do
        %Right{right: value} -> {:cont, pure([value | acc])}
        %Left{} = left -> {:halt, left}
      end
    end)
    |> map(&:lists.reverse/1)
  end

  @doc """
  Sequences a list of `Either` values, collecting all errors from `Left` values, rather than short-circuiting.

  ## Examples

      iex> Funx.Monad.Either.sequence_a([Funx.Monad.Either.right(1), Funx.Monad.Either.left("error"), Funx.Monad.Either.left("another error")])
      %Funx.Monad.Either.Left{left: ["error", "another error"]}
  """
  @spec sequence_a([t(error, value)]) :: t([error], [value])
        when error: term(), value: term()

  def sequence_a(list) when is_list(list), do: traverse_a(list, fn x -> x end)

  @doc """
  Traverses a list, applying the given function to each element and collecting the results in a single `Right`.

  Unlike `traverse/2`, this version accumulates all `Left` values rather than stopping at the first failure.
  It is useful for validations where you want to gather all errors at once.

  ## Examples

      iex> validate = fn x -> Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "must be positive: \#{v}" end) end
      iex> Funx.Monad.Either.traverse_a([1, 2, 3], validate)
      %Funx.Monad.Either.Right{right: [1, 2, 3]}
      iex> Funx.Monad.Either.traverse_a([1, -2, -3], validate)
      %Funx.Monad.Either.Left{left: ["must be positive: -2", "must be positive: -3"]}
  """
  @spec traverse_a([a], (a -> t([e], b))) :: t([e], [b])
        when a: term(), b: term(), e: term()
  def traverse_a([], _func), do: right([])

  def traverse_a(list, func) when is_list(list) and is_function(func, 1) do
    fold_l(list, right([]), fn item, acc_result ->
      case {func.(item), acc_result} do
        {%Right{right: value}, %Right{right: acc}} ->
          right([value | acc])

        {%Left{left: new}, %Left{left: existing}} ->
          left(append(existing, coerce(new)))

        {%Right{}, %Left{left: existing}} ->
          left(existing)

        {%Left{left: err}, %Right{}} ->
          left(coerce(err))
      end
    end)
    |> map(&:lists.reverse/1)
  end

  @doc """
  Traverses a list, applying the given function to each element, and collects the successful `Just` results into a single `Right`.

  The given function must return an `Either` of `Maybe`. `Right(Just x)` values are kept; `Right(Nothing)` values are filtered out.
  If any application returns `Left`, all `Left` values are accumulated.

  This is useful for effectful filtering, where you want to validate or transform elements and conditionally keep them, while still reporting all errors.

  ## Examples

      iex> filter_positive = fn x ->
      ...>   Funx.Monad.Either.lift_predicate(x, &is_integer/1, fn v -> "not an integer: \#{inspect(v)}" end)
      ...>   |> Funx.Monad.map(fn x -> if x > 0, do: Funx.Monad.Maybe.just(x), else: Funx.Monad.Maybe.nothing() end)
      ...> end
      iex> Funx.Monad.Either.wither_a([1, -2, 3], filter_positive)
      %Funx.Monad.Either.Right{right: [1, 3]}
      iex> Funx.Monad.Either.wither_a(["oops", -2], filter_positive)
      %Funx.Monad.Either.Left{left: ["not an integer: \\"oops\\""]}
  """

  @spec wither_a([a], (a -> t([e], Maybe.t(b)))) :: t([e], [b])
        when a: term(), b: term(), e: term()
  def wither_a([], _func), do: right([])

  def wither_a(list, func) when is_list(list) and is_function(func, 1) do
    fold_l(list, right([]), fn item, acc_result ->
      case {func.(item), acc_result} do
        {%Right{right: %Just{value: value}}, %Right{right: acc}} ->
          right([value | acc])

        {%Right{right: %Nothing{}}, %Right{right: acc}} ->
          right(acc)

        {%Left{left: new}, %Left{left: existing}} ->
          left(append(existing, coerce(new)))

        {%Right{}, %Left{left: existing}} ->
          left(existing)

        {%Left{left: err}, %Right{}} ->
          left(coerce(err))
      end
    end)
    |> map(&:lists.reverse/1)
  end

  @doc """
  Validates a value using a list of validator functions. Each validator returns an `Either.Right` if
  the check passes, or an `Either.Left` with an error message if it fails. If any validation fails,
  all errors are aggregated and returned in a single `Left`.

  ### Flat list aggregation

  When using the default aggregation strategy, errors 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` and a corresponding
  `Funx.Semigroup` implementation to accumulate errors into a single structure:

  ```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"]
  #     }
  #   }
  ```
  """

  @spec validate(value, [(value -> t(error, any))]) :: t([error], value)
        when error: term(), value: term()

  def validate(value, validators) when is_list(validators) do
    traverse_a(validators, fn validator -> validator.(value) end)
    |> map(fn _ -> value end)
  end

  def validate(value, validator) when is_function(validator, 1) do
    validate(value, [validator])
  end

  @doc """
  Converts a `Maybe` value to an `Either`. If the `Maybe` is `Nothing`, a `Left` is returned using `on_none`.

  ## Examples

      iex> Funx.Monad.Either.lift_maybe(Funx.Monad.Maybe.just(5), fn -> "error" end)
      %Funx.Monad.Either.Right{right: 5}

      iex> Funx.Monad.Either.lift_maybe(Funx.Monad.Maybe.nothing(), fn -> "error" end)
      %Funx.Monad.Either.Left{left: "error"}
  """
  @spec lift_maybe(Maybe.t(any()), (-> any())) :: t(any(), any())
  def lift_maybe(maybe, on_none) when is_struct(maybe, Just) or is_struct(maybe, Nothing) do
    maybe
    |> fold_l(
      fn value -> right(value) end,
      fn -> left(on_none.()) end
    )
  end

  @doc """
  Lifts a value into an `Either` based on the result of a predicate.

  Returns `Right(value)` if the predicate returns `true`, or `Left(on_false.(value))` if it returns `false`.

  This allows you to wrap a conditional check in a functional context with a custom error message.

  ## Examples

      iex> Funx.Monad.Either.lift_predicate(5, fn x -> x > 3 end, fn x -> "\#{x} is too small" end)
      %Funx.Monad.Either.Right{right: 5}

      iex> Funx.Monad.Either.lift_predicate(2, fn x -> x > 3 end, fn x -> "\#{x} is too small" end)
      %Funx.Monad.Either.Left{left: "2 is too small"}
  """

  @spec lift_predicate(value, (value -> boolean), (value -> error)) :: t(error, value)
        when value: term(), error: term()
  def lift_predicate(value, predicate, on_false)
      when is_function(predicate, 1) and is_function(on_false, 1) do
    fold_l(
      fn -> predicate.(value) end,
      fn -> right(value) end,
      fn -> left(on_false.(value)) end
    )
  end

  @doc """
  Converts a result (`{:ok, _}` or `{:error, _}`) to an `Either`.

  ## Examples

      iex> Funx.Monad.Either.from_result({:ok, 5})
      %Funx.Monad.Either.Right{right: 5}

      iex> Funx.Monad.Either.from_result({:error, "error"})
      %Funx.Monad.Either.Left{left: "error"}
  """
  @spec from_result({:ok, right} | {:error, left}) :: t(left, right)
        when left: term(), right: term()
  def from_result({:ok, value}), do: right(value)
  def from_result({:error, reason}), do: left(reason)

  @doc """
  Converts an `Either` to a result (`{:ok, value}` or `{:error, reason}`).

  ## Examples

      iex> Funx.Monad.Either.to_result(Funx.Monad.Either.right(5))
      {:ok, 5}

      iex> Funx.Monad.Either.to_result(Funx.Monad.Either.left("error"))
      {:error, "error"}
  """
  @spec to_result(t(left, right)) :: {:ok, right} | {:error, left}
        when left: term(), right: term()
  def to_result(either) when is_struct(either, Right) or is_struct(either, Left) do
    case either do
      %Right{right: value} -> {:ok, value}
      %Left{left: reason} -> {:error, reason}
    end
  end

  @doc """
  Wraps a value in an `Either`, catching any exceptions. If an exception occurs, a `Left` is returned with the exception.

  ## Examples

      iex> Funx.Monad.Either.from_try(fn -> 5 end)
      %Funx.Monad.Either.Right{right: 5}

      iex> Funx.Monad.Either.from_try(fn -> raise "error" end)
      %Funx.Monad.Either.Left{left: %RuntimeError{message: "error"}}
  """
  @spec from_try((-> right)) :: t(Exception.t(), right) when right: term()
  def from_try(func) do
    try do
      result = func.()
      right(result)
    rescue
      exception ->
        left(exception)
    end
  end

  @doc """
  Converts an `Either` to its inner value, raising an exception if it is `Left`.

  If the `Left` holds an exception struct, it is raised directly. If it holds a string or list of errors, they are converted into a `RuntimeError`. Unexpected types are inspected and raised as a `RuntimeError`.

  ## Examples

      iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.right(5))
      5

      iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left("error"))
      ** (RuntimeError) error

      iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left(["error 1", "error 2"]))
      ** (RuntimeError) error 1, error 2

      iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left(%ArgumentError{message: "bad argument"}))
      ** (ArgumentError) bad argument
  """

  @spec to_try!(t(left, right)) :: right | no_return
        when left: term(), right: term()
  def to_try!(%Right{right: value}), do: value

  def to_try!(%Left{left: reason}) do
    raise normalize_reason(reason)
  end

  defp normalize_reason(%_{} = exception), do: exception

  defp normalize_reason(reason) when is_list(reason),
    do: Enum.map_join(reason, ", ", &to_string/1)

  defp normalize_reason(reason) when is_binary(reason), do: reason

  defp normalize_reason(reason), do: "Unexpected error: #{inspect(reason)}"
end
