"""Convenience transform runtime — loads convenience.json and implements transform primitives.

Drop-in replacement for the runtime portion of registries/convenience.py.
The transform pipeline is driven by convenience.json (declarative, language-agnostic).
This module implements the Python-specific runtime for those transforms.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from goodmem._registries import (
    EMBEDDER_MODEL_REGISTRY,
    LLM_MODEL_REGISTRY,
    PROVIDER_ENDPOINTS,
    PROVIDER_REGISTRY,
    RERANKER_MODEL_REGISTRY,
)
from goodmem._schema_bridge import build_api_key_credentials

# ---------------------------------------------------------------------------
# Load convenience.json (bundled in the package at build time)
# ---------------------------------------------------------------------------

_CONVENIENCE_PATH = Path(__file__).parent / "convenience.json"
if _CONVENIENCE_PATH.exists():
    with open(_CONVENIENCE_PATH) as _f:
        _CONVENIENCE: dict[str, Any] = json.load(_f)
else:
    _CONVENIENCE = {}

_DEFAULTS: dict[str, Any] = _CONVENIENCE.get("defaults", {})
_METHODS: dict[str, Any] = _CONVENIENCE.get("methods", {})

# Re-export for backward compatibility
DEFAULT_CHUNKING_CONFIG: dict[str, Any] = dict(_DEFAULTS.get("chunking_config", {}))

# CONVENIENCE_REGISTRY as a dict keyed by (namespace, method) tuples — for backward compat
# with code that reads the registry structure (tests, doc generation).
CONVENIENCE_REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
    tuple(k.split(".", 1)): v for k, v in _METHODS.items()  # type: ignore[misc]
}

# Registry indexed by registry name
_REGISTRIES: dict[str, dict[str, dict]] = {
    "embedders": EMBEDDER_MODEL_REGISTRY,
    "llms": LLM_MODEL_REGISTRY,
    "rerankers": RERANKER_MODEL_REGISTRY,
}


# ---------------------------------------------------------------------------
# Transform primitives
# ---------------------------------------------------------------------------

def _apply_api_key_to_credentials(kwargs: dict[str, Any], _config: dict) -> None:
    """Pop api_key, build credential struct."""
    api_key = kwargs.pop("api_key", None)
    if api_key is None:
        return
    kwargs["credentials"] = build_api_key_credentials(api_key)


def _apply_registry_lookup(kwargs: dict[str, Any], config: dict) -> None:
    """Auto-fill fields from model registry when model_identifier matches."""
    registry_name = config.get("registry", "")
    registry = _REGISTRIES.get(registry_name, {})

    model_id = kwargs.get("model_identifier")
    if not model_id:
        return

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

    for key, value in info.items():
        if value is not None:
            kwargs.setdefault(key, value)

    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], config: dict) -> 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


def _apply_dimensions_to_dimensionality(kwargs: dict[str, Any], _config: dict) -> None:
    """Extract dimensions.default → dimensionality."""
    dims = kwargs.pop("dimensions", None)
    if isinstance(dims, dict):
        kwargs.setdefault("dimensionality", dims.get("default"))


def _apply_inject_default(kwargs: dict[str, Any], config: dict) -> None:
    """Set kwargs.setdefault(field, value) using value resolved from defaults."""
    field = config["field"]
    value_key = config.get("value_key", "")
    value = _resolve_value(value_key)
    if value is not None:
        if isinstance(value, dict):
            value = dict(value)  # shallow copy to avoid mutation
        kwargs.setdefault(field, value)


def _apply_space_ids_to_space_keys(kwargs: dict[str, Any], _config: dict) -> None:
    """Convert space_ids list to space_keys structure."""
    space_ids = kwargs.pop("space_ids", None)
    if space_ids is not None:
        kwargs["space_keys"] = [{"space_id": sid} for sid in space_ids]


def _apply_credential_check(kwargs: dict[str, Any], _config: dict) -> None:
    """Raise ValueError if the resolved endpoint is a known SaaS host and no credentials were provided."""
    if kwargs.get("credentials"):
        return  # api_key already converted to credentials, or user passed credentials directly
    provider = kwargs.get("provider_type")
    endpoint_url = kwargs.get("endpoint_url")
    if not provider or not endpoint_url:
        return
    provider_meta = PROVIDER_REGISTRY.get(provider)
    if provider_meta is None:
        return
    endpoint_host = urlparse(endpoint_url).netloc
    for saas_url in provider_meta.get("base_urls_requiring_api_key", []):
        if urlparse(saas_url).netloc == endpoint_host:
            raise ValueError(
                f"Provider '{provider}' at '{endpoint_url}' requires an API key. "
                f"Pass api_key=\"sk-...\" to the create call."
            )


def _apply_flat_to_nested(kwargs: dict[str, Any], config: dict) -> None:
    """Assemble flat kwargs into nested struct.

    When ``activating_keys`` is specified, the nested struct is only created
    if at least one activating key is present in the flat values. This
    prevents default-valued config params (e.g., ``llm_temp``,
    ``gen_token_budget``) from triggering struct creation when the user
    didn't request post-processing.
    """
    target = config["target"]
    factory_key = config.get("factory_key", "")
    flat_keys_key = config.get("flat_keys_key", "")
    activating_keys = config.get("activating_keys", [])

    factory = _resolve_value(factory_key) if factory_key else None
    flat_keys = _resolve_value(flat_keys_key) if flat_keys_key else []

    flat_values = {}
    for key in flat_keys:
        if key in kwargs:
            flat_values[key] = kwargs.pop(key)

    if not flat_values:
        return

    # Only build nested struct if an activating key is present (or none required)
    if activating_keys and not any(k in flat_values for k in activating_keys):
        return

    result: dict[str, Any] = {"config": flat_values}
    if factory:
        result["name"] = factory
    kwargs[target] = result


def _resolve_value(key: str) -> Any:
    """Resolve a dotted key from _DEFAULTS, e.g., 'defaults.chunking_config'."""
    if not key:
        return None
    parts = key.split(".")
    if parts[0] == "defaults":
        obj: Any = _DEFAULTS
        for part in parts[1:]:
            if isinstance(obj, dict):
                obj = obj.get(part)
            else:
                return None
        return obj
    return None


# ---------------------------------------------------------------------------
# Transform dispatch
# ---------------------------------------------------------------------------

_TRANSFORM_DISPATCH: dict[str, Any] = {
    "api_key_to_credentials": _apply_api_key_to_credentials,
    "registry_lookup": _apply_registry_lookup,
    "endpoint_inference": _apply_endpoint_inference,
    "dimensions_to_dimensionality": _apply_dimensions_to_dimensionality,
    "inject_default": _apply_inject_default,
    "space_ids_to_space_keys": _apply_space_ids_to_space_keys,
    "flat_to_nested": _apply_flat_to_nested,
    "credential_check": _apply_credential_check,
}


# ---------------------------------------------------------------------------
# Type validation (identical to convenience.py)
# ---------------------------------------------------------------------------

_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."""
    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

    if declared.startswith("list[") and declared.endswith("]"):
        elem_type = declared[5:-1]
        elem_types = _SCALAR_TYPES.get(elem_type)
        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 (drop-in replacement for convenience.py)
# ---------------------------------------------------------------------------

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 transforms.
    """
    key_str = f"{method_key[0]}.{method_key[1]}"
    entry = _METHODS.get(key_str)
    if entry is None:
        return

    # Enforce constraints (replaces/maps_to/type validation)
    for pname, pspec in entry.get("params", {}).items():
        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 = pspec.get("maps_to")
        if maps_to and pname in kwargs:
            root = maps_to.split(".")[0]
            if root in kwargs:
                if "." in maps_to:
                    # Nested path (e.g. post_processor.config.llm_temp):
                    # explicit root object wins — drop the flat shortcut.
                    # Handles defaults (e.g. llm_temp=0.3) coexisting with
                    # an explicit post_processor.
                    del kwargs[pname]
                else:
                    # Direct mapping (e.g. space_ids → space_keys):
                    # genuine conflict, user must pick one.
                    raise ValueError(
                        f"Cannot specify both '{pname}' and '{root}'. "
                        f"Use either '{pname}' (SDK) or '{root}' (REST), not both."
                    )
        declared_type = pspec.get("type")
        if declared_type and pname in kwargs:
            _validate_type(pname, kwargs[pname], declared_type)

    # Execute transform pipeline
    for transform in entry.get("transforms", []):
        kind = transform["kind"]
        handler = _TRANSFORM_DISPATCH.get(kind)
        if handler is not None:
            handler(kwargs, transform)


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 Page._max_items)
    - next_token stays in kwargs (flows through to tier-2 for eager first fetch)

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

    max_items = kwargs.pop("max_items", None)

    return max_items
