"""
Safe serializer with cycle detection and depth/breadth limits.

Converts arbitrary Python objects into JSON-safe structures without
raising exceptions, hanging on deep nesting, or blowing up on
circular references.
"""

from overcast_sdk.consts import (
    MAX_SERIALIZE_DEPTH,
    MAX_SERIALIZE_BREADTH,
    MAX_STRING_LENGTH,
)
from overcast_sdk.utils import safe_str

CYCLE_MARKER = "<circular reference>"


class _Memo:
    """Track object ids to detect circular references during traversal."""
    __slots__ = ("_seen",)

    def __init__(self):
        self._seen = set()

    def enter(self, obj):
        """Return True if *obj* was already visited (cycle)."""
        obj_id = id(obj)
        if obj_id in self._seen:
            return True
        self._seen.add(obj_id)
        return False

    def leave(self, obj):
        self._seen.discard(id(obj))


def safe_serialize(
    obj,
    _memo=None,
    _depth=0,
    max_depth=MAX_SERIALIZE_DEPTH,
    max_breadth=MAX_SERIALIZE_BREADTH,
    max_string=MAX_STRING_LENGTH,
):
    """Recursively convert *obj* to a JSON-safe value.

    * Detects circular references and replaces them with a marker.
    * Stops recursing beyond *max_depth*.
    * Truncates dicts/lists beyond *max_breadth* items.
    * Truncates strings beyond *max_string* characters.
    """
    if _memo is None:
        _memo = _Memo()

    # Depth guard
    if _depth > max_depth:
        return safe_str(obj, max_string)

    # Primitives — safe to return directly
    if obj is None or isinstance(obj, (bool, int, float)):
        return obj

    if isinstance(obj, str):
        return obj[:max_string] if len(obj) > max_string else obj

    if isinstance(obj, bytes):
        try:
            s = obj.decode("utf-8", errors="replace")
        except Exception:
            s = "<binary>"
        return s[:max_string] if len(s) > max_string else s

    # Container types — need cycle detection
    if isinstance(obj, dict):
        if _memo.enter(obj):
            return CYCLE_MARKER
        try:
            result = {}
            for i, (k, v) in enumerate(obj.items()):
                if i >= max_breadth:
                    result["..."] = f"{len(obj) - max_breadth} more keys"
                    break
                result[safe_str(k, max_string)] = safe_serialize(
                    v, _memo, _depth + 1, max_depth, max_breadth, max_string
                )
            return result
        finally:
            _memo.leave(obj)

    if isinstance(obj, (list, tuple)):
        if _memo.enter(obj):
            return CYCLE_MARKER
        try:
            result = []
            for i, item in enumerate(obj):
                if i >= max_breadth:
                    result.append(f"<... {len(obj) - max_breadth} more items>")
                    break
                result.append(
                    safe_serialize(
                        item, _memo, _depth + 1, max_depth, max_breadth, max_string
                    )
                )
            return result
        finally:
            _memo.leave(obj)

    if isinstance(obj, (set, frozenset)):
        if _memo.enter(obj):
            return CYCLE_MARKER
        try:
            items = []
            for i, item in enumerate(sorted(obj, key=lambda x: safe_str(x))):
                if i >= max_breadth:
                    items.append(f"<... {len(obj) - max_breadth} more items>")
                    break
                items.append(
                    safe_serialize(
                        item, _memo, _depth + 1, max_depth, max_breadth, max_string
                    )
                )
            return items
        finally:
            _memo.leave(obj)

    # Fallback for unknown types
    return safe_str(obj, max_string)
