# https://docs.astral.sh/ruff/rules/future-rewritable-type-annotation/
#
# 2024-05-02(bwarsaw): We must ignore I001 on this line or the ruff formatter
# and linter will be in conflict between I001 and F404 (which wants to move
# this import to below `import sys`.  Ruff's unified linter and formatter
# will hopefully resolve this: https://github.com/astral-sh/ruff/issues/8232
from __future__ import annotations  # noqa: I001

import sys

from typing import Any, TYPE_CHECKING, overload

if TYPE_CHECKING:
    from .types import ModuleAware


# fmt: off
@overload
def public(thing: ModuleAware) -> ModuleAware:
    ...


@overload
def public(**kws: Any) -> Any | tuple[Any]:
    ...


# fmt: on
def public(
    thing: Any | None = None, **kws: Any
) -> ModuleAware | Any | tuple[Any]:
    """Add a name or names to __all__.

    There are two forms of use for this function.  Most commonly it will
    be used as a decorator on a class or function at module scope.  In
    this case, ``thing`` will be an object with both ``__module__`` and
    ``__name__`` attributes, and the name is added to the module's
    ``__all__`` list, creating that if necessary.

    When used in its function call form, ``thing`` will be None.  ``__all__``
    is looked up in the globals at the function's call site, and each key in
    the keyword arguments is added to the ``__all__``.  In addition, the key
    will be bound to the value in the globals.  This form returns the keyword
    argument values in order.  If only a single keyword argument is given, its
    value is return, otherwise a tuple of the values is returned.

    Only one or the other format may be used.

    :param thing: None, or an object with both a __module__ and a __name__
        argument.
    :param kws: Keyword arguments.
    :return: In the decorator form, the original ``thing`` object is
        returned.  In the functional form, the keyword argument value is
        returned if only a single keyword argument is given, otherwise a
        tuple of the keyword argument values is returned.
    :raises ValueError: When the inputs are invalid, or this function finds
        a non-list ``__all__`` attribute.
    """
    # 2020-07-14(warsaw): I considered using inspect.getmodule() here but
    # looking at its implementation, I feel like it does a ton of unnecessary
    # work in the oddball cases (i.e. where the object does not have an
    # __module__ attribute).  Because @public runs at module import time, and
    # because I'm not really sure we even want to support those oddball cases,
    # I'm taking the more straightforward approach of just looking the module
    # up in sys.modules.  That should be good enough for our purposes.
    mdict = (
        # The function call syntax.
        sys._getframe(1).f_globals
        if thing is None
        # The decorator syntax.
        else sys.modules[thing.__module__].__dict__
    )
    dunder_all = mdict.setdefault('__all__', [])
    if not isinstance(dunder_all, list):
        # https://docs.astral.sh/ruff/rules/f-string-in-exception/
        msg = f'__all__ must be a list not: {type(dunder_all)}'
        raise TypeError(msg)
    if thing is None:
        # The function call form.
        retval = []
        for key, value in kws.items():
            # This overwrites any previous similarly named __all__ entry.
            if key not in dunder_all:
                dunder_all.append(key)
            # We currently do not check for duplications in the globals.
            mdict[key] = value
            retval.append(value)
        if len(retval) == 1:
            return retval[0]
        return tuple(retval)
    # I think it's impossible to use the @public decorator and pass in keyword
    # arguments.  Not quite syntactically impossible, but you'll get a
    # TypeError if you try it, before you even get to this code.
    assert (
        len(kws) == 0
    ), 'Keyword arguments are incompatible with use as decorator'
    if thing.__name__ not in dunder_all:
        dunder_all.append(thing.__name__)
    return thing
