import functools
import inspect
from typing import (
    TypeVar,
    Any,
    Iterable,
    AsyncIterable,
    AsyncIterator,
    Iterator,
    get_origin,
    get_args,
    get_type_hints,
)
from .exceptions import ValidationError
from .types import ErrorDict
from .config import settings
from .protocols import MonkConstraint

T = TypeVar("T")

_PRIMITIVE_TYPES = frozenset((str, int, float, bool, type(None)))


def _prepare_constraints(constraints: Iterable[Any]) -> list[Any]:
    """Takes raw constraints (classes or instances) and prepares them for execution."""
    field_rules: list[Any] = []
    for m in constraints:
        # Auto-instantiate classes that implement the protocol but were passed as a type
        if isinstance(m, type) and issubclass(m, MonkConstraint):
            try:
                field_rules.append(m())
            except TypeError as e:
                raise TypeError(
                    f"Constraint '{m.__name__}' missing required arguments. Did you mean {m.__name__}(...)?"
                ) from e
        elif isinstance(m, MonkConstraint):
            field_rules.append(m)
    return field_rules


def _extract_monk_metadata(hints: dict[str, Any]) -> dict[str, list[MonkConstraint]]:
    """Extracts validation rules from type hints."""
    rules: dict[str, list[MonkConstraint]] = {}

    for name, hint in hints.items():
        # Look for our specific MonkConstraint in Annotated metadata
        metadata = getattr(hint, "__metadata__", [])

        # Support framework wrappers (like SQLAlchemy's Mapped[Annotated[...]])
        if not metadata:
            args = get_args(hint)
            origin = get_origin(hint)
            # Only unwrap if it's not a standard collection (collections use Each constraint)
            if args and origin not in (list, set, frozenset, tuple, dict):
                metadata = getattr(args[0], "__metadata__", [])

        field_rules = _prepare_constraints(metadata)
        if field_rules:
            rules[name] = field_rules

    return rules


def _recurse(val: Any, prefix: str, errors: list[ErrorDict]) -> None:
    """Recursively validates nested Monk objects and bubbles up errors."""
    if hasattr(val, "__monk_rules__"):
        try:
            validate(val)
        except ValidationError as e:
            for err in e.errors:
                err["field"] = f"{prefix}.{err['field']}"
                errors.append(err)
    elif isinstance(val, (list, tuple, set)):
        for i, item in enumerate(val):
            if type(item) not in _PRIMITIVE_TYPES:
                _recurse(item, f"{prefix}[{i}]", errors)
    elif isinstance(val, dict):
        for k, v in val.items():
            if type(v) not in _PRIMITIVE_TYPES:
                _recurse(v, f"{prefix}[{repr(k)}]", errors)


def _validate_field_and_recurse(
    field_name: str,
    value: Any,
    constraints: list[Any],
    errors: list[ErrorDict],
) -> None:
    """Core validation loop shared by both dataclasses and function arguments."""
    if value is None:
        if constraints:
            is_explicitly_nullable = False
            is_explicitly_not_null = False
            not_null_c = None

            for c in constraints:
                c_name = type(c).__name__
                if c_name == "Nullable":
                    is_explicitly_nullable = True
                elif c_name == "NotNull":
                    is_explicitly_not_null = True
                    not_null_c = c

            allows_none = (
                True if is_explicitly_nullable else (False if is_explicitly_not_null else settings.default_allow_none)
            )

            if not allows_none:
                msg = getattr(not_null_c, "message", None) or "Field is required and cannot be null."
                code = getattr(not_null_c, "code", None) or "NotNull"
                errors.append({"field": field_name, "message": msg, "code": code})
        return

    for c in constraints:
        c_name = type(c).__name__
        if c_name in ("Nullable", "NotNull"):
            continue
        try:
            c.validate(value)
        except ValidationError as e:
            for err in e.errors:
                err["field"] = f"{field_name}{err.get('field', '')}"
                errors.append(err)
        except (ValueError, TypeError) as e:
            error_code = getattr(c, "code", None) or c_name
            errors.append({"field": field_name, "message": str(e), "code": error_code})

    if type(value) not in _PRIMITIVE_TYPES:
        _recurse(value, field_name, errors)


def validate_arguments(arguments: dict[str, Any], rules: dict[str, list[Any]]) -> None:
    """Validates a dictionary of function arguments against extracted constraints."""
    errors: list[ErrorDict] = []
    for arg_name, value in arguments.items():
        constraints = rules.get(arg_name, [])
        _validate_field_and_recurse(arg_name, value, constraints, errors)

    if errors:
        raise ValidationError(errors)


def validate_return(value: Any, constraints: list[Any]) -> None:
    """Validates a function's return value against extracted constraints."""
    errors: list[ErrorDict] = []
    _validate_field_and_recurse("return", value, constraints, errors)

    if errors:
        raise ValidationError(errors)


@functools.lru_cache(maxsize=None)
def _get_schema_rules(schema: type) -> tuple[set[str], dict[str, list[MonkConstraint]]]:
    """Caches the allowed keys and extracted rules for a given TypedDict schema to maximize performance."""
    hints = get_type_hints(schema, include_extras=True)
    return set(hints.keys()), _extract_monk_metadata(hints)


def validate_dict(
    data: dict[str, Any], schema: type, *, partial: bool = False, drop_extra_keys: bool = False
) -> dict[str, Any]:
    """Validates a raw dictionary against a TypedDict or Dataclass schema without instantiating an object."""
    allowed_keys, rules = _get_schema_rules(schema)
    errors: list[ErrorDict] = []

    if not drop_extra_keys:
        # Using data.keys() view is much faster than instantiating a new set() object
        extra_keys = data.keys() - allowed_keys
        if extra_keys:
            errors.append(
                {
                    "field": "__root__",
                    "message": f"Unrecognized fields provided: {', '.join(sorted(extra_keys))}",
                    "code": "StrictDictionary",
                }
            )

    # We iterate over the SCHEMA rules to catch missing required fields
    for field_name, constraints in rules.items():
        if partial and field_name not in data:
            continue

        value = data.get(field_name)
        _validate_field_and_recurse(field_name, value, constraints, errors)

    if errors:
        raise ValidationError(errors)

    if drop_extra_keys:
        return {k: v for k, v in data.items() if k in allowed_keys}

    return data


def validate_stream(stream: Iterable[Any], *constraints: Any) -> Iterator[Any]:
    """
    Lazily validates an iterable/generator on the fly without consuming it entirely.
    Yields items one by one, raising a ValidationError if an item fails.
    """
    prepared = _prepare_constraints(constraints)

    for i, item in enumerate(stream):
        errors: list[ErrorDict] = []
        _validate_field_and_recurse(f"[{i}]", item, prepared, errors)
        if errors:
            raise ValidationError(errors)
        yield item


async def validate_async_stream(stream: AsyncIterable[Any], *constraints: Any) -> AsyncIterator[Any]:
    """
    Lazily validates an async iterable/generator on the fly without consuming it entirely.
    Yields items one by one, raising a ValidationError if an item fails.
    """
    prepared = _prepare_constraints(constraints)

    i = 0
    async for item in stream:
        errors: list[ErrorDict] = []
        _validate_field_and_recurse(f"[{i}]", item, prepared, errors)
        if errors:
            raise ValidationError(errors)
        yield item
        i += 1


def _process_monk_validate_result(result: Any, errors: list[ErrorDict]) -> None:
    """Normalizes and processes the output of the __monk_validate__ hook."""
    if result is None:
        return

    items: Iterable[Any]
    # 1. Normalize the output: if it's a single string or single tuple, wrap it in a list
    if isinstance(result, str) or (isinstance(result, tuple) and len(result) > 0 and isinstance(result[0], str)):
        items = [result]
    elif isinstance(result, Iterable):
        items = result
    else:
        raise TypeError(f"Invalid yield/return from __monk_validate__: {result}. Expected a string or tuple.")

    # 2. Process the items
    for err in items:
        if isinstance(err, str):
            errors.append({"field": "__root__", "message": err, "code": "ModelRule"})
        elif isinstance(err, tuple):
            if not all(isinstance(item, str) for item in err):
                raise TypeError(f"Invalid tuple items from __monk_validate__: {err}. All tuple items must be strings.")

            if len(err) == 1:
                errors.append({"field": "__root__", "message": err[0], "code": "ModelRule"})
            elif len(err) == 2:
                errors.append({"field": err[0], "message": err[1], "code": "ModelRule"})
            elif len(err) == 3:
                errors.append({"field": err[0], "message": err[1], "code": err[2]})
            else:
                raise TypeError(f"Invalid tuple length from __monk_validate__: {err}. Expected 1, 2, or 3 items.")
        else:
            raise TypeError(f"Invalid item yielded/returned from __monk_validate__: {err}. Expected a string or tuple.")


def validate(instance: T) -> T:
    """
    Validates a Monk dataclass instance.
    Returns the instance so it can be used inline or reassigned.
    """
    if not hasattr(instance, "__monk_rules__"):
        raise TypeError(f"Object of type {type(instance).__name__} is not a valid Monk dataclass.")

    errors: list[ErrorDict] = []
    rules = object.__getattribute__(instance, "__monk_rules__")
    fields = object.__getattribute__(instance, "__monk_fields__")

    for field_name in fields:
        value = object.__getattribute__(instance, field_name)
        constraints = rules.get(field_name)

        if constraints:
            _validate_field_and_recurse(field_name, value, constraints, errors)
        elif type(value) not in _PRIMITIVE_TYPES:
            _recurse(value, field_name, errors)

    # Run cross-field validation ONLY if all field-level rules passed
    if not errors and hasattr(instance, "__monk_validate__"):
        hook = getattr(instance, "__monk_validate__")
        if inspect.iscoroutinefunction(hook) or inspect.isasyncgenfunction(hook):
            raise TypeError(
                "iron-monk is strictly synchronous. Async __monk_validate__ hooks are not supported. Stateful validation (like DB lookups) should occur in your service layer after syntax validation."
            )

        result = hook()
        _process_monk_validate_result(result, errors)

    if errors:
        raise ValidationError(errors)

    # Uncloak the instance
    object.__setattr__(instance, "__monk_safe__", True)

    return instance
