"""
Utilities for pretty-printing JSON in debug logs.
These functions are useful for dumping large data structures,
with options to trim long strings and lists to specified limits.
"""

import json
from dataclasses import asdict, is_dataclass
from enum import Enum
from typing import Any

import proto
from pydantic.v1 import BaseModel as BaseModelV1

from aidial_interceptors_sdk.examples.utils.decorator import fail_safe
from aidial_interceptors_sdk.examples.utils.protobuf import message_to_dict


@fail_safe
def json_dumps_short(
    obj: Any, *, string_limit: int = 100, list_len_limit: int = 10, **kwargs
) -> str:
    def default(obj) -> str:
        return _truncate_strings(str(obj), string_limit)

    return json.dumps(
        _truncate_lists(
            _truncate_strings(_to_dict(obj, **kwargs), string_limit),
            list_len_limit,
        ),
        default=default,
    )


@fail_safe
def json_dumps(obj: Any, **kwargs) -> str:
    return json.dumps(_to_dict(obj, **kwargs), default=str)


def _to_dict(obj: Any, **kwargs) -> Any:
    def rec(val):
        return _to_dict(val, **kwargs)

    def dict_field(key: str, val: Any) -> Any:
        if key in kwargs.get("excluded_keys", []):
            return "<excluded>"
        return val

    if isinstance(obj, bytes):
        return f"<bytes>({len(obj):_} B)"

    if isinstance(obj, Enum):
        return obj.value

    if isinstance(obj, dict):
        filters = []
        if kwargs.get("exclude_none"):
            filters.append(lambda x: x is not None)
        if kwargs.get("exclude_empty_dict"):
            filters.append(lambda x: x != {})

        ret = {}
        for k, v in obj.items():
            v = rec(dict_field(k, v))
            if all(f(v) for f in filters):
                ret[k] = v
        return ret

    if isinstance(obj, list):
        return [rec(element) for element in obj]

    if isinstance(obj, tuple):
        return tuple(rec(element) for element in obj)

    if isinstance(obj, BaseModelV1):
        return rec(obj.dict())

    if isinstance(obj, proto.Message):
        return rec(message_to_dict(obj))

    if hasattr(obj, "to_dict"):
        return rec(obj.to_dict())

    if is_dataclass(type(obj)):
        return rec(asdict(obj))

    return obj


def _truncate_strings(obj: Any, limit: int) -> Any:
    def rec(val):
        return _truncate_strings(val, limit)

    if isinstance(obj, dict):
        return {key: rec(value) for key, value in obj.items()}

    if isinstance(obj, list):
        return [rec(element) for element in obj]

    if isinstance(obj, tuple):
        return tuple(rec(element) for element in obj)

    if isinstance(obj, str) and len(obj) > limit:
        skip = len(obj) - limit
        return (
            obj[: limit // 2] + f"...({skip:_} skipped)..." + obj[-limit // 2 :]
        )

    return obj


def _truncate_lists(obj: Any, limit: int) -> Any:
    def rec(val):
        return _truncate_lists(val, limit)

    if isinstance(obj, dict):
        return {key: rec(value) for key, value in obj.items()}

    if isinstance(obj, list):
        if len(obj) > limit:
            skip = len(obj) - limit
            obj = (
                obj[: limit // 2]
                + [f"...({skip:_} skipped)..."]
                + obj[-limit // 2 :]
            )
        return [rec(element) for element in obj]

    if isinstance(obj, tuple):
        return tuple(rec(element) for element in obj)

    return obj
