# ruff: noqa: INP001

from __future__ import annotations

import ast
import importlib
import importlib.util
import inspect
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any, get_args, get_origin

from pydantic import BaseModel

REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = REPO_ROOT / "src"
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))
if str(SRC_ROOT) not in sys.path:
    sys.path.insert(0, str(SRC_ROOT))

MODEL_MODULES = (
    "polymarket_apis.types.clob_types",
    "polymarket_apis.types.common",
    "polymarket_apis.types.data_types",
    "polymarket_apis.types.gamma_types",
    "polymarket_apis.types.web3_types",
    "polymarket_apis.types.websockets_types",
)
TEST_MODULE = "tests.prod_read.test_schema_contracts"
TEST_FILE = REPO_ROOT / "tests" / "prod_read" / "test_schema_contracts.py"


def iter_models_in_annotation(annotation: Any) -> set[type[BaseModel]]:
    models: set[type[BaseModel]] = set()

    if isinstance(annotation, type) and issubclass(annotation, BaseModel):
        models.add(annotation)
        return models

    origin = get_origin(annotation)
    if origin is None:
        return models

    for arg in get_args(annotation):
        models.update(iter_models_in_annotation(arg))

    return models


def walk_nested_models(
    seeds: set[type[BaseModel]],
) -> set[type[BaseModel]]:
    covered: set[type[BaseModel]] = set()
    pending = list(seeds)

    while pending:
        model = pending.pop()
        if model in covered:
            continue

        covered.add(model)
        model.model_rebuild(force=True)

        for field in model.model_fields.values():
            for nested in iter_models_in_annotation(field.annotation):
                if nested not in covered:
                    pending.append(nested)

    return covered


def load_model_universe() -> tuple[
    dict[str, list[tuple[str, type[BaseModel]]]],
    dict[type[BaseModel], str],
]:
    models_by_module: dict[str, list[tuple[str, type[BaseModel]]]] = {}
    canonical_names: dict[type[BaseModel], str] = {}

    for module_name in MODEL_MODULES:
        module = importlib.import_module(module_name)
        module_source = Path(module.__file__).read_text()
        module_tree = ast.parse(module_source)
        declared_names: set[str] = set()

        for node in module_tree.body:
            if isinstance(node, ast.ClassDef):
                declared_names.add(node.name)
            if (
                isinstance(node, ast.Assign)
                and isinstance(node.value, ast.Subscript)
                and isinstance(node.value.value, ast.Name)
                and node.value.value.id == "RootModel"
            ):
                for target in node.targets:
                    if isinstance(target, ast.Name):
                        declared_names.add(target.id)

        items: list[tuple[str, type[BaseModel]]] = []

        for name, obj in sorted(vars(module).items()):
            if not isinstance(obj, type) or not issubclass(obj, BaseModel):
                continue
            if name.startswith("_"):
                continue
            if name not in declared_names:
                continue

            items.append((name, obj))
            canonical_names.setdefault(obj, f"{module_name}.{name}")

        models_by_module[module_name] = items

    return models_by_module, canonical_names


def load_tested_root_models() -> set[type[BaseModel]]:
    spec = importlib.util.spec_from_file_location(TEST_MODULE, TEST_FILE)
    if spec is None or spec.loader is None:
        message = f"Could not load test module from {TEST_FILE}"
        raise RuntimeError(message)

    test_module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(test_module)
    source = inspect.getsource(test_module)
    tree = ast.parse(source)
    roots: set[type[BaseModel]] = set()

    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue
        if not isinstance(node.func, ast.Name) or node.func.id != "assert_api_contract":
            continue
        if len(node.args) < 2:
            continue

        type_expr = node.args[1]
        resolved = eval(  # noqa: S307
            compile(ast.Expression(type_expr), filename=TEST_MODULE, mode="eval"),
            test_module.__dict__,
        )
        roots.update(iter_models_in_annotation(resolved))

    return roots


def format_model_list(
    models: list[type[BaseModel]],
    canonical_names: dict[type[BaseModel], str],
) -> list[str]:
    return [f"- `{canonical_names[model]}`" for model in models]


def main() -> None:
    models_by_module, canonical_names = load_model_universe()
    all_models = {
        model
        for entries in models_by_module.values()
        for _, model in entries
    }
    tested_roots = load_tested_root_models()
    transitively_covered = walk_nested_models(tested_roots)
    untested = all_models - transitively_covered

    direct_by_module: dict[str, list[type[BaseModel]]] = defaultdict(list)
    covered_by_module: dict[str, list[type[BaseModel]]] = defaultdict(list)
    untested_by_module: dict[str, list[type[BaseModel]]] = defaultdict(list)

    for module_name, entries in models_by_module.items():
        for _, model in entries:
            if model in tested_roots:
                direct_by_module[module_name].append(model)
            if model in transitively_covered:
                covered_by_module[module_name].append(model)
            else:
                untested_by_module[module_name].append(model)

    print("# Model Coverage Report")
    print()
    print(f"- Universe: `{len(all_models)}` Pydantic models")
    print(f"- Directly tested roots: `{len(tested_roots)}`")
    print(f"- Transitively covered through current prod-read tests: `{len(transitively_covered)}`")
    print(f"- Not covered by current prod-read tests: `{len(untested)}`")
    print()

    print("## Directly Tested Roots")
    print()
    for line in format_model_list(
        sorted(tested_roots, key=lambda model: canonical_names[model]),
        canonical_names,
    ):
        print(line)
    print()

    print("## Coverage By Module")
    print()
    for module_name in MODEL_MODULES:
        module_models = [model for _, model in models_by_module[module_name]]
        direct = sorted(direct_by_module[module_name], key=lambda model: canonical_names[model])
        covered = sorted(
            covered_by_module[module_name], key=lambda model: canonical_names[model]
        )
        missing = sorted(
            untested_by_module[module_name], key=lambda model: canonical_names[model]
        )

        print(f"### `{module_name}`")
        print()
        print(
            f"- Universe: `{len(module_models)}`"
            f" | Direct roots: `{len(direct)}`"
            f" | Transitively covered: `{len(covered)}`"
            f" | Not covered: `{len(missing)}`"
        )
        if missing:
            print("- Missing:")
            for line in format_model_list(missing, canonical_names):
                print(f"  {line}")
        print()

    print("## Full Universe")
    print()
    for module_name in MODEL_MODULES:
        print(f"### `{module_name}`")
        print()
        for _, model in models_by_module[module_name]:
            if model in tested_roots:
                status = "direct"
            elif model in transitively_covered:
                status = "transitive"
            else:
                status = "untested"
            print(f"- `{canonical_names[model]}`: `{status}`")
        print()


if __name__ == "__main__":
    main()
