from abc import ABC, abstractmethod
from typing import Iterator, Self, Callable, Any

from monads.functional_types import Predicate, Function, O
from .functional_types import Supplier
from .monad import Monad, I, O, U
from .option_monad import Option, Nothing


class Try(Monad[I], ABC):
    """
    Monad representing a computation that may either result in a value (Success)
    or an exception (Failure).

    Use `Try.of(supplier)` to safely execute a computation and capture exceptions.

    This is inspired by Scala's `Try` monad.
    """

    @staticmethod
    def of(supplier: Supplier[I]) -> "Try[I]":
        """
        Executes the given supplier function and wraps the result in a Try.

        - If the function executes without raising an exception, returns Success(value).
        - If an exception is raised, returns Failure(exception).

        Args:
            supplier: A no-argument callable that produces a value of type I.

        Returns:
            Try[I]: Success or Failure.
        """
        try:
            return Success(supplier())
        except Exception as e:
            return Failure(e)

    @property
    @abstractmethod
    def is_success(self) -> bool:
        """
        Returns True if this Try is a Success.
        """
        pass

    @property
    @abstractmethod
    def is_failure(self) -> bool:
        """
        Returns True if this Try is a Failure.

        """
        pass

    @abstractmethod
    def fold(self, default: Function[Exception, O], mapping_fn: Function[I, O]) -> O:
        """
        Extracts a value from this Try by applying one of two functions:

        - If Success, applies `mapping_fn` to the contained value.
        - If Failure, applies `default` to the exception.

        Args:
            default: Function to handle exceptions.
            mapping_fn: Function to handle the successful value.

        Returns:
            O: Result of either function.
        """
        pass

    @abstractmethod
    def to_option(self) -> Option[I]:
        """
        Converts this Try into an Option:

        - Success(value) -> Some(value)
        - Failure(_) -> Nothing

        Returns:
            Option[I]
        """
        pass


class Success(Try[I]):
    """
    Represents a successful computation containing a value.
    """

    def __init__(self, value: I):
        self._value: I = value

    @property
    def is_success(self) -> bool:
        return True

    @property
    def is_failure(self) -> bool:
        return False

    def for_each(self, fn: Function[I, U]) -> None:
        """
        Executes `fn` for side effects on the contained value.
        """
        fn(self._value)

    def map(self, mapping_fn: Function[I, O]) -> Try[O]:
        """
        Transforms the contained value using `mapping_fn`, returning a new Try.
        """
        return Try.of(lambda: mapping_fn(self._value))

    def flatten(self) -> Try[I]:
        """
        If the contained value is itself a Try, returns it to remove nesting.
        Otherwise, returns self.
        """
        return self._value if isinstance(self._value, Try) else self

    def flat_map(self, mapping_fn: Function[I, Try[O]]) -> Try[O]:
        """
        Applies a function returning a Try to the contained value, flattening the result.
        """
        try:
            return mapping_fn(self._value)
        except Exception as e:
            return Failure(e)

    def get_or_else(self, default: I) -> I:
        """
        Returns the contained value.
        """
        return self._value

    def or_else(self, default: Try[I]) -> Try[I]:
        """
        Returns self, ignoring the provided default.
        """
        return self

    def fold(self, default: Function[Exception, O], mapping_fn: Function[I, O]) -> O:
        """
        Applies `mapping_fn` to the contained value.
        """
        return mapping_fn(self._value)

    def filter(self, predicate: Predicate[I]) -> Try[I]:
        """
        Returns self if predicate(value) is True, otherwise Failure.
        """
        if predicate(self._value):
            return self
        else:
            return Failure(ValueError(f"Predicate does not hold for {self._value!r}"))

    def to_option(self) -> Option[I]:
        """
        Converts to Some(value).
        """
        return Option.of(self._value)

    def __iter__(self) -> Iterator[I]:
        """
        Iterates over the contained value.
        """
        yield self._value


class Failure(Try[I]):
    """
    Represents a failed computation containing an Exception.
    """

    def __init__(self, value: Exception):
        if not isinstance(value, Exception):
            raise ValueError('value must be an Exception')
        self._value: Exception = value

    @property
    def is_success(self) -> bool:
        return False

    @property
    def is_failure(self) -> bool:
        return True

    def for_each(self, fn: Function[I, U]) -> None:
        """
        Does nothing because there is no value.
        """
        return

    def map(self, mapping_fn: Function[I, O]) -> Try[O]:
        """
        Returns self without applying the mapping function.
        """
        return self

    def flat_map(self, mapping_fn: Function[I, Try[O]]) -> Try[O]:
        """
        Returns self without applying the flat mapping function.
        """
        return self

    def get_or_else(self, default: I) -> I:
        """
        Returns the provided default value.
        """
        return default

    def or_else(self, default: Try[I]) -> Try[I]:
        """
        Returns the provided fallback Try.
        """
        return default

    def fold(self, default: Function[Exception, O], mapping_fn: Function[I, O]) -> O:
        """
        Applies `default` to the contained exception.
        """
        return default(self._value)

    def filter(self, predicate: Predicate[I]) -> Try[I]:
        """
        Returns self unchanged because there is no value to filter.
        """
        return self

    def flatten(self) -> Try[I]:
        """
        Returns self unchanged.
        """
        return self

    def to_option(self) -> Option[I]:
        """
        Converts to Nothing.
        """
        return Nothing

    def __iter__(self) -> Iterator[I]:
        """
        Yields nothing because there is no value.
        """
        return iter([])


def to_try(fn: Callable[..., Any]) -> Callable[..., Try[Any]]:
    """
    Decorator that wraps function execution into a Try, capturing exceptions.

    Usage:

        @to_try
        def divide(a, b):
            return a / b

        divide(4, 2)  # Success(2.0)
        divide(1, 0)  # Failure(ZeroDivisionError)

    Args:
        fn: The function to wrap.

    Returns:
        A function that returns a Try wrapping the original function's result or exception.
    """
    def wrapper(*args, **kwargs) -> Try[Any]:
        return Try.of(lambda: fn(*args, **kwargs))
    return wrapper
