from dataclasses import asdict, fields, is_dataclass
from typing import Any, Callable, Dict, Optional

from snapflow.core import operators
from snapflow.core.data_block import DataBlock
from snapflow.core.environment import (
    Environment,
    current_env,
    produce,
    run_graph,
    run_node,
)
from snapflow.core.graph import DeclaredGraph, Graph, graph
from snapflow.core.module import SnapflowModule
from snapflow.core.node import DeclaredNode, Node, node
from snapflow.core.snap import Input, Output, Param, Snap, _Snap
from snapflow.core.sql.sql_snap import Sql, SqlSnap, sql_snap
from snapflow.core.streams import DataBlockStream, StreamBuilder

# TODO: use an existing lib for this:
#       - maybe the best: https://github.com/Fatal1ty/mashumaro


# def get_serializer(obj: Any) -> Optional[Callable[..., Dict]]:
#     if hasattr(obj, "_serialize"):
#         return obj._serialize
#     return None


# def _serialize(obj: Any, dict_factory: Callable[..., Dict]) -> Any:
#     """Adapted from dataclass.asdict"""
#     if is_dataclass(obj):
#         result = []
#         for f in fields(obj):
#             value = _serialize(getattr(obj, f.name), dict_factory)
#             result.append((f.name, value))
#         return dict_factory(result)
#     elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
#         # obj is a namedtuple.  Recurse into it, but the returned
#         # object is another namedtuple of the same type.  This is
#         # similar to how other list- or tuple-derived classes are
#         # treated (see below), but we just need to create them
#         # differently because a namedtuple's __init__ needs to be
#         # called differently (see bpo-34363).

#         # I'm not using namedtuple's _asdict()
#         # method, because:
#         # - it does not recurse in to the namedtuple fields and
#         #   convert them to dicts (using dict_factory).
#         # - I don't actually want to return a dict here.  The main
#         #   use case here is json.dumps, and it handles converting
#         #   namedtuples to lists.  Admittedly we're losing some
#         #   information here when we produce a json list instead of a
#         #   dict.  Note that if we returned dicts here instead of
#         #   namedtuples, we could no longer call asdict() on a data
#         #   structure where a namedtuple was used as a dict key.

#         return type(obj)(*[_serialize(v, dict_factory) for v in obj])
#     elif isinstance(obj, (list, tuple)):
#         # Assume we can create an object of this type by passing in a
#         # generator (which is not true for namedtuples, handled
#         # above).
#         return type(obj)(_serialize(v, dict_factory) for v in obj)
#     elif isinstance(obj, dict):
#         return type(obj)(
#             (_serialize(k, dict_factory), _serialize(v, dict_factory))
#             for k, v in obj.items()
#         )
#     else:
#         s = get_serializer(obj)
#         if s is None:
#             return copy.deepcopy(obj)
#         return s(obj)

from dataclasses import asdict, fields, is_dataclass
from typing import Any, Callable, Dict, Optional

from snapflow.core import operators
from snapflow.core.data_block import DataBlock
from snapflow.core.environment import (
    Environment,
    current_env,
    produce,
    run_graph,
    run_node,
)
from snapflow.core.graph import DeclaredGraph, Graph, graph
from snapflow.core.module import SnapflowModule
from snapflow.core.node import DeclaredNode, Node, node
from snapflow.core.snap import Input, Output, Param, Snap, _Snap
from snapflow.core.sql.sql_snap import Sql, SqlSnap, sql_snap
from snapflow.core.streams import DataBlockStream, StreamBuilder

# TODO: use an existing lib for this:
#       - maybe the best: https://github.com/Fatal1ty/mashumaro


# def get_serializer(obj: Any) -> Optional[Callable[..., Dict]]:
#     if hasattr(obj, "_serialize"):
#         return obj._serialize
#     return None


# def _serialize(obj: Any, dict_factory: Callable[..., Dict]) -> Any:
#     """Adapted from dataclass.asdict"""
#     if is_dataclass(obj):
#         result = []
#         for f in fields(obj):
#             value = _serialize(getattr(obj, f.name), dict_factory)
#             result.append((f.name, value))
#         return dict_factory(result)
#     elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
#         # obj is a namedtuple.  Recurse into it, but the returned
#         # object is another namedtuple of the same type.  This is
#         # similar to how other list- or tuple-derived classes are
#         # treated (see below), but we just need to create them
#         # differently because a namedtuple's __init__ needs to be
#         # called differently (see bpo-34363).

#         # I'm not using namedtuple's _asdict()
#         # method, because:
#         # - it does not recurse in to the namedtuple fields and
#         #   convert them to dicts (using dict_factory).
#         # - I don't actually want to return a dict here.  The main
#         #   use case here is json.dumps, and it handles converting
#         #   namedtuples to lists.  Admittedly we're losing some
#         #   information here when we produce a json list instead of a
#         #   dict.  Note that if we returned dicts here instead of
#         #   namedtuples, we could no longer call asdict() on a data
#         #   structure where a namedtuple was used as a dict key.

#         return type(obj)(*[_serialize(v, dict_factory) for v in obj])
#     elif isinstance(obj, (list, tuple)):
#         # Assume we can create an object of this type by passing in a
#         # generator (which is not true for namedtuples, handled
#         # above).
#         return type(obj)(_serialize(v, dict_factory) for v in obj)
#     elif isinstance(obj, dict):
#         return type(obj)(
#             (_serialize(k, dict_factory), _serialize(v, dict_factory))
#             for k, v in obj.items()
#         )
#     else:
#         s = get_serializer(obj)
#         if s is None:
#             return copy.deepcopy(obj)
#         return s(obj)

# TODO: use an existing lib for this: #       - maybe the best: https://github.com/Fatal1ty/mashumaro # def get_serializer(obj: Any) -> Optional[Callable[..., Dict]]: #     if hasattr(obj, "_serialize"): #         return obj._serialize #     return None # def _serialize(obj: Any, dict_factory: Callable[..., Dict]) -> Any: #     """Adapted from dataclass.asdict""" #     if is_dataclass(obj): #         result = [] #         for f in fields(obj): #             value = _serialize(getattr(obj, f.name), dict_factory) #             result.append((f.name, value)) #         return dict_factory(result) #     elif isinstance(obj, tuple) and hasattr(obj, "_fields"): #         # obj is a namedtuple.  Recurse into it, but the returned #         # object is another namedtuple of the same type.  This is #         # similar to how other list- or tuple-derived classes are #         # treated (see below), but we just need to create them #         # differently because a namedtuple's __init__ needs to be #         # called differently (see bpo-34363). #         # I'm not using namedtuple's _asdict() #         # method, because: #         # - it does not recurse in to the namedtuple fields and #         #   convert them to dicts (using dict_factory). #         # - I don't actually want to return a dict here.  The main #         #   use case here is json.dumps, and it handles converting #         #   namedtuples to lists.  Admittedly we're losing some #         #   information here when we produce a json list instead of a #         #   dict.  Note that if we returned dicts here instead of #         #   namedtuples, we could no longer call asdict() on a data #         #   structure where a namedtuple was used as a dict key. #         return type(obj)(*[_serialize(v, dict_factory) for v in obj]) #     elif isinstance(obj, (list, tuple)): #         # Assume we can create an object of this type by passing in a #         # generator (which is not true for namedtuples, handled #         # above). #         return type(obj)(_serialize(v, dict_factory) for v in obj) #     elif isinstance(obj, dict): #         return type(obj)( #             (_serialize(k, dict_factory), _serialize(v, dict_factory)) #             for k, v in obj.items() #         ) #     else: #         s = get_serializer(obj) #         if s is None: #             return copy.deepcopy(obj) #         return s(obj)