"""Convenience registry — declarative source of truth for SDK convenience transforms.

Each entry in CONVENIENCE_REGISTRY maps a (namespace, method) pair to:
  - An optional description override for documentation
  - Convenience params (with type, description, maps_to/replaces for doc generation)
  - An optional transform function applied at runtime by apply_convenience_transforms()

Transform functions mutate kwargs in-place (pop convenience params, inject API params).

Param specs:
  - "type": Only for convenience-only params NOT on the pydantic request model.
    Model-field types are derived automatically from the model.
  - "description": Overrides the Tier-2 (auto-generated) description for docs.
  - "maps_to": Dot-path to the Tier-2 param this convenience param transforms
    into.  The original Tier-2 param stays visible in docs (parallel params).
    Use this when both the convenience shorthand and the raw Tier-2 param are
    valid ways to call the method (e.g., api_key alongside credentials).
  - "replaces": Name of the Tier-2 param this convenience param fully aliases.
    The original is suppressed from docs.  Use this for 1:1 renames where
    exposing both would confuse users (e.g., page_size replaces max_results).
  - "required": Overrides the required/optional status. When omitted, inherited
    from Tier-2 (if same name) or defaults to optional.
  - "default": Default value shown in the signature.  When present, the param
    is automatically optional.
"""

from __future__ import annotations

from typing import Any

from goodmem._registries import (
    EMBEDDER_MODEL_REGISTRY,
    EMBEDDER_PROVIDER_ENDPOINTS,
    LLM_MODEL_REGISTRY,
    LLM_PROVIDER_ENDPOINTS,
    RERANKER_MODEL_REGISTRY,
    RERANKER_PROVIDER_ENDPOINTS,
)
from goodmem._schema_bridge import DEFAULT_DISTRIBUTION_TYPE, build_api_key_credentials

# ---------------------------------------------------------------------------
# Default chunking config applied to spaces.create when none is provided
# ---------------------------------------------------------------------------

DEFAULT_CHUNKING_CONFIG: dict[str, Any] = {
    "recursive": {
        "chunk_size": 512,
        "chunk_overlap": 64,
        "keep_strategy": "KEEP_END",
        "length_measurement": "CHARACTER_COUNT",
    }
}

# Full factory class name used when assembling a PostProcessor from flat kwargs
_POST_PROCESSOR_FACTORY = "com.goodmem.retrieval.postprocess.ChatPostProcessorFactory"

# Flat kwargs that map into post_processor.config
_POST_PROCESSOR_FLAT_KEYS = frozenset({
    "llm_id",
    "reranker_id",
    "llm_temp",
    "gen_token_budget",
    "relevance_threshold",
    "max_results",
    "prompt",
    "sys_prompt",
    "chronological_resort",
})


# ---------------------------------------------------------------------------
# Transform helpers
# ---------------------------------------------------------------------------

def _apply_api_key_transform(kwargs: dict[str, Any]) -> None:
    """Convert api_key shorthand to full credentials structure.

    Mutual exclusivity with ``credentials`` is enforced automatically by
    ``apply_convenience_transforms()`` via the ``"maps_to"`` registry field.
    """
    api_key = kwargs.pop("api_key", None)
    if api_key is None:
        return
    kwargs["credentials"] = build_api_key_credentials(api_key)


def _apply_model_registry(
    kwargs: dict[str, Any],
    registry: dict[str, dict],
    provider_endpoints: dict[str, str],
) -> None:
    """Auto-fill fields from model registry when model_identifier matches."""
    model_id = kwargs.get("model_identifier")
    if not model_id:
        return

    info = registry.get(model_id)
    if info is None:
        return

    # Set fields from registry only when not already explicitly provided
    for key, value in info.items():
        if value is not None:
            kwargs.setdefault(key, value)

    # Infer endpoint_url from provider_type if not set
    provider = kwargs.get("provider_type")
    if provider and "endpoint_url" not in kwargs:
        endpoint = provider_endpoints.get(provider)
        if endpoint:
            kwargs["endpoint_url"] = endpoint


def _apply_endpoint_inference(
    kwargs: dict[str, Any],
    provider_endpoints: dict[str, str],
) -> None:
    """Infer endpoint_url from provider_type when not explicitly set."""
    if "endpoint_url" in kwargs:
        return
    provider = kwargs.get("provider_type")
    if provider:
        endpoint = provider_endpoints.get(provider)
        if endpoint:
            kwargs["endpoint_url"] = endpoint


# ---------------------------------------------------------------------------
# Per-method transforms
# ---------------------------------------------------------------------------

def _transform_embedder_create(kwargs: dict[str, Any]) -> None:
    """Embedder create: model registry, api_key, default distribution_type."""
    _apply_api_key_transform(kwargs)
    _apply_model_registry(kwargs, EMBEDDER_MODEL_REGISTRY, EMBEDDER_PROVIDER_ENDPOINTS)
    _apply_endpoint_inference(kwargs, EMBEDDER_PROVIDER_ENDPOINTS)
    # Registry stores dimensions as a nested dict {default, range, allowed}.
    # Extract dimensions.default → dimensionality (the API field name).
    dims = kwargs.pop("dimensions", None)
    if isinstance(dims, dict):
        kwargs.setdefault("dimensionality", dims.get("default"))
    kwargs.setdefault("distribution_type", DEFAULT_DISTRIBUTION_TYPE)


def _transform_llm_create(kwargs: dict[str, Any]) -> None:
    """LLM create: model registry, api_key, endpoint inference."""
    _apply_api_key_transform(kwargs)
    _apply_model_registry(kwargs, LLM_MODEL_REGISTRY, LLM_PROVIDER_ENDPOINTS)
    _apply_endpoint_inference(kwargs, LLM_PROVIDER_ENDPOINTS)


def _transform_reranker_create(kwargs: dict[str, Any]) -> None:
    """Reranker create: model registry, api_key, endpoint inference."""
    _apply_api_key_transform(kwargs)
    _apply_model_registry(kwargs, RERANKER_MODEL_REGISTRY, RERANKER_PROVIDER_ENDPOINTS)
    _apply_endpoint_inference(kwargs, RERANKER_PROVIDER_ENDPOINTS)


def _transform_space_create(kwargs: dict[str, Any]) -> None:
    """Space create: default chunking config."""
    kwargs.setdefault("default_chunking_config", DEFAULT_CHUNKING_CONFIG)


def _transform_retrieve(kwargs: dict[str, Any]) -> None:
    """Retrieve: space_ids→space_keys, flat post-processor kwargs→nested structure.

    Mutual exclusivity (space_ids vs space_keys, flat post-processor params vs
    post_processor) is enforced automatically by ``apply_convenience_transforms()``
    via the ``"maps_to"`` registry fields.
    """
    # --- space_ids → space_keys ---
    space_ids = kwargs.pop("space_ids", None)
    if space_ids is not None:
        if isinstance(space_ids, str):
            space_ids = [space_ids]
        kwargs["space_keys"] = [{"space_id": sid} for sid in space_ids]

    # --- post-processor flattening ---
    # Assembles flat kwargs (llm_id, reranker_id, …) into the nested PostProcessor
    # structure: {"name": <factory FQN>, "config": {<flat kwargs>}}.
    # The result is injected as a raw dict in memories.py:_build_retrieve_body()
    # because PostProcessor.config is untyped in the current OpenAPI spec.
    # TODO(#847): Once the server defines a typed ChatPostProcessorConfig schema,
    # use the generated pydantic model here instead of a raw dict.
    flat_pp_kwargs = {}
    for key in _POST_PROCESSOR_FLAT_KEYS:
        if key in kwargs:
            flat_pp_kwargs[key] = kwargs.pop(key)

    if flat_pp_kwargs:
        kwargs["post_processor"] = {
            "name": _POST_PROCESSOR_FACTORY,
            "config": flat_pp_kwargs,
        }


# ---------------------------------------------------------------------------
# Type validation
# ---------------------------------------------------------------------------

_SCALAR_TYPES: dict[str, tuple[type, ...]] = {
    "str": (str,),
    "int": (int,),
    "float": (int, float),
    "bool": (bool,),
}


def _validate_type(pname: str, value: Any, declared: str) -> None:
    """Raise TypeError if *value* doesn't match the declared type string.

    Handles scalar types (str, int, float, bool) and ``list[X]`` generics.
    Rejects ``bool`` for int/float params (bool is a subclass of int in Python).
    """
    # bool is a subclass of int — reject it for int/float params.
    if isinstance(value, bool) and declared in ("int", "float"):
        raise TypeError(f"'{pname}' expected {declared}, got bool")

    if declared in _SCALAR_TYPES:
        if not isinstance(value, _SCALAR_TYPES[declared]):
            raise TypeError(
                f"'{pname}' expected {declared}, got {type(value).__name__}"
            )
        return

    # list[X] — check outer type + elements.  Accept a bare scalar as
    # shorthand (the transform will wrap it into a list).
    if declared.startswith("list[") and declared.endswith("]"):
        elem_type = declared[5:-1]
        elem_types = _SCALAR_TYPES.get(elem_type)
        if elem_types and isinstance(value, elem_types):
            return  # bare scalar accepted as shorthand
        if not isinstance(value, list):
            raise TypeError(
                f"'{pname}' expected {declared}, got {type(value).__name__}"
            )
        if elem_types:
            for i, item in enumerate(value):
                if isinstance(item, bool) and elem_type in ("int", "float"):
                    raise TypeError(
                        f"'{pname}[{i}]' expected {elem_type}, got bool"
                    )
                if not isinstance(item, elem_types):
                    raise TypeError(
                        f"'{pname}[{i}]' expected {elem_type}, "
                        f"got {type(item).__name__}"
                    )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def apply_convenience_transforms(
    method_key: tuple[str, str],
    kwargs: dict[str, Any],
) -> None:
    """Apply registered convenience transforms for (namespace, method).

    Mutates *kwargs* in-place: pops convenience params, injects API-level params.
    No-op if *method_key* has no registry entry or no transform.

    Before running transforms, automatically enforces constraints derived
    from the registry:

    - ``"replaces"`` — the old Tier-2 param is fully aliased and must not be
      used directly.
    - ``"maps_to"`` — the convenience param and the Tier-2 param it maps to
      are mutually exclusive (both cannot be set at the same time).
    - ``"type"`` — convenience-only params with a declared type are validated
      via ``isinstance`` (raises ``TypeError`` on mismatch).
    """
    entry = CONVENIENCE_REGISTRY.get(method_key)
    if entry is None:
        return
    for pname, pspec in entry.get("params", {}).items():
        # "replaces" — reject the old Tier-2 param entirely.
        replaced = pspec.get("replaces")
        if replaced and replaced in kwargs:
            raise ValueError(
                f"Use '{pname}' instead of '{replaced}'. "
                f"'{replaced}' has been renamed to '{pname}' in this SDK."
            )
        # "maps_to" — enforce mutual exclusivity when both are present.
        maps_to = pspec.get("maps_to")
        if maps_to and pname in kwargs:
            root = maps_to.split(".")[0]
            if root in kwargs:
                raise ValueError(
                    f"Cannot specify both '{pname}' and '{root}'. "
                    f"Use either '{pname}' (SDK) or '{root}' (REST), not both."
                )
        # Type validation for convenience-only params with declared types.
        # Workaround until server-side OpenAPI spec gains proper typed models
        # for PostProcessor.config et al. (https://github.com/PAIR-Systems-Inc/goodmem/issues/870)
        declared_type = pspec.get("type")
        if declared_type and pname in kwargs:
            _validate_type(pname, kwargs[pname], declared_type)
    transform = entry.get("transform")
    if transform is not None:
        transform(kwargs)


def _transform_list_kwargs(kwargs: dict[str, Any]) -> int | None:
    """Transform list-method convenience kwargs.

    - page_size → max_results (rename)
    - max_items → extracted, returned to caller (for paginator._max_items)
    - next_token → suppressed (paginator handles it internally)

    Returns max_items value or None.
    """
    page_size = kwargs.pop("page_size", None)
    if page_size is not None:
        kwargs["max_results"] = page_size

    # Extract max_items (not an API param — used by the paginator)
    max_items = kwargs.pop("max_items", None)

    # Suppress next_token (paginator manages tokens internally)
    kwargs.pop("next_token", None)

    return max_items


# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------

CONVENIENCE_REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
    # ==================================================================
    # EMBEDDERS
    # ==================================================================
    ("embedders", "create"): {
        "transform": _transform_embedder_create,
        "params": {
            "api_key": {
                "type": "str",
                "description": (
                    "Converts a plain API key string "
                    "to the full `EndpointAuthentication` structure "
                    '(i.e. `{"kind": "CREDENTIAL_KIND_API_KEY", "api_key": '
                    '{"inline_secret": "sk-..."}}`). Most SaaS/cloud and/or '
                    "proprietary providers (e.g., OpenAI, Cohere, Jina, Voyage, "
                    "OpenRouter, Gemini) require an API key."
                ),
                "maps_to": "credentials",
            },
            "model_identifier": {
                "description": (
                    "The string that identifies the embedder. Usually the model "
                    "identifier assigned by HuggingFace or the LLM provider, "
                    'e.g., `"text-embedding-3-small"` by OpenAI. '
                    "When a known model, auto-fills `provider_type`, `endpoint_url`, "
                    "`dimensionality`, `max_sequence_length`, `supported_modalities`, "
                    "and `distribution_type` for 29 registered models."
                ),
            },
            "credentials": {
                "description": (
                    "Structured credential payload describing how to authenticate "
                    "with the provider."
                ),
            },
            "dimensionality": {
                "required": False,
                "description": (
                    "Output vector dimensions. Auto-inferred from `model_identifier` "
                    "for known models (using `dimensions.default` from the model "
                    "registry); required when `model_identifier` is not in "
                    "the model registry."
                ),
            },
            "distribution_type": {
                "description": (
                    'The distribution type of the embedder\'s vector output. '
                    'Defaults to `"DENSE"` when not specified.'
                ),
                "default": '"DENSE"',
            },
            "endpoint_url": {
                "required": False,
                "description": (
                    "Base URL for the embedding endpoint. Auto-inferred from "
                    "`provider_type` for known providers."
                ),
            },
            "max_sequence_length": {
                "description": (
                    "Maximum token length accepted by the model. Auto-inferred "
                    "from `model_identifier` for known models; required when "
                    "`model_identifier` is not in the registry."
                ),
            },
            "provider_type": {
                "required": False,
                "description": (
                    'Provider backend (e.g. `"OPENAI"`, `"COHERE"`, `"VLLM"`, `"TEI"`). Auto-inferred from `model_identifier` for '
                    "known models."
                ),
            },
            "supported_modalities": {
                "description": (
                    'Modalities supported by this embedder (e.g. `["TEXT"]`). '
                    "Auto-inferred from `model_identifier` for known models; "
                    "required when `model_identifier` is not in the registry."
                ),
            },
        },
    },
    ("embedders", "list"): {
        "params": {
            "labels": {
                "description": (
                    "Filters to match embedders by labels attached via "
                    "create/update. You can verify these labels in "
                    "`EmbedderResponse.labels`. Example: "
                    '`labels={"env":"prod", "team":"nlp"}` matches embedders '
                    "whose `env` label is `prod` and `team` label is `nlp`. "
                    "The dict is translated to `label.env=prod&label.team=nlp` "
                    "for the underlying REST GET request."
                ),
            },
        },
    },
    # ==================================================================
    # LLMS
    # ==================================================================
    ("llms", "create"): {
        "transform": _transform_llm_create,
        "params": {
            "api_key": {
                "type": "str",
                "description": (
                    "Converts a plain API key string "
                    "to the full `EndpointAuthentication` structure "
                    '(i.e. `{"kind": "CREDENTIAL_KIND_API_KEY", "api_key": '
                    '{"inline_secret": "sk-..."}}`).'
                ),
                "maps_to": "credentials",
            },
            "model_identifier": {
                "description": (
                    "When a known model, auto-fills `provider_type`, `endpoint_url`, "
                    "`max_context_length`, and `supported_modalities` for 34 "
                    "registered models."
                ),
            },
            "api_path": {
                "description": (
                    "API path for chat/completions request (defaults to "
                    "`/chat/completions` if not provided)."
                ),
            },
            "capabilities": {
                "description": (
                    "LLM capabilities defining supported features and modes. "
                    "Optional — server infers capabilities from model identifier "
                    "if not provided."
                ),
            },
            "credentials": {
                "description": (
                    "Structured credential payload describing how to authenticate "
                    "with the provider."
                ),
            },
            "endpoint_url": {
                "required": False,
                "description": (
                    "Base URL for the LLM endpoint (OpenAI-compatible base, "
                    "typically ends with `/v1`). Auto-inferred from `provider_type` "
                    "for known providers; required when `model_identifier` is not "
                    "in the registry."
                ),
            },
            "max_context_length": {
                "description": (
                    "Maximum context window size in tokens. Auto-inferred from "
                    "`model_identifier` for known models; recommended when "
                    "`model_identifier` is not in the registry."
                ),
            },
            "provider_type": {
                "required": False,
                "description": (
                    'Provider backend (e.g. `"OPENAI"`, `"VLLM"`). Auto-inferred '
                    "from `model_identifier` for known models; required when "
                    "`model_identifier` is not in the registry."
                ),
            },
            "supported_modalities": {
                "description": (
                    'Modalities supported by this LLM (e.g. `["TEXT"]`). '
                    "Auto-inferred from `model_identifier` for known models; "
                    'defaults to `["TEXT"]` on the server if omitted.'
                ),
            },
        },
    },
    ("llms", "list"): {
        "params": {
            "labels": {
                "description": (
                    "Filters to match LLMs by labels attached via create/update. "
                    "You can verify these labels in `LLMResponse.labels`. Example: "
                    '`labels={"env":"prod", "team":"nlp"}` matches LLMs whose `env` '
                    "label is `prod` and `team` label is `nlp`. The dict is translated "
                    "to `label.env=prod&label.team=nlp` for the underlying REST "
                    "GET request."
                ),
            },
        },
    },
    # ==================================================================
    # RERANKERS
    # ==================================================================
    ("rerankers", "create"): {
        "transform": _transform_reranker_create,
        "params": {
            "api_key": {
                "type": "str",
                "description": (
                    "Converts a plain API key string "
                    "to the full `EndpointAuthentication` structure "
                    '(i.e. `{"kind": "CREDENTIAL_KIND_API_KEY", "api_key": '
                    '{"inline_secret": "sk-..."}}`).'
                ),
                "maps_to": "credentials",
            },
            "model_identifier": {
                "description": (
                    "When a known model, auto-fills `provider_type`, `endpoint_url`, "
                    "and `supported_modalities` for 16 registered models."
                ),
            },
            "api_path": {
                "description": (
                    "API path for reranking request (defaults: Cohere `/v2/rerank`, "
                    "Jina `/v1/rerank`, others `/rerank`)."
                ),
            },
            "credentials": {
                "description": (
                    "Structured credential payload describing how to authenticate "
                    "with the provider."
                ),
            },
            "endpoint_url": {
                "required": False,
                "description": (
                    "Base URL for the reranking endpoint. Auto-inferred from "
                    "`provider_type` for known providers; required when "
                    "`model_identifier` is not in the registry."
                ),
            },
            "provider_type": {
                "required": False,
                "description": (
                    'Provider backend (e.g. `"COHERE"`, `"JINA"`). Auto-inferred '
                    "from `model_identifier` for known models; required when "
                    "`model_identifier` is not in the registry."
                ),
            },
            "supported_modalities": {
                "description": (
                    'Modalities supported by this reranker (e.g. `["TEXT"]`). '
                    "Auto-inferred from `model_identifier` for known models; "
                    'defaults to `["TEXT"]` on the server if omitted.'
                ),
            },
        },
    },
    ("rerankers", "list"): {
        "params": {
            "labels": {
                "description": (
                    "Filters to match rerankers by labels attached via "
                    "create/update. You can verify these labels in "
                    "`RerankerResponse.labels`. Example: "
                    '`labels={"env":"prod", "team":"nlp"}` matches rerankers '
                    "whose `env` label is `prod` and `team` label is `nlp`. "
                    "The dict is translated to `label.env=prod&label.team=nlp` "
                    "for the underlying REST GET request."
                ),
            },
        },
    },
    # ==================================================================
    # SPACES
    # ==================================================================
    ("spaces", "create"): {
        "transform": _transform_space_create,
        "params": {
            "name": {
                "description": (
                    "The desired name for the space. Must be unique within "
                    "the user's scope."
                ),
            },
            "default_chunking_config": {
                "required": False,
                "description": (
                    "If omitted, the SDK applies recursive chunking with "
                    "`chunk_size=512`, `chunk_overlap=64`, "
                    '`keep_strategy="KEEP_END"`, '
                    '`length_measurement="CHARACTER_COUNT"`. '
                    "Specify explicitly to override."
                ),
            },
            "labels": {
                "description": (
                    "A set of key-value pairs to categorize or tag the space. "
                    "Used for filtering and organizational purposes."
                ),
            },
            "public_read": {
                "default": "False",
            },
        },
    },
    ("spaces", "list"): {
        "description": (
            "List spaces accessible to the caller, with optional "
            "filtering by owner, labels, and name. Results are paginated — use "
            "`page_size` to control items per page"
            " and `max_items` to cap total results. Pagination is "
            "handled automatically by the returned `Paginator`."
        ),
        "hide": ["next_token"],
        "params": {
            "labels": {
                "description": (
                    "Filters to match spaces by labels attached via "
                    "create/update. You can verify these labels in "
                    "`Space.labels`. Example: "
                    '`labels={"env":"prod", "team":"nlp"}` matches spaces '
                    "whose `env` label is `prod` and `team` label is `nlp`. "
                    "The dict is translated to `label.env=prod&label.team=nlp` "
                    "for the underlying REST GET request."
                ),
            },
            "name_filter": {
                "description": (
                    "Filter spaces by name using glob pattern matching."
                ),
            },
            "sort_by": {
                "description": (
                    "Field to sort by: `'created_at'` or `'name'` "
                    "(default: `'created_at'`). Unsupported values return "
                    "INVALID_ARGUMENT."
                ),
            },
            "sort_order": {
                "description": (
                    "Sort order (`ASCENDING` or `DESCENDING`, "
                    "default: `DESCENDING`)."
                ),
            },
            "page_size": {
                "type": "int",
                "description": (
                    "Number of results per page (defaults to 50, clamped to "
                    "[1, 1000] by the server)."
                ),
                "replaces": "max_results",
            },
            "max_items": {
                "type": "int",
                "description": (
                    "Maximum total number of items to return across all pages."
                ),
            },
        },
    },
    # ==================================================================
    # MEMORIES
    # ==================================================================
    ("memories", "create"): {
        "description": (
            "Create a memory from text content, base64-encoded content, or "
            "a local file. Content must be provided via exactly one of "
            "`file_path`, `original_content`, or `original_content_b64`."
        ),
        "params": {
            "file_path": {
                "type": "str",
                "description": (
                    "Path to a local file to upload. Mutually exclusive with "
                    "`original_content` and `original_content_b64`."
                ),
            },
            "content_type": {
                "required": False,
                "description": (
                    'MIME type of the content. Auto-inferred as `"text/plain"` '
                    "for `original_content`, or from the file extension for "
                    "`file_path`. Required when using `original_content_b64`."
                ),
            },
            "original_content": {
                "description": (
                    "Original content as plain text. Mutually exclusive with "
                    "`file_path` and `original_content_b64`."
                ),
            },
            "original_content_b64": {
                "description": (
                    "Original content as base64-encoded binary data. Mutually "
                    "exclusive with `file_path` and `original_content`."
                ),
            },
            "original_content_ref": {
                "description": (
                    "Reference to external content location. Functions as a "
                    "metadata field. Does not make Goodmem download the content "
                    "from the URL and use it to create memory."
                ),
            },
            "metadata": {
                "description": (
                    "Metadata for the memory. Any JSON string. Can be nested. "
                    "Can be used for filtering in memory list operation. "
                    '(e.g. `{"author": "John Doe", "tags": ["production", '
                    '"urgent"]}`)'
                ),
            },
        },
    },
    ("memories", "list"): {
        "description": (
            "Lists memories within a given space. Results are paginated — "
            "use `page_size` to control items per page "
            "and `max_items` to cap total results. Pagination is "
            "handled automatically by the returned `Paginator`."
        ),
        "hide": ["next_token"],
        "params": {
            "filter": {
                "description": (
                    "Metadata filter expression for list results. "
                    "See [Metadata Filters Guide](../../../../how-to/metadata-filters) "
                    "and [Filter Expressions Reference](../../filter-expressions) "
                    "for usage."
                ),
            },
            "include_content": {
                "default": "False",
            },
            "page_size": {
                "type": "int",
                "description": (
                    "Number of results per page (defaults to 50, clamped to "
                    "[1, 500] by the server). A smaller value will reduce the "
                    "latency of each page — so you get items faster each page, "
                    "but increase the number of requests made to the server. "
                    "Use `max_items` to cap total results."
                ),
                "replaces": "max_results",
            },
            "max_items": {
                "type": "int",
                "description": (
                    "Maximum total number of items to return across all pages. "
                    "When set, iteration stops after this many items. If left "
                    "unset, all pages will be fetched."
                ),
            },
        },
    },
    ("memories", "retrieve"): {
        "title": "Retrieve Memories",
        "description": (
            "Performs a streaming semantic search across one or more memory "
            "spaces and returns matching chunks ranked by relevance as well as LLM-postprocessed results (like summarization, question-answering, etc.)."
        ),
        "transform": _transform_retrieve,
        "hide": ["format"],
        "params": {
            "space_ids": {
                "type": "list[str]",
                "required": False,
                "description": (
                    "A list of space UUID strings, converted to the "
                    "`space_keys` structure the API requires."
                ),
                "maps_to": "space_keys",
            },
            "space_keys": {
                "required": False,
                "description": (
                    "Full space configuration for retrieval — a list of "
                    "`SpaceKey` dicts, each with a required `space_id` and optional "
                    "`embedder_weights` (per-embedder weight overrides) and "
                    "`filter` (metadata filter expression). Use this instead of "
                    "`space_ids` when you need per-space weight tuning or "
                    "filtering."
                ),
            },
            "stream": {
                "type": "bool",
                "description": (
                    "If `True` (default), returns a `RetrieveMemoryStream` context "
                    "manager that yields events as they arrive from the server. "
                    "If `False`, collects all events and returns a plain "
                    "`list[RetrieveMemoryEvent]`."
                ),
                "default": "True",
            },
            "fetch_memory": {
                "description": (
                    "If `True` (default), memory definition events are streamed. "
                    "Set to `False` to skip them."
                ),
                "default": "True",
            },
            "fetch_memory_content": {
                "description": (
                    "If `True`, includes the raw content of each memory in the "
                    "response. Only applicable when `fetch_memory` is `True`."
                ),
                "default": "False",
            },
            "llm_id": {
                "type": "str",
                "description": (
                    "The ID of the LLM to process the retrieved memories, "
                    "e.g., RAG. Assembles the nested `PostProcessor` structure "
                    "automatically. If unset, no LLM will be used. "
                    "Setting `llm_id` or `reranker_id` activates post-processing; "
                    "the remaining post-processor params below (`llm_temp`, "
                    "`gen_token_budget`, etc.) only take effect when at least one "
                    "is set."
                ),
                "maps_to": "post_processor.config.llm_id",
            },
            "reranker_id": {
                "type": "str",
                "description": (
                    "The ID of the reranker to process the retrieved memories. "
                    "If unset, no reranker will be used."
                ),
                "maps_to": "post_processor.config.reranker_id",
            },
            "llm_temp": {
                "type": "float",
                "description": (
                    "LLM temperature for post-processing. "
                    "Valid range is 0.0-2.0. "
                    "Only applies when `llm_id` is set."
                ),
                "default": "0.3",
                "maps_to": "post_processor.config.llm_temp",
            },
            "gen_token_budget": {
                "type": "int",
                "description": (
                    "Token budget for LLM post-processing. Must be positive. "
                    "If the token budget is insufficient, the server will "
                    "return an error. "
                    "Only applies when `llm_id` is set."
                ),
                "default": "512",
                "maps_to": "post_processor.config.gen_token_budget",
            },
            "relevance_threshold": {
                "type": "float",
                "description": (
                    "Minimum relevance score for retrieved memories. "
                    "Only applies when `reranker_id` is set."
                ),
                "maps_to": "post_processor.config.relevance_threshold",
            },
            "max_results": {
                "type": "int",
                "description": (
                    "Maximum number of retrieved memories to return. Must be "
                    "positive. Only applies when `llm_id` or `reranker_id` "
                    "is set."
                ),
                "default": "10",
                "maps_to": "post_processor.config.max_results",
            },
            "prompt": {
                "type": "str",
                "description": (
                    "Custom prompt for LLM post-processing. If unset, the "
                    "server's default prompt is used. "
                    "Only applies when `llm_id` is set."
                ),
                "maps_to": "post_processor.config.prompt",
            },
            "sys_prompt": {
                "type": "str",
                "description": (
                    "System prompt for LLM post-processing. If unset, the "
                    "server's default system prompt is used. "
                    "Only applies when `llm_id` is set."
                ),
                "maps_to": "post_processor.config.sys_prompt",
            },
            "chronological_resort": {
                "type": "bool",
                "description": (
                    "Re-sort retrieved memories chronologically after semantic "
                    "ranking. Only applies when `llm_id` or `reranker_id` "
                    "is set."
                ),
                "default": "True",
                "maps_to": "post_processor.config.chronological_resort",
            },
        },
    },
}
