"""Microbench for _cast_value_type and dump().

This benchmark is self-contained and does not import pydivkit (the generated
pydivkit.div package may be absent). Run it after significant changes to
measure current performance:

  python3 perf/bench_cast_value_type.py          # normal output
  python3 perf/bench_cast_value_type.py --check   # CI mode: fail on regression
"""

from __future__ import annotations

import enum
import sys
import time
from contextlib import suppress
from statistics import median
from typing import Any, Mapping, Sequence, Union, get_args, get_origin


TYPE_FIELD = "type"


class _Field:
    def __init__(self, default: Any = None) -> None:
        self.default = default


def inject_types(type_: Any) -> None:
    """Minimal copy of pydivkit.core.types.union.inject_types."""

    if get_origin(type_) is not Union:
        return

    types_dict: dict[str, type[Any]] = {}
    for u_type in get_args(type_):
        fields = getattr(u_type, "__fields__", None)
        if fields is None:
            continue
        field = fields.get(TYPE_FIELD)
        if field is None:
            continue
        type_value = getattr(field, "default", None)
        if (type_value is None) or not isinstance(type_value, str):
            continue
        types_dict[type_value] = u_type

    if types_dict:
        _UNION_TYPES[type_] = types_dict


# In CPython 3.14+, some typing objects do not allow setting attributes.
# Keep injection bookkeeping in module-level dicts.
_UNION_TYPES: dict[Any, dict[str, type[Any]]] = {}
_UNION_INJECTED: set[Any] = set()


def _get_union_types(type_: Any) -> dict[str, type[Any]] | None:
    return _UNION_TYPES.get(type_)


def _ensure_union_injected(type_: Any) -> None:
    if type_ in _UNION_INJECTED:
        return
    inject_types(type_)
    _UNION_INJECTED.add(type_)


class Expr:
    __slots__ = ("v",)

    def __init__(self, v: str) -> None:
        if not v.startswith("@{") or not v.endswith("}"):
            raise ValueError(f"failed to initiate Expr with {v}")
        self.v = v

    def __str__(self) -> str:
        return self.v


class BaseEntity:
    pass


def cast(value: Any, type_: Any) -> Any:  # noqa: C901
    """Current implementation of _cast_value_type."""

    if isinstance(value, _Field):
        raise ValueError("Value cannot be of type _Field")

    if type_ is Any:
        return value

    if type(value) == type_:
        return value

    origin_type = get_origin(type_)
    args_type = get_args(type_)

    if value is None and type(None) in args_type and origin_type is Union:
        return None

    if isinstance(origin_type, type) and issubclass(origin_type, Sequence):
        if not isinstance(value, Sequence):
            raise ValueError(
                f"Value {value} has wrong type: {type(value)}. Expected type is {type_}.",
            )
        element_type, *_ = args_type
        return [cast(v, element_type) for v in value]

    if isinstance(origin_type, type) and issubclass(origin_type, Mapping):
        if not isinstance(value, Mapping):
            raise ValueError(
                f"Value {value} has wrong type: {type(value)}. Expected type is {type_}.",
            )

        key_type, value_type = args_type
        return {
            cast(k, key_type): cast(v, value_type) for k, v in value.items()
        }

    if origin_type is Union:
        _ensure_union_injected(type_)
        types = _get_union_types(type_)
        if types and isinstance(value, dict):
            type_value = value.get(TYPE_FIELD)
            if not type_value:
                raise ValueError(
                    f"Value {value} does not have field {TYPE_FIELD}.",
                )

            target_type = types.get(type_value)
            if not target_type:
                raise ValueError(
                    f"Union {type_} does not contain type {type_value}.",
                )
            return target_type(**value)

        if type(value) in set(args_type):
            return value

        for u_type in args_type:
            with suppress(ValueError):
                return cast(value, u_type)

        if Expr in get_args(type_):
            with suppress(ValueError):
                return cast(value, Expr)

        raise ValueError(
            f"Value {value} has wrong type: {type(value)}. Expected type is {type_}.",
        )

    try:
        if isinstance(value, type_):
            return value
    except TypeError:
        pass

    if isinstance(value, BaseEntity) and isinstance(type_, type):
        if isinstance(value, type_):
            return value
        raise ValueError(
            f"Value {value} has wrong type. Expected type is {type_}",
        )

    if (
        isinstance(type_, type)
        and issubclass(type_, BaseEntity)
        and isinstance(value, dict)
    ):
        return type_(**value)

    if value is not None and isinstance(type_, type):
        try:
            return type_(value)  # pyright: ignore[reportCallIssue]
        except Exception:
            pass

    raise ValueError(
        f"Value {value} has wrong type. Expected type is {type_}.",
    )


class Foo(BaseEntity):
    __fields__ = {TYPE_FIELD: _Field(default="foo")}

    def __init__(self, **_: Any) -> None:
        return


class Bar(BaseEntity):
    __fields__ = {TYPE_FIELD: _Field(default="bar")}

    def __init__(self, **_: Any) -> None:
        return


U_FOO_BAR = Union[Foo, Bar]
U_OPT_INT = Union[int, type(None)]


def dump(obj: Any) -> Any:
    """Reduced copy of pydivkit.core.entities.dump."""

    if isinstance(obj, (str, bytes)):
        return obj
    if isinstance(obj, Sequence):
        return [dump(v) for v in obj]
    if isinstance(obj, Mapping):
        return {k: dump(v) for k, v in obj.items()}
    if isinstance(obj, Expr):
        return str(obj)
    if isinstance(obj, MiniEntity):
        return obj.dict()
    return obj


class MiniField:
    __slots__ = ("name", "default")

    def __init__(self, name: str, default: Any = None) -> None:
        self.name = name
        self.default = default

    @property
    def field_name(self) -> str:
        return self.name


class MiniRef:
    __slots__ = ("ref_to",)

    def __init__(self, ref_to: MiniField) -> None:
        self.ref_to = ref_to


def Ref(field: MiniField) -> MiniRef:
    return MiniRef(ref_to=field)


class MiniEntity(BaseEntity):
    """Minimal entity with BaseEntity-like __setattr__ and dict()."""

    __fields__: dict[str, MiniField] = {}
    __field_types__: dict[str, Any] = {}
    # Dynamic attribute set in __init__; keep type checkers quiet.
    _caster: Any  # pyright: ignore[reportUninitializedInstanceVariable]

    def __init__(self, _caster, **kwargs: Any) -> None:
        object.__setattr__(self, "_caster", _caster)
        for k, v in kwargs.items():
            if v is not None:
                setattr(self, k, v)
        for k, f in self.__fields__.items():
            if k not in kwargs:
                setattr(self, k, f.default)

    def __setattr__(self, key: str, value: Any) -> None:
        if key.startswith("_"):
            object.__setattr__(self, key, value)
            return

        ftype = self.__field_types__[key]
        if not isinstance(value, MiniRef):
            value = self._caster(value, ftype)
        object.__setattr__(self, key, value)

    def dict(self) -> dict[str, Any]:
        out: dict[str, Any] = {}
        for attr_name, field in self.__fields__.items():
            value = getattr(self, attr_name, field.default)
            if value is None:
                continue
            if isinstance(value, MiniRef):
                out[f"${field.field_name}"] = value.ref_to.field_name
            else:
                out[field.field_name] = dump(value)
        return out

    def related_templates(self) -> set[type[Any]]:
        return set()


class MiniAction(MiniEntity):
    __fields__ = {
        "type": MiniField("type", default="action"),
        "log_id": MiniField("log_id"),
        "url": MiniField("url"),
    }
    __field_types__ = {
        "type": str,
        "log_id": str,
        "url": str,
    }


class MiniText(MiniEntity):
    text = MiniField("text")
    __fields__ = {
        "type": MiniField("type", default="text"),
        "text": text,
        "font_size": MiniField("font_size", default=12),
        "color": MiniField("color", default="#000000"),
        "text_expr": MiniField("text_expr", default=None),
        "ref_text": MiniField("ref_text", default=None),
    }
    __field_types__ = {
        "type": str,
        "text": str,
        "font_size": int,
        "color": str,
        "text_expr": Union[Expr, type(None)],
        "ref_text": Union[MiniRef, type(None)],
    }


class MiniContainer(MiniEntity):
    __fields__ = {
        "type": MiniField("type", default="container"),
        "items": MiniField("items"),
        "actions": MiniField("actions", default=None),
    }
    __field_types__ = {
        "type": str,
        "items": Sequence[MiniEntity],
        "actions": Union[Sequence[MiniAction], type(None)],
    }


class _TplA:
    template_name = "TplA"

    @classmethod
    def template(cls) -> dict[str, Any]:
        return {"type": "container", "items": [{"type": "text", "text": "..."}]}


class _TplB:
    template_name = "TplB"

    @classmethod
    def template(cls) -> dict[str, Any]:
        return {"type": "text", "text": "..."}


def make_card_dict(log_id: str, div: MiniEntity) -> dict[str, Any]:
    return {
        "log_id": log_id,
        "states": [
            {
                "state_id": 0,
                "div": div.dict(),
            }
        ],
    }


def make_div_dict(div: MiniEntity) -> dict[str, Any]:
    templates = {t.template_name: t.template() for t in (_TplA, _TplB)}
    return {
        "templates": templates,
        "card": make_card_dict("card", div),
    }


def _bench_one(fn, value: Any, type_: Any, n: int) -> float:
    # Warmup
    for _ in range(10_000):
        fn(value, type_)

    t0 = time.perf_counter()
    for _ in range(n):
        fn(value, type_)
    t1 = time.perf_counter()
    return (t1 - t0) / n


def bench(label: str, value: Any, type_: Any, n: int = 500_000) -> None:
    times = [_bench_one(cast, value, type_, n) for _ in range(5)]
    t = median(times)
    print(f"{label:28s}  {t * 1e9:9.1f} ns")


def _build_tree() -> MiniContainer:
    items: list[MiniEntity] = []
    for i in range(20):
        text_expr = Expr(f"@{{t{i}}}") if (i % 3 == 0) else None
        ref_text = Ref(MiniText.text) if (i % 5 == 0) else None
        items.append(
            MiniText(
                cast,
                text=f"hello {i}",
                font_size=12 + (i % 6),
                color="#112233",
                text_expr=text_expr,
                ref_text=ref_text,
            )
        )

    actions = [
        MiniAction(cast, log_id="tap", url="https://example.com"),
        MiniAction(cast, log_id="long_tap", url="https://example.com/2"),
        MiniAction(cast, log_id="menu", url="https://example.com/3"),
    ]

    return MiniContainer(cast, items=items, actions=actions)


def _bench_call(fn, n: int) -> float:
    for _ in range(200):
        fn()
    t0 = time.perf_counter()
    for _ in range(n):
        fn()
    t1 = time.perf_counter()
    return (t1 - t0) / n


def bench_dict_and_make_div(
    results: dict[str, float],
    n_construct: int = 20_000,
    n_call: int = 50_000,
) -> None:
    print("")
    print("Benchmarking dict()/make_div (median of 5 runs)")

    tree = _build_tree()

    dict_only = median([_bench_call(tree.dict, n_call) for _ in range(5)])
    make_div_only = median(
        [_bench_call(lambda: make_div_dict(tree), n_call) for _ in range(5)]
    )

    def build_and_dict() -> None:
        _build_tree().dict()

    def build_and_make_div() -> None:
        make_div_dict(_build_tree())

    cd = median([_bench_call(build_and_dict, n_construct) for _ in range(5)])
    cm = median([_bench_call(build_and_make_div, n_construct) for _ in range(5)])

    results["dict_only"] = dict_only
    results["make_div_only"] = make_div_only
    results["build+dict"] = cd
    results["build+make_div"] = cm

    print(f"{'dict_only':28s}  {dict_only * 1e9:9.1f} ns/call")
    print(f"{'make_div_only':28s}  {make_div_only * 1e9:9.1f} ns/call")
    print(f"{'build+dict':28s}  {cd * 1e9:9.1f} ns/call")
    print(f"{'build+make_div':28s}  {cm * 1e9:9.1f} ns/call")


# ---------------------------------------------------------------------------
# dump() benchmarks
# ---------------------------------------------------------------------------


class Color(enum.Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"


def _build_flat_entity() -> MiniText:
    return MiniText(cast, text="hello", font_size=14, color="#aabbcc")


def _build_nested_entities(depth: int = 3) -> MiniContainer:
    """Build a tree of containers `depth` levels deep."""
    leaf = MiniText(cast, text="leaf", font_size=12, color="#000")
    current: MiniEntity = MiniContainer(cast, items=[leaf])
    for _ in range(depth - 1):
        current = MiniContainer(cast, items=[current])
    return current  # type: ignore[return-value]


def _build_mapping_with_values() -> dict[str, Any]:
    """Mapping whose values include Expr, enum, and entity objects."""
    return {
        "expr": Expr("@{x}"),
        "color": Color.RED,
        "entity": _build_flat_entity(),
        "plain": 42,
        "text": "hello",
    }


def bench_dump(n_call: int = 50_000) -> dict[str, float]:
    print("")
    print("Benchmarking dump() (median of 5 runs)")

    flat = _build_flat_entity()
    nested = _build_nested_entities(depth=3)
    seq_entities = [_build_flat_entity() for _ in range(10)]
    mapping_vals = _build_mapping_with_values()

    results: dict[str, float] = {}

    scenarios: list[tuple[str, Any]] = [
        ("dump flat entity", flat),
        ("dump nested (3 deep)", nested),
        ("dump seq[10 entities]", seq_entities),
        ("dump mapping+expr+enum", mapping_vals),
        ("dump enum value", Color.GREEN),
    ]

    for label, obj in scenarios:
        times = [
            _bench_call(lambda o=obj: dump(o), n_call) for _ in range(5)
        ]
        t = median(times)
        results[label] = t
        print(f"{label:28s}  {t * 1e9:9.1f} ns/call")

    return results


# ---------------------------------------------------------------------------
# Regression check mode
# ---------------------------------------------------------------------------

# Thresholds in nanoseconds.  These are generous upper bounds; actual
# performance on modern hardware should be well below them.
_CHECK_THRESHOLDS_NS: dict[str, float] = {
    "exact int": 200,
    "optional[int] with None": 500,
    "seq[int]": 5_000,
    "map[str,int]": 5_000,
    "union dict dispatch": 5_000,
    "union exact instance": 1_000,
    "dict_only": 100_000,
    "build+dict": 500_000,
    "dump flat entity": 50_000,
    "dump nested (3 deep)": 200_000,
    "dump seq[10 entities]": 200_000,
    "dump mapping+expr+enum": 50_000,
}


def run_check(all_results: dict[str, float]) -> bool:
    """Return True if all benchmarks are within thresholds."""
    print("")
    print("Regression check:")
    ok = True
    for label, threshold_ns in _CHECK_THRESHOLDS_NS.items():
        actual_ns = all_results.get(label)
        if actual_ns is None:
            continue
        actual_ns_val = actual_ns * 1e9
        status = "OK" if actual_ns_val <= threshold_ns else "FAIL"
        if status == "FAIL":
            ok = False
        print(
            f"  {label:28s}  {actual_ns_val:9.1f} ns "
            f"(limit {threshold_ns:9.0f} ns)  [{status}]"
        )
    return ok


def main() -> None:
    check_mode = "--check" in sys.argv

    print("Benchmarking _cast_value_type (median of 5 runs)")
    pyver = ".".join(map(str, sys.version_info[:3]))
    print(f"Python {pyver}")
    print("")

    all_results: dict[str, float] = {}

    cast_scenarios: list[tuple[str, Any, Any, int]] = [
        ("exact int", 123, int, 500_000),
        ("optional[int] with None", None, U_OPT_INT, 500_000),
        ("seq[int]", [1, 2, 3, 4], Sequence[int], 200_000),
        (
            "map[str,int]",
            {"a": 1, "b": 2, "c": 3},
            Mapping[str, int],
            200_000,
        ),
        (
            "union dict dispatch",
            {"type": "foo", "x": 1},
            U_FOO_BAR,
            300_000,
        ),
        ("union exact instance", Foo(), U_FOO_BAR, 500_000),
    ]

    for label, value, type_, n in cast_scenarios:
        bench(label, value, type_, n)
        times = [_bench_one(cast, value, type_, n) for _ in range(5)]
        all_results[label] = median(times)

    bench_dict_and_make_div(all_results)
    dump_results = bench_dump()
    all_results.update(dump_results)

    if check_mode:
        if not run_check(all_results):
            print("\nRegression detected!")
            sys.exit(1)
        else:
            print("\nAll checks passed.")


if __name__ == "__main__":
    main()
