"""Result monad with Ok and Err variants for error handling."""

from __future__ import annotations

import sys
from abc import ABC, abstractmethod
from typing import (
    TYPE_CHECKING,
    Any,
    Generic,
    Literal,
    TypeVar,
)

if sys.version_info >= (3, 11):
    from typing import Never
else:
    from typing_extensions import Never

import attrs
from attrs.validators import instance_of

T_co = TypeVar("T_co", covariant=True)
E_co = TypeVar("E_co", bound=object, covariant=True)

if TYPE_CHECKING:
    from collections.abc import Callable
    from types import TracebackType

    if sys.version_info >= (3, 11):
        from typing import Self
    else:
        from typing_extensions import Self


@attrs.frozen
class Result(ABC, Generic[T_co, E_co]):
    """
    Result monad with *Ok* and *Err* variants.
    """

    @classmethod
    def unit(cls, inner: Any) -> Ok[Any]:
        """
        Wrap *inner* in an *Ok*.
        """
        return Ok(inner)

    @abstractmethod
    def is_ok(self) -> bool:
        """Return whether this result is Ok."""
        ...

    @abstractmethod
    def map(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Result[Any, Any]:
        """Apply *func* to the success value, if present."""
        ...

    @abstractmethod
    def map_err(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Result[Any, Any]:
        """Apply *func* to the error value, if present."""
        ...

    @abstractmethod
    def and_then(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Result[Any, Any]:
        """Apply *func* to the success value and flatten."""
        ...

    @abstractmethod
    def or_else(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Result[Any, Any]:
        """Apply *func* to the error value and flatten."""
        ...

    @abstractmethod
    def unwrap(self) -> T_co:
        """Return the success value or raise on error."""
        ...

    @staticmethod
    def result_is_ok(
        result: Result[Any, Any],
    ) -> bool:
        """
        Return whether *result* is an *Ok*.
        """
        return result.is_ok()

    @staticmethod
    def result_unwrap(
        result: Result[Any, Any],
    ) -> Any:
        """
        Unwrap *result* or raise if *Err*.
        """
        return result.unwrap()


@attrs.frozen
class Ok(Result[T_co, Never]):
    """
    Successful result wrapping a value.
    """

    inner: Any = attrs.field(default=None)

    def is_ok(self) -> Literal[True]:
        """
        Return *True*.
        """
        return True

    def map(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Ok[Any]:
        """
        Apply *func* to the wrapped value.
        """
        return Ok(func(self.inner, *args, **kwargs))

    def map_err(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Self:
        """
        No-op on *Ok*.
        """
        return self

    def and_then(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Any:
        """
        Apply *func* to the wrapped value and flatten.
        """
        return func(self.inner, *args, **kwargs)

    def or_else(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Self:
        """
        No-op on *Ok*.
        """
        return self

    def unwrap(self) -> T_co:
        """
        Return the wrapped value.
        """
        return self.inner  # type: ignore[no-any-return]


SafeArgs = tuple[tuple[Any, ...], dict[str, Any]]
SafeMethodArgs = tuple[object, tuple[Any, ...], dict[str, Any]]


@attrs.frozen(eq=False, hash=False)
class Err(Result[Never, E_co]):
    """
    Failed result wrapping an error.
    """

    error: Any = attrs.field(default=None)
    input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field(
        default=(),
        validator=instance_of(tuple),
        repr=False,
    )
    traceback: str = attrs.field(
        default="",
        validator=instance_of(str),
    )
    details: list[dict[str, Any]] = attrs.field(
        factory=list, init=False, repr=False
    )

    def __attrs_post_init__(self) -> None:
        """Extract traceback details from the wrapped exception."""
        if isinstance(self.error, Exception):
            object.__setattr__(
                self,
                "details",
                self._extract_details(self.error.__traceback__),
            )

    def _extract_details(
        self, tb: TracebackType | None
    ) -> list[dict[str, Any]]:
        """
        Walk *tb* and collect frame information.
        """
        trace_info: list[dict[str, Any]] = []
        while tb:
            frame = tb.tb_frame
            trace_info.append(
                {
                    "file": frame.f_code.co_filename,
                    "func": frame.f_code.co_name,
                    "line_no": tb.tb_lineno,
                    "locals": frame.f_locals,
                }
            )
            tb = tb.tb_next
        return trace_info

    def is_ok(self) -> Literal[False]:
        """
        Return *False*.
        """
        return False

    def map(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Self:
        """
        No-op on *Err*.
        """
        return self

    def map_err(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Err[Any]:
        """
        Apply *func* to the error value.
        """
        return Err(func(self.error, *args, **kwargs))

    def and_then(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Self:
        """
        No-op on *Err*.
        """
        return self

    def or_else(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any,
    ) -> Any:
        """
        Apply *func* to the error and flatten.
        """
        return func(self.error, *args, **kwargs)

    def unwrap(self) -> Never:
        """
        Raise the wrapped error, or *ValueError*.
        """
        if isinstance(self.error, Exception):
            raise self.error
        msg = f"Err does not have a caught error to raise: {self.error = }"
        raise ValueError(msg)

    def __eq__(self, other: object) -> bool:
        """Compare by error type, message, and input args."""
        if not isinstance(other, Err):
            return False
        return all(
            (
                type(self.error) is type(other.error),
                str(self.error) == str(other.error),
                self.input_args == other.input_args,
            )
        )

    def __hash__(self) -> int:
        """Hash by error type, message, and input args."""
        return hash(f"{type(self.error)}{self.error}{self.input_args}")
