"""
Recommended to import this module as follows:
import forcen_public_utils.checked as ch

Motivation:

Alternative to Exception-style error handling. Use this to return either a value, or an error,
but never both at the same time. Also ferries a human-readable string message.

When To Use:

- Use this utility wherever a function can fail to return the desired value.
- Set return types of functions to Checked, MaybeVal, MaybeErr or Bool

How to Return Values:

- when returning values for success:
    - return ch.ok(<value>, message="some message") // message is optional
    - return ch.ok(message="some message") // if value type is None

- when returning values for failure:
    - return ch.bad(<error>, message="some message") // message is optional
    - return ch.bad(message="some message") // if error type is None

- when forwarding error or value with different signature:
    - return ch.fwd_val(other) // throws if other.has_value() is False
    - return ch.fwd_err(other) // throws if other.has_error() is False

How to Use Values Safely:
- If you want to avoid exceptions, always check before accessing value

def DoSomeWork() -> ch.Bool:
    maybe_value = GetSomeValue()
    if maybe_value.has_error():
        return ch.fwd_err(maybe_value)

    do_something(maybe_value.value())

How to Use Values Without Error Checking:
- You can directly use values without checking if you're ok with exceptions

def DoSomeWork() -> ch.Bool:
    do_something(GetSomeValue().value()) # might raise CheckedBadAccess

Example:

def parse_json_file(filepath: Path) -> Checked[str, ParseError]:
    if not filepath.exists():
        return Checked.failed(ParseError.InvalidPath, "{filepath} does not exist")
    if not filepath.suffix != '.json':
        return Checked.failed(ParseError.InvalidPath, "{filepath} is not a json file")

    try:
        return Checked.ok(parse_json(filepath))
    except Exception as e:
        return Checked.failed(ParseError.FailedToParse, "{e}")

checked_json_str = parse_json_file("~/Documents/test.json")
if json_str.has_value():
    json_str = checked_json_str.value()
    ...
else:
    print("Failed with error: {checked_json_str.error()})
    ...


"""

import dataclasses
from typing import Any, Generic, Optional, TypeVar, Union, cast, overload

_Value = TypeVar("_Value")
_Error = TypeVar("_Error")

_Other_Value = TypeVar("_Other_Value")
_Other_Error = TypeVar("_Other_Error")


class CheckedBadAccess(RuntimeError):
    def __init__(
        self,
        checked: "Checked[_Value, _Error]",  # pyright: ignore [reportInvalidTypeVarUse]
        *args: Any,
        **kwargs: Any,
    ):
        super().__init__(checked, *args, **kwargs)
        self.checked = checked


@dataclasses.dataclass
class FullError(Generic[_Error]):
    error: _Error
    message: str

    def __str__(self) -> str:
        return f"error_code: [{self.error}]\nmessage: {self.message}"


@dataclasses.dataclass
class Checked(Generic[_Value, _Error]):
    """
    Analogous to std::expected (or boost::expected) from C++ or std::result from rust

    A `Checked[_Value, _Error]` contains either some meaningful data (_Value) or an error (_Error)
    but never both at the same time.

    If the incorrect type is accessed, an exception is thrown

    In general, the _Error type should only enumerate values which a client may use to make
    decisions. For generic human error output, carry this info through the `message` field instead
    """

    message: str
    _data: Union[_Value, _Error]
    _is_ok: bool

    def has_value(self) -> bool:
        return self._is_ok

    def has_error(self) -> bool:
        return not self._is_ok

    def __bool__(self) -> bool:
        return self.has_value()

    def value(self, exception: Optional[Exception] = None) -> _Value:
        if not self._is_ok:
            checked_exception = CheckedBadAccess(
                self, f"Checked[V, E] has error, but value access was attempted"
            )
            if exception is not None:
                raise checked_exception from exception
            raise checked_exception

        return cast(_Value, self._data)

    def value_or(self, default: _Other_Value) -> Union[_Value, _Other_Value]:
        if not self._is_ok:
            raise CheckedBadAccess(self, f"Checked[V, E] has error, but value access was attempted")

        return cast(_Value, self._data)

    def error(self) -> _Error:
        if self._is_ok:
            raise CheckedBadAccess(self, f"Checked[V, E] has value, but error access was attempted")

        return cast(_Error, self._data)

    def full_error(self) -> FullError[_Error]:
        return FullError(self.error(), self.message)


MaybeErr = Checked[None, _Error]
MaybeVal = Checked[_Value, None]
Bool = Checked[None, None]


# Type hints
@overload
def ok(
    value: _Value, /, *, message: str = ""
) -> Checked[_Value, _Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    ...


# Type hints
@overload
def ok(*, message: str = "") -> Checked[None, _Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    ...


# Note (swapnil):
# Some funky things are happening here:
#
# Goal:
#  - support callsite for Checked[None, E] to look like `ok()`
#  - support callsite for Checked[V, E] to look like `ok(V())`
#  - Provide good typehint support to callers
#  - Capture all incorrectluy typed args, and don't flag acceptable ones
#
# Python has no true overloading, so we can only have one real implementatio
# - Using `@overload`, we define 2 different stubs, satisfying the typehinting req
# - By making `value` default to None, we can support both types of calls
#
# By providing `_Error` in return sig, we eliminate `partially typed` errors at call-site
#   - This still generates the error here, which we're ignoring, because it's fine
#
# The cast is required to deal with `None` not being acceptable to _Value, even tho its fine
#
# There is probably a better way to type hint this, but the unit test should prove that this works
def ok(  # pyright: ignore [reportGeneralTypeIssues, reportInconsistentOverload]
    value: _Value = None, /, *, message: str = ""
) -> Checked[_Value, _Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    return Checked[_Value, _Error](message=message, _data=value, _is_ok=True)


_E = TypeVar("_E", contravariant=True)


@overload
def bad(
    error: _E, /, *, message: str = ""
) -> Checked[_Value, _E]:  # pyright: ignore [reportInvalidTypeVarUse]
    ...


@overload
def bad(*, message: str = "") -> Checked[_Value, None]:  # pyright: ignore [reportInvalidTypeVarUse]
    ...


# Note (swapnil):
# Same funky stuff is going on here as the `ok(...)` impls, so corresponding note
def bad(  # pyright: ignore [reportGeneralTypeIssues, reportInconsistentOverload]
    error: _Error = None, /, *, message: str = ""
) -> Checked[_Value, _Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    return Checked[_Value, _Error](message=message, _data=error, _is_ok=False)


def fwd_err(
    other: Checked[_Value, _Error],  # pyright: ignore [reportInvalidTypeVarUse]
    layered_message: Optional[str] = None,
) -> Checked[_Other_Value, _Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    if other.has_value():
        raise CheckedBadAccess(
            other, f"Checked[V, E] fwd_err was attempted but object contains value type"
        )
    message = other.message

    if layered_message is not None:
        message = f"{layered_message}\n{other.message}"

    return Checked[_Other_Value, _Error](
        message=message,
        _is_ok=False,
        _data=cast(_Error, other._data),  # pyright: ignore [reportPrivateUsage]
    )


def fwd_val(
    other: Checked[_Value, _Error],  # pyright: ignore [reportInvalidTypeVarUse]
    layered_message: Optional[str] = None,
) -> Checked[_Value, _Other_Error]:  # pyright: ignore [reportInvalidTypeVarUse]
    if other.has_error():
        raise CheckedBadAccess(
            other, f"Checked[V, E] fwd_val was attempted but object contains error type"
        )

    message = other.message
    if layered_message is not None:
        message = f"{layered_message}\n{other.message}"

    return Checked[_Value, _Other_Error](
        message=message,
        _is_ok=True,
        _data=cast(_Value, other._data),  # pyright: ignore [reportPrivateUsage]
    )
