"""Runtime helpers for generated API base classes.

Provides helper functions used by the generated Tier-2 code:
``_op_meta``, ``_prepare_create_payload``, ``_preprocess_oneof_in_response``,
``_expand_label_filters``, etc.
"""

from __future__ import annotations

import sys
from pathlib import Path
from typing import Any, Mapping, get_args, get_origin

import httpx

from goodmem.errors import raise_for_status


def _op_meta(namespace: Any, op_name: str) -> dict[str, Any]:
    """Look up generated operation metadata for a CRUD method."""
    return (getattr(namespace, "_rest_metadata", {}) or {}).get(op_name, {})


def _supports_label_wildcard(op: dict[str, Any]) -> bool:
    return bool(op.get("supports_label_wildcard")) or "label.*" in (op.get("query_params") or [])


def _expand_label_filters(
    *,
    op: dict[str, Any],
    labels: Mapping[str, str] | None,
    params: dict[str, Any],
) -> dict[str, Any]:
    """Expand labels mapping to literal label.<key> query params when supported."""
    if labels is None:
        return params
    if not _supports_label_wildcard(op):
        raise ValueError("This endpoint does not support label.* query filters.")
    params.update({f"label.{k}": v for k, v in labels.items()})
    return params


def _normalize_multipart_file(file_value: Any) -> Any:
    """Normalize supported file inputs into an httpx files= payload tuple."""
    if isinstance(file_value, tuple):
        return file_value
    if isinstance(file_value, (bytes, bytearray)):
        return ("upload.bin", bytes(file_value), "application/octet-stream")
    if hasattr(file_value, "read"):
        raw_name = getattr(file_value, "name", "upload.bin")
        filename = Path(str(raw_name)).name
        return (filename or "upload.bin", file_value, "application/octet-stream")
    raise TypeError(
        "Multipart file must be a tuple, bytes, bytearray, or a file-like object with read()."
    )


def _is_oneof_wrapper(cls: type) -> bool:
    """Check if *cls* is an openapi-generator oneOf wrapper model.

    These wrappers have an ``actual_instance`` pydantic field and a
    ``one_of_schemas`` attribute (ClassVar or model field) listing the
    variant class names.
    """
    fields = getattr(cls, "model_fields", None)
    return (
        isinstance(cls, type)
        and fields is not None
        and "actual_instance" in fields
        and (hasattr(cls, "one_of_schemas") or "one_of_schemas" in fields)
    )


def _find_model_for_dict(d: dict[str, Any]) -> type | None:
    """Find a pydantic model whose fields match every key in *d*.

    Searches the generated models package for a model whose field names or
    aliases cover all keys in the dict.  Returns ``None`` if no match.

    This fixes a second openapi-generator limitation: oneOf variant fields are
    typed ``Optional[Any]`` instead of their proper typed model (e.g.
    ``RecursiveChunkingConfiguration``), so pydantic can't convert snake_case
    kwargs to camelCase aliases automatically.
    """
    models_module = sys.modules.get("goodmem._generated.models")
    if models_module is None:
        return None

    input_keys = set(d.keys())
    best_cls = None
    best_overlap = 0

    for attr_name in dir(models_module):
        cls = getattr(models_module, attr_name, None)
        if not isinstance(cls, type) or not hasattr(cls, "model_fields"):
            continue
        if _is_oneof_wrapper(cls):
            continue

        model_keys: set[str] = set()
        for fname, finfo in cls.model_fields.items():
            model_keys.add(fname)
            if finfo.alias:
                model_keys.add(finfo.alias)

        overlap = len(input_keys & model_keys)
        # All input keys must be recognized as fields/aliases — prevents false positives.
        if overlap >= len(input_keys) and overlap > best_overlap:
            best_overlap = overlap
            best_cls = cls

    return best_cls


def _resolve_inner_dicts(d: dict[str, Any]) -> dict[str, Any]:
    """Resolve inner dict values to typed model instances where possible.

    For each dict value inside *d*, tries to find a matching pydantic model
    in the generated models package.  If found, validates through it so that
    ``model_dump(by_alias=True)`` later produces correct camelCase keys.
    """
    result = dict(d)
    for key, val in result.items():
        if not isinstance(val, dict) or not val:
            continue
        model_cls = _find_model_for_dict(val)
        if model_cls is None:
            continue
        try:
            obj = model_cls.model_validate(val)
            # Serialize back to a plain dict with camelCase aliases and
            # primitive values (no enum instances) so it's JSON-safe.
            if hasattr(obj, "to_dict"):
                result[key] = obj.to_dict()
            else:
                result[key] = obj.model_dump(by_alias=True, mode="json")
        except Exception:
            pass  # dict doesn't fully match — keep as raw dict
    return result


def _resolve_oneof(wrapper_cls: type, value: Any) -> Any:
    """Build a proper oneOf wrapper from a raw dict using key-based dispatch.

    openapi-generator's ``from_dict()``/``from_json()`` is broken for
    discriminator-less oneOf schemas — it tries every variant and raises
    *"Multiple matches"* when variants have all-optional fields.

    This function matches the dict's keys against each variant's field names
    (including aliases) and picks the variant with the best overlap.  It also
    resolves inner dict values through typed models so that snake_case keys
    get properly aliased to camelCase.

    See: https://github.com/OpenAPITools/openapi-generator/issues/17763
    """
    if not isinstance(value, dict):
        return value

    wrapper_module = sys.modules.get(wrapper_cls.__module__)
    if wrapper_module is None:
        return value

    input_keys = set(value.keys())
    best_cls = None
    best_overlap = 0

    # one_of_schemas may be a ClassVar (patched) or a model field (unpatched).
    if hasattr(wrapper_cls, "one_of_schemas"):
        schema_names = wrapper_cls.one_of_schemas or set()
    else:
        schema_names = wrapper_cls.model_fields["one_of_schemas"].default or set()
    for schema_name in schema_names:
        variant_cls = getattr(wrapper_module, schema_name, None)
        if variant_cls is None or not hasattr(variant_cls, "model_fields"):
            continue

        variant_keys: set[str] = set()
        for fname, finfo in variant_cls.model_fields.items():
            variant_keys.add(fname)
            if finfo.alias:
                variant_keys.add(finfo.alias)

        overlap = len(input_keys & variant_keys)
        if overlap > best_overlap:
            best_overlap = overlap
            best_cls = variant_cls

    if best_cls is not None and best_overlap > 0:
        # Resolve inner dicts (e.g. {"chunk_size": 1024, ...}) through typed
        # models so snake_case keys serialize as camelCase aliases.
        resolved = _resolve_inner_dicts(value)
        inner = (
            best_cls.from_dict(resolved)
            if hasattr(best_cls, "from_dict")
            else best_cls.model_validate(resolved)
        )
        return wrapper_cls(inner)

    return value


def _unwrap_optional(annotation: Any) -> Any:
    """Unwrap ``Optional[T]`` (i.e. ``Union[T, None]``) to ``T``."""
    args = get_args(annotation)
    if args:
        non_none = [a for a in args if a is not type(None)]
        if len(non_none) == 1 and len(args) == 2:
            return non_none[0]
    return annotation


def _preprocess_oneof_in_kwargs(model_cls: type, kwargs: dict[str, Any]) -> dict[str, Any]:
    """Resolve raw dicts that map to oneOf wrapper fields before ``model_validate()``.

    Walks the model's field annotations, finds oneOf wrapper types, and converts
    plain-dict values to proper wrapper instances so pydantic can validate them.
    """
    if not hasattr(model_cls, "model_fields"):
        return kwargs

    result = dict(kwargs)
    for field_name, field_info in model_cls.model_fields.items():
        if field_name not in result or not isinstance(result[field_name], dict):
            continue
        field_type = _unwrap_optional(field_info.annotation)
        if _is_oneof_wrapper(field_type):
            result[field_name] = _resolve_oneof(field_type, result[field_name])
    return result


def _deep_resolve_any_fields(instance: Any) -> None:
    """Resolve ``Optional[Any]`` dict values to typed model instances in-place.

    oneOf variant classes (e.g. ``RetrievedItemOneOf1``) have their payload
    fields typed ``Optional[Any]`` by openapi-generator.  This means pydantic
    stores JSON objects as plain dicts, preventing attribute access.

    This function finds those dicts, matches them against known generated
    models (using ``_find_model_for_dict``), and replaces them with typed
    model instances so ``chunk.chunk.chunk_text`` works instead of requiring
    ``chunk["chunk"]["chunkText"]``.
    """
    cls = type(instance) if instance is not None else None
    if cls is None or not hasattr(cls, "model_fields"):
        return
    for fname, finfo in cls.model_fields.items():
        val = getattr(instance, fname, None)
        if not isinstance(val, dict) or len(val) < 2:
            continue
        inner_type = _unwrap_optional(finfo.annotation)
        if inner_type is not Any:
            continue
        model_cls = _find_model_for_dict(val)
        if model_cls is None:
            continue
        try:
            typed = (
                model_cls.from_dict(val)
                if hasattr(model_cls, "from_dict")
                else model_cls.model_validate(val)
            )
            setattr(instance, fname, typed)
        except Exception:
            pass


def _preprocess_oneof_in_response(model_cls: type, data: Any) -> Any:
    """Resolve oneOf wrapper dicts in a JSON response payload before ``model_validate()``.

    Response JSON uses camelCase aliases.  This walks *model_cls*'s fields,
    finds oneOf wrapper types, and builds proper wrapper instances from raw
    dicts so that ``model_validate()`` can handle them.

    Recurses into nested models and lists so deeply nested oneOf wrappers
    (e.g. ``RetrieveMemoryEvent.retrieved_item``) are resolved too.
    """
    if not isinstance(data, dict) or not hasattr(model_cls, "model_fields"):
        return data

    result = dict(data)

    for field_name, field_info in model_cls.model_fields.items():
        alias = field_info.alias or field_name
        # Check both alias (camelCase from server) and field_name (snake_case)
        if alias in result:
            key = alias
        elif field_name in result:
            key = field_name
        else:
            continue

        val = result[key]
        if val is None:
            continue

        field_type = _unwrap_optional(field_info.annotation)

        if isinstance(val, dict) and _is_oneof_wrapper(field_type):
            wrapper = _resolve_oneof(field_type, val)
            # Resolve Optional[Any] fields inside the variant to typed models
            if hasattr(wrapper, "actual_instance"):
                _deep_resolve_any_fields(wrapper.actual_instance)
            result[key] = wrapper
        elif isinstance(val, dict) and isinstance(field_type, type) and hasattr(field_type, "model_fields"):
            result[key] = _preprocess_oneof_in_response(field_type, val)
        elif isinstance(val, list):
            inner_type = _unwrap_list_inner(field_type)
            if inner_type is not None and hasattr(inner_type, "model_fields"):
                result[key] = [
                    _preprocess_oneof_in_response(inner_type, item)
                    if isinstance(item, dict) else item
                    for item in val
                ]

    return result


def _unwrap_list_inner(annotation: Any) -> type | None:
    """Extract ``T`` from ``List[T]`` or ``list[T]``, else ``None``."""
    origin = get_origin(annotation)
    if origin is list:
        args = get_args(annotation)
        if args:
            return args[0]
    return None


def _prepare_create_payload(
    *,
    create_model: type | None,
    op: dict[str, Any],
    kwargs: dict[str, Any],
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None]:
    """Build json/data/files payloads for create(), including multipart when declared."""
    supports_multipart = bool(op.get("supports_multipart")) or "multipart/form-data" in (
        op.get("body_kinds") or []
    )
    file_field = op.get("multipart_file_field") or "file"
    request_field = op.get("multipart_request_field") or "request"

    if supports_multipart and file_field in kwargs:
        payload = dict(kwargs)
        raw_file = payload.pop(file_field)
        raw_request = payload.pop(request_field, None)
        if raw_request is None:
            request_kwargs = payload
        elif isinstance(raw_request, dict):
            request_kwargs = raw_request
        elif hasattr(raw_request, "model_dump"):
            request_kwargs = raw_request.model_dump(by_alias=False, exclude_unset=True)
        else:
            raise TypeError(
                f"Multipart '{request_field}' must be a dict or pydantic model, got {type(raw_request)!r}."
            )

        body = create_model.model_validate(request_kwargs) if create_model is not None else None
        request_json = (
            body.model_dump_json(by_alias=True, exclude_unset=True)
            if body is not None
            else "{}"
        )
        data = {request_field: request_json}
        files = {file_field: _normalize_multipart_file(raw_file)}
        return None, data, files

    if create_model is not None:
        kwargs = _preprocess_oneof_in_kwargs(create_model, kwargs)
        body = create_model.model_validate(kwargs)
    else:
        body = None

    if body is not None and hasattr(body, "to_dict"):
        json_payload = body.to_dict()
    elif body is not None:
        json_payload = body.model_dump(by_alias=True, exclude_unset=True)
    else:
        json_payload = None
    return json_payload, None, None


"""CRUDNamespace and AsyncCRUDNamespace were removed — all generated classes
now inline __init__, response handling, and URL construction directly.  The
helper functions above (_op_meta, _prepare_create_payload, etc.) are still
used by the generated code.
"""
