#!/usr/bin/env python

import inspect

import ray
from ray.util.annotations import _is_annotated

IGNORE_PATHS = {
    ".impl.",
    ".backend.",
    ".experimental.",
    ".internal.",
    ".generated.",
    ".test_utils.",
    ".annotations.",
    ".deprecation.",
    ".protobuf.",
    ".cloudpickle.",
}


def _fullname(attr):
    """Fully qualified name of an attribute."""
    fullname = ""
    try:
        if hasattr(attr, "__module__"):
            fullname += attr.__module__
        if hasattr(attr, "__name__"):
            if fullname:
                fullname += "."
            fullname += attr.__name__
        if not fullname:
            fullname = str(attr)
    except Exception as e:
        print("Error qualifying", e)
    return fullname


def _ignore(attr, extra_ignore):
    """Whether an attr should be ignored from annotation checking."""
    attr = _fullname(attr)
    if "ray." not in attr or "._" in attr:
        return True
    for path in IGNORE_PATHS:
        if path in attr:
            return True
    for path in extra_ignore or []:
        if path in attr:
            return True
    return False


def verify(symbol, scanned, ok, output, prefix=None, ignore=None):
    """Recursively verify all child symbols of a given module."""
    if not prefix:
        prefix = symbol.__name__ + "."
    if symbol in scanned:
        return
    scanned.add(symbol)
    for child in dir(symbol):
        if child.startswith("_"):
            continue
        attr = getattr(symbol, child)
        if _ignore(attr, ignore):
            continue
        if (inspect.isclass(attr) or inspect.isfunction(attr)) and prefix in _fullname(
            attr
        ):
            print("Scanning class", attr)
            if _is_annotated(attr):
                if attr not in scanned:
                    print("OK:", _fullname(attr))
                    ok.add(attr)
            else:
                output.add(attr)
            scanned.add(attr)
        elif inspect.ismodule(attr):
            print("Scanning module", attr)
            verify(attr, scanned, ok, output, prefix, ignore)
        else:
            print("Not scanning", attr, type(attr))


if __name__ == "__main__":
    import ray.data
    import ray.rllib
    import ray.serve
    import ray.train
    import ray.tune
    import ray.workflow

    output = set()
    ok = set()
    verify(ray.data, set(), ok, output)
    # Sanity check the lint logic.
    assert len(ok) >= 60, len(ok)

    verify(ray.rllib, set(), ok, output)
    verify(ray.air, set(), ok, output)
    verify(ray.train, set(), ok, output)
    verify(ray.tune, set(), ok, output)
    verify(
        ray,
        set(),
        ok,
        output,
        ignore=["ray.workflow", "ray.tune", "ray.serve"],
    )
    verify(ray.serve, set(), ok, output)
    assert len(ok) >= 500, len(ok)
    # TODO(ekl) enable it for all modules.
    #    verify(ray.tune, set(), ok, output)
    #    verify(ray.workflow, set(), ok, output)

    print("Num ok", len(ok))
    print("Num bad", len(output))
    print("!!! No API stability annotation found for:")
    for x in sorted([_fullname(x) for x in output]):
        print(x)
    if output:
        exit(1)
