from __future__ import annotations

import os
from collections.abc import Iterable, Mapping as MappingABC
from typing import Any, Dict, Optional, Set, Tuple, TypeVar

from receipts.compat.pyd import BaseModel


IMMUTABLE_RECEIPTS_ENV_VAR = "IMMUTABLE_RECEIPTS"

_TRUTHY = {"1", "true", "yes", "on"}
_FROZEN_ERROR = "frozen container is immutable"

T = TypeVar("T")
KT = TypeVar("KT")
VT = TypeVar("VT")


def immutable_receipts_enabled() -> bool:
    """Return True when immutable receipt enforcement is active."""

    token = os.getenv(IMMUTABLE_RECEIPTS_ENV_VAR, "").strip().lower()
    return token in _TRUTHY


class FrozenDict(dict):
    """Read-only mapping used to guard receipt payload dictionaries."""

    __slots__ = ()

    def __init__(self, data: Optional[MappingABC[KT, VT] | Iterable[Tuple[KT, VT]]] = None, **kwargs: VT) -> None:
        if data is not None and kwargs:
            merged = dict(data)
            merged.update(kwargs)
            dict.__init__(self, merged)
        elif data is not None:
            dict.__init__(self, dict(data))
        elif kwargs:
            dict.__init__(self, dict(kwargs))
        else:
            dict.__init__(self)

    def __setitem__(self, key: KT, value: VT) -> None:  # pragma: no cover - handled by _immutable
        self._immutable()

    def __delitem__(self, key: KT) -> None:  # pragma: no cover - handled by _immutable
        self._immutable()

    def _immutable(self, *args: Any, **kwargs: Any) -> None:
        raise TypeError(_FROZEN_ERROR)

    clear = _immutable
    pop = _immutable
    popitem = _immutable
    setdefault = _immutable
    update = _immutable

    def copy(self) -> Dict[KT, VT]:
        return dict(self)

    def to_dict(self) -> Dict[KT, VT]:
        return dict(self)


class FrozenList(list):
    """Read-only sequence used to guard receipt payload lists."""

    __slots__ = ()

    def __init__(self, data: Optional[Iterable[T]] = None) -> None:
        if data is None:
            list.__init__(self)
        else:
            list.__init__(self, data)

    def __setitem__(self, index, value):  # pragma: no cover - handled by _immutable
        self._immutable()

    def __delitem__(self, index):  # pragma: no cover - handled by _immutable
        self._immutable()

    def __iadd__(self, other):  # pragma: no cover - handled by _immutable
        self._immutable()

    def __imul__(self, other):  # pragma: no cover - handled by _immutable
        self._immutable()

    def _immutable(self, *args: Any, **kwargs: Any) -> None:
        raise TypeError(_FROZEN_ERROR)

    append = _immutable
    clear = _immutable
    extend = _immutable
    insert = _immutable
    pop = _immutable
    remove = _immutable
    reverse = _immutable
    sort = _immutable

    def to_list(self) -> list[T]:
        return list(self)

    def copy(self) -> list[T]:
        return list(self)


def deep_freeze(value: Any, *, _seen: Optional[Set[int]] = None) -> Any:
    """Recursively convert mutable containers to their frozen counterparts."""

    if _seen is None:
        _seen = set()
    ident = id(value)
    if ident in _seen:
        return value
    _seen.add(ident)

    if isinstance(value, (FrozenDict, FrozenList)):
        return value

    if isinstance(value, BaseModel):
        freeze_model(value, _seen=_seen)
        return value

    if isinstance(value, MappingABC):
        frozen_items = {key: deep_freeze(item, _seen=_seen) for key, item in value.items()}
        return FrozenDict(frozen_items)

    if isinstance(value, list):
        return FrozenList(deep_freeze(item, _seen=_seen) for item in value)

    if isinstance(value, tuple):
        return tuple(deep_freeze(item, _seen=_seen) for item in value)

    if isinstance(value, set):
        return frozenset(deep_freeze(item, _seen=_seen) for item in value)

    return value


def freeze_model(model: BaseModel, *, _seen: Optional[Set[int]] = None) -> BaseModel:
    """Deep-freeze all container fields on a BaseModel instance."""

    if _seen is None:
        _seen = set()
    ident = id(model)
    if ident in _seen:
        return model
    _seen.add(ident)

    for field_name in model.model_fields:
        if not hasattr(model, field_name):
            continue
        value = getattr(model, field_name)
        frozen_value = deep_freeze(value, _seen=_seen)
        if frozen_value is not value:
            object.__setattr__(model, field_name, frozen_value)
    return model


def _is_primitive(value: Any) -> bool:
    return isinstance(value, (str, bytes, int, float, bool, type(None)))


def is_frozen(value: Any, *, _seen: Optional[Set[int]] = None) -> bool:
    """Return True when value contains no mutable dict/list containers."""

    if _seen is None:
        _seen = set()
    ident = id(value)
    if ident in _seen:
        return True
    _seen.add(ident)

    if _is_primitive(value):
        return True

    if isinstance(value, FrozenDict):
        return all(is_frozen(item, _seen=_seen) for item in value.values())

    if isinstance(value, FrozenList):
        return all(is_frozen(item, _seen=_seen) for item in value)

    if isinstance(value, (MappingABC, list)):
        return False

    if isinstance(value, tuple):
        return all(is_frozen(item, _seen=_seen) for item in value)

    if isinstance(value, set):
        return False

    if isinstance(value, frozenset):
        return all(is_frozen(item, _seen=_seen) for item in value)

    if isinstance(value, BaseModel):
        return all(is_frozen(getattr(value, field), _seen=_seen) for field in value.model_fields)

    return True


def ensure_frozen(value: Any, *, context: str = "value") -> None:
    """Raise a ValueError when ``value`` still contains mutable containers."""

    if immutable_receipts_enabled() and not is_frozen(value):
        raise ValueError(f"{context} must be frozen before signing")
