from __future__ import annotations

from collections.abc import Callable
from functools import update_wrapper
from types import MethodType
from typing import (
    Any,
    Concatenate,
    Final,
    ParamSpec,
    TypeVar,
    cast,
    overload,
    override,
)

from unipercept.utils.missing import MissingValue

__all__ = ["immutable", "objectmagic"]

_P = ParamSpec("_P")
_T = TypeVar("_T", bound=object)
_R = TypeVar("_R", covariant=True)


class objectmagic[T: object, **P, R]:
    """
    A variant on classmethod that can only be called on instances of a class.
    Useful for wrapping magic methods while still preserving the ability to call such a method statically.
    """

    @property
    def __func__(self) -> Callable[Concatenate[_T, _P], _R]: ...

    def __init__(self, fn: Callable[Concatenate[_T, _P], _R]) -> None:
        self.fn = fn
        self.name = fn.__name__
        self.owner = None

        update_wrapper(self, fn)  # type: ignore

    def __set_name__(self, owner: type[_T], name: str) -> None:
        self.name = name
        self.owner = owner

    @overload
    def __get__(self, obj: None, *args, **kwargs) -> Callable[_P, _R]: ...

    @overload
    def __get__(
        self, obj: _T, *args, **kwargs
    ) -> Callable[Concatenate[_T, _P], _R]: ...

    def __get__(
        self, obj: _T | None, *args, **kwargs
    ) -> Callable[Concatenate[_T, _P], _R] | Callable[_P, _R]:
        if obj is None:
            return self.fn  # (obj_a, obj_b)
        method = MethodType(self.fn, obj)  # type: ignore
        return cast(Callable[P, R], method)  # (obj_b)


_V = TypeVar("_V", covariant=True)
_NA: Final = MissingValue("NA")


class immutable[V]:
    """A value that can only be set once and not changed afterwards."""

    def __init__(self, value: _V | MissingValue = _NA) -> None:
        self.value: _V = cast(V, value)
        self.name: str | None = None

    def __get__(self, obj: object, cls: type[object]) -> _V:
        res = self.value
        if _NA.is_value(res):
            return res
        msg = f"{self.name} is not set"
        raise AttributeError(msg)

    def __set_name__(self, owner: type[object], name: str) -> None:
        self.name = name

    def __set__(self, obj: object, value: Any, /) -> None:
        if self.name is not None:
            msg = f"{self.name} is read-only"
            raise AttributeError(msg)
        self.value = value

    @override
    def __str__(self) -> str:
        return str(self.value)

    @override
    def __repr__(self) -> str:
        return f"<immutable {type(self.value)} {self!s}>"


class private[V]:
    """
    A property that can only be accessed by the class that owns it. It is not accessible by subclasses.
    This is useful for properties that are only meant to be applicable by the class itself, like a version string.

    Note that this is not a security feature, and is not meant to be used as such.
    """

    value: _V | MissingValue
    owner: type | None
    name: str | None

    def __init__(
        self, value: _V | MissingValue = _NA, owner: type[object] | None = None
    ) -> None:
        self.value = value
        self.owner: type | None = owner
        self.name: str | None = repr(self)

    @override
    def __str__(self) -> str:
        res = []
        if self.owner is not None:
            res.append(f"{self.owner.__name__}")
        if self.name is not None:
            res.append(self.name)
        else:
            res.append("(unbound)")
        rep = ".".join(res)
        if self.value is not _NA:
            rep += "=" + repr(rep)
        return rep

    @override
    def __repr__(self) -> str:
        return f"<private property {self!s}>"

    def __get__(self, obj: object, cls: type[object]):
        if self.value in _NA:
            raise _NA.Error()
        return self.value

    def __set_name__(self, owner: type[object], name: str) -> None:
        self.name = name

    def __set__(self, obj: object, value: Any, /) -> None:
        if self.owner is not None:
            msg = f"{self.name} property is read-only"
            raise AttributeError(msg)
        self.value = value

    # __class_getitem__ = classmethod(types.GenericAlias)


class blocked:
    def __init__(self, fn: Callable[..., Any] | None = None) -> None:
        self.name = fn.__name__ if fn is not None else "method"
        self.owner = None

    def __set_name__(self, owner: type[object], name: str) -> None:
        self.name = name
        self.owner = owner

    def __get__(self, obj: object, cls: type[object]) -> Callable[..., Any]:
        if obj is None:
            return MethodType(self, cls)
        return MethodType(self, obj)

    def __call__(self, obj, *args, **kwargs):
        msg = f"{self.name} is blocked on {obj}"
        raise AttributeError(msg)
