from collections.abc import Hashable, Iterable, Mapping, Sequence
from copy import deepcopy
from functools import reduce
from typing import Any, TypeVar, cast, overload

from .display import identity

T = TypeVar("T")
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")

JsonValue = Hashable | Sequence["JsonValue"] | Mapping[Hashable, "JsonValue"]
DictType = Mapping[Hashable, "JsonValue"]


def _dedup[T](x: Iterable[T]) -> list[T]:
    tracker: set[T] = set()
    result: list[T] = []
    for elem in x:
        if elem in tracker:
            continue
        tracker.add(elem)
        result.append(elem)
    return result


@overload
def dedup[T](x: list[T]) -> list[T]: ...
@overload
def dedup[T](x: tuple[T, ...]) -> tuple[T, ...]: ...
@overload
def dedup[T](x: set[T]) -> set[T]: ...
def dedup(x):  # type: ignore
    if isinstance(x, list):
        return _dedup(x)
    if isinstance(x, tuple):
        return tuple(_dedup(x))
    if isinstance(x, set):
        return set(x)
    raise TypeError


def make_hashable(obj: Any) -> Hashable:
    """
    Convert any object to a hashable representation.

    Improved: Recursively converts nested structures for true hashability.
    """

    if isinstance(obj, (list, tuple, set)):
        return tuple(make_hashable(item) for item in obj)

    if isinstance(obj, Hashable):
        return obj

    if isinstance(obj, dict):
        return tuple(((make_hashable(k), make_hashable(v)) for k, v in obj.items()))

    return str(obj)


def _should_include(item: Any, existing: Iterable, add_duplicates: bool) -> bool:
    """Determine if an item should be included when merging sequences."""
    if add_duplicates:
        return True

    if isinstance(existing, (list, tuple, set)):
        return item not in existing
    return item != existing


def _merge_sequence_with_value(
    seq: Sequence[T] | set[T], value: T, add_duplicates: bool
) -> Sequence[T] | set[T]:
    """Merge a sequence with a single value."""
    if isinstance(seq, list):
        return seq if not _should_include(value, seq, add_duplicates) else [*seq, value]

    if isinstance(seq, tuple):
        return seq if not _should_include(value, seq, add_duplicates) else (*seq, value)

    if isinstance(seq, set):
        hashable_val = make_hashable(value)
        return (  # type: ignore
            seq
            if not _should_include(hashable_val, seq, add_duplicates)
            else seq | {cast(T, hashable_val)}
        )

    return seq


def _merge_value_with_sequence(
    value: T, seq: Sequence[T] | set[T], add_duplicates: bool
) -> Sequence[T] | set[T]:
    """Merge a sequence with a single value."""
    if isinstance(seq, list):
        return seq if not _should_include(value, seq, add_duplicates) else [value, *seq]

    if isinstance(seq, tuple):
        return seq if not _should_include(value, seq, add_duplicates) else (value, *seq)

    if isinstance(seq, set):
        hashable_val = cast(T, make_hashable(value))
        return seq | {hashable_val}

    return seq


def _merge_two_sequences(
    left: Sequence[T] | set[T], right: Sequence[T] | set[T], add_duplicates: bool
) -> Sequence[T] | set[T]:
    """Merge two sequences/collections."""
    if isinstance(left, set):
        right_hashable = (make_hashable(item) for item in right)
        return {*left, *right_hashable}  # type: ignore

    _maybe_dedup = identity if add_duplicates else dedup
    return left.__class__(_maybe_dedup(list(left) + list(right)))  # type: ignore


def join_as_sequence(
    left: JsonValue,
    right: JsonValue,
    default_type: type[list] | type[tuple] | type[set] = tuple,
    add_duplicates: bool = True,
) -> Sequence | set:
    """
    Join two values into a sequence.

    Improved: More consistent type handling and clearer logic flow.
    """
    if isinstance(left, (list, tuple, set)) and isinstance(right, (list, tuple, set)):
        return _merge_two_sequences(left, right, add_duplicates)

    if isinstance(left, (list, tuple, set)):
        return _merge_sequence_with_value(left, right, add_duplicates)

    if isinstance(right, (list, tuple, set)):
        result = _merge_value_with_sequence(left, right, add_duplicates)
        return result

    if add_duplicates or left != right:
        return default_type((left, right))
    return default_type((left,))


def merge_dicts(  # noqa: C901 (complexity)
    *dicts: DictType,
    default_sequence_type: type[list] | type[tuple] | type[set] = tuple,
    add_duplicates: bool = True,
) -> dict[Hashable, Any]:
    """
    Deep merge multiple dictionaries.
    """

    def merge_values(left_val: Any, right_val: Any) -> Any:  # noqa: PLR0911 (too many return statements)
        """Merge two values, handling nested structures."""
        if left_val == right_val:
            return left_val.copy() if isinstance(left_val, dict) else left_val

        if isinstance(left_val, dict) and isinstance(right_val, dict):
            return merge_dicts(
                left_val,
                right_val,
                default_sequence_type=default_sequence_type,
                add_duplicates=add_duplicates,
            )

        if left_val is None:
            return right_val
        if right_val is None:
            return left_val

        if isinstance(left_val, dict):
            return left_val.copy()
        if isinstance(right_val, dict):
            return right_val.copy()

        return join_as_sequence(
            left_val, right_val, default_type=default_sequence_type, add_duplicates=add_duplicates
        )

    def merge_two(left: DictType, right: DictType) -> dict[Hashable, Any]:
        """Merge two dictionaries."""
        result = dict(left)

        for key, right_value in right.items():
            if key not in result:
                result[key] = right_value
            else:
                result[key] = merge_values(result[key], right_value)

        return result

    if not dicts:
        return {}
    if len(dicts) == 1:
        return dict(dicts[0]).copy()

    return cast(dict[Hashable, Any], reduce(merge_two, dicts))


def deep_get(dictionary: DictType, *keys: Hashable, default: Any = None) -> Any:
    """Safely get a value from a nested dictionary."""
    current = dictionary
    for key in keys:
        if not isinstance(current, dict) or key not in current:
            return default
        current = current[key]
    return current


def deep_set(dictionary: DictType, value: Any, *keys: Hashable) -> DictType:
    """Set a value in a nested dictionary (returns new dict)."""

    result = dict(deepcopy(dictionary))
    print(id(dictionary), id(result))
    if not keys:
        return result
    current: dict = result

    for key in keys[:-1]:
        if key not in current or not isinstance(current[key], dict):
            current[key] = {}
        current = current[key]

        # if key not in current or not isinstance(value := current[key], dict):  # type: ignore
        #     current[key] = {}  # type: ignore
        # current = current[key]  # type: ignore

    # current[keys[-1]] = value  # type: ignore
    # return result.copy()  # type: ignore

    current[keys[-1]] = value
    return result
