# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Adaptive plugin configuration helpers.

Adaptive is configured as a single flat top-level plugin component. Hosted
plugins remain separate top-level components managed through ``nemo_relay.plugin``.
"""

from __future__ import annotations

from dataclasses import dataclass, field, fields, is_dataclass
from typing import Literal, Protocol, TypedDict, cast

from nemo_relay import Json, JsonObject, UnsupportedBehavior
from nemo_relay._native import AdaptiveRuntime as AdaptiveRuntime
from nemo_relay._native import build_cache_telemetry_event as _build_cache_telemetry_event
from nemo_relay._native import set_latency_sensitivity as _set_latency_sensitivity
from nemo_relay._native import validate_adaptive_config as _validate_adaptive_config


class _ConfigDiagnosticRequired(TypedDict):
    level: Literal["warning", "error"]
    code: str
    message: str


class ConfigDiagnostic(_ConfigDiagnosticRequired, total=False):
    """One adaptive validation diagnostic."""

    component: str
    field: str


class ConfigReport(TypedDict):
    """Validation report for adaptive configuration."""

    diagnostics: list[ConfigDiagnostic]


class _SupportsToDict(Protocol):
    def to_dict(self) -> JsonObject: ...


def _normalize(value: object) -> Json:
    if hasattr(value, "to_dict"):
        return cast(_SupportsToDict, value).to_dict()
    if is_dataclass(value) and not isinstance(value, type):
        return {
            field_info.name: _normalize(field_value)
            for field_info in fields(value)
            if (field_value := getattr(value, field_info.name)) is not None
        }
    if isinstance(value, list):
        return [_normalize(item) for item in value]
    if isinstance(value, dict):
        return {cast(str, key): _normalize(val) for key, val in value.items() if val is not None}
    return cast(Json, value)


def _normalize_object(value: object) -> JsonObject:
    return cast(JsonObject, _normalize(value))


@dataclass(slots=True)
class ConfigPolicy:
    """Policy for unsupported adaptive configuration.

    Args:
        unknown_component: How to handle unknown component kinds.
        unknown_field: How to handle unknown adaptive config fields.
        unsupported_value: How to handle known fields with unsupported values.
    """

    unknown_component: UnsupportedBehavior = "warn"
    unknown_field: UnsupportedBehavior = "warn"
    unsupported_value: UnsupportedBehavior = "error"

    def to_dict(self) -> JsonObject:
        """Serialize this policy to the canonical JSON object shape."""
        return {
            "unknown_component": self.unknown_component,
            "unknown_field": self.unknown_field,
            "unsupported_value": self.unsupported_value,
        }


@dataclass(slots=True)
class BackendSpec:
    """Adaptive state backend selection.

    Args:
        kind: Backend kind string such as ``"in_memory"`` or ``"redis"``.
        config: Backend-specific JSON object.
    """

    kind: str
    config: JsonObject = field(default_factory=dict)

    @staticmethod
    def in_memory() -> "BackendSpec":
        """Return an in-memory adaptive backend spec."""
        return BackendSpec(kind="in_memory")

    @staticmethod
    def redis(url: str, key_prefix: str = "nemo_relay:") -> "BackendSpec":
        """Return a Redis adaptive backend spec."""
        return BackendSpec(kind="redis", config={"url": url, "key_prefix": key_prefix})

    def to_dict(self) -> JsonObject:
        """Serialize this backend spec to the canonical JSON object shape."""
        return {"kind": self.kind, "config": _normalize_object(self.config)}


@dataclass(slots=True)
class StateConfig:
    """Adaptive state configuration.

    Args:
        backend: State backend selection for adaptive features that persist or
            learn over time.
    """

    backend: BackendSpec

    def to_dict(self) -> JsonObject:
        """Serialize this state config to the canonical JSON object shape."""
        return {"backend": _normalize_object(self.backend)}


@dataclass(slots=True)
class TelemetryConfig:
    """Built-in adaptive telemetry subscriber settings.

    Args:
        subscriber_name: Optional subscriber registration name override.
        learners: Enabled learner identifiers.
    """

    subscriber_name: str | None = None
    learners: list[str] = field(default_factory=list)

    def to_dict(self) -> JsonObject:
        """Serialize this telemetry config to the canonical JSON object shape."""
        return _normalize_object(
            {
                "subscriber_name": self.subscriber_name,
                "learners": self.learners,
            }
        )


@dataclass(slots=True)
class AdaptiveHintsConfig:
    """Built-in adaptive hints injection settings.

    Args:
        priority: Intercept priority. Lower values run first.
        break_chain: Whether to stop later request intercepts after this one.
        inject_header: Whether to inject the adaptive hints HTTP header.
        inject_body_path: JSON body path used when injecting request-body hints.
    """

    priority: int = 100
    break_chain: bool = False
    inject_header: bool = True
    inject_body_path: str = "nvext.agent_hints"

    def to_dict(self) -> JsonObject:
        """Serialize this adaptive-hints config to the canonical JSON object shape."""
        return _normalize_object(
            {
                "priority": self.priority,
                "break_chain": self.break_chain,
                "inject_header": self.inject_header,
                "inject_body_path": self.inject_body_path,
            }
        )


@dataclass(slots=True)
class ToolParallelismConfig:
    """Built-in adaptive tool scheduling settings.

    Args:
        priority: Intercept priority. Lower values run first.
        mode: Scheduling mode. ``"observe_only"`` records signals without
            changing behavior, while other modes enable stronger adaptive
            scheduling behavior.
    """

    priority: int = 100
    mode: Literal["observe_only", "inject_hints", "schedule"] = "observe_only"

    def to_dict(self) -> JsonObject:
        """Serialize this tool-parallelism config to the canonical JSON object shape."""
        return _normalize_object({"priority": self.priority, "mode": self.mode})


@dataclass(slots=True)
class AcgStabilityThresholds:
    """Prompt-stability classification thresholds for ACG.

    Args:
        stable_threshold: Minimum effective score classified as stable.
        semi_stable_threshold: Minimum effective score classified as semi-stable.
        min_observations_for_full_confidence: Observation count required to
            reach full confidence.
    """

    stable_threshold: float = 0.95
    semi_stable_threshold: float = 0.50
    min_observations_for_full_confidence: int = 20

    def to_dict(self) -> JsonObject:
        """Serialize these ACG stability thresholds to the canonical JSON object shape."""
        return _normalize_object(
            {
                "stable_threshold": self.stable_threshold,
                "semi_stable_threshold": self.semi_stable_threshold,
                "min_observations_for_full_confidence": self.min_observations_for_full_confidence,
            }
        )


@dataclass(slots=True)
class AcgConfig:
    """Adaptive Cache Governor settings.

    Args:
        provider: Provider cache plugin name.
        observation_window: Rolling PromptIR observation window size.
        priority: LLM execution intercept priority.
        stability_thresholds: Prompt-stability classification thresholds.
    """

    provider: Literal["anthropic", "openai", "passthrough"] = "passthrough"
    observation_window: int = 100
    priority: int = 50
    stability_thresholds: AcgStabilityThresholds | None = field(default_factory=AcgStabilityThresholds)

    def to_dict(self) -> JsonObject:
        """Serialize this ACG config to the canonical JSON object shape."""
        return _normalize_object(
            {
                "provider": self.provider,
                "observation_window": self.observation_window,
                "priority": self.priority,
                "stability_thresholds": _normalize(self.stability_thresholds),
            }
        )


@dataclass(slots=True)
class AdaptiveConfig:
    """Canonical config document for the top-level adaptive component.

    Args:
        version: Adaptive config schema version.
        agent_id: Optional explicit agent identifier for learned state.
        state: Adaptive state backend configuration.
        telemetry: Built-in adaptive telemetry settings.
        adaptive_hints: Built-in LLM hint-injection settings.
        tool_parallelism: Built-in tool scheduling settings.
        acg: Adaptive Cache Governor settings.
        policy: Unsupported-config policy applied within the adaptive config.

    Behavior:
        This document configures only the adaptive component. Plugins are
        configured separately through top-level plugin components.
    """

    version: int = 1
    agent_id: str | None = None
    state: StateConfig | None = None
    telemetry: TelemetryConfig | None = None
    adaptive_hints: AdaptiveHintsConfig | None = None
    tool_parallelism: ToolParallelismConfig | None = None
    acg: AcgConfig | None = None
    policy: ConfigPolicy = field(default_factory=ConfigPolicy)

    def to_dict(self) -> JsonObject:
        """Serialize this adaptive config to the canonical JSON object shape."""
        return {
            "version": self.version,
            "agent_id": self.agent_id,
            "state": _normalize(self.state),
            "telemetry": _normalize(self.telemetry),
            "adaptive_hints": _normalize(self.adaptive_hints),
            "tool_parallelism": _normalize(self.tool_parallelism),
            "acg": _normalize(self.acg),
            "policy": self.policy.to_dict(),
        }


ADAPTIVE_PLUGIN_KIND = "adaptive"


@dataclass(slots=True)
class ComponentSpec:
    """Top-level adaptive component wrapper.

    Args:
        config: ``AdaptiveConfig`` or an equivalent JSON object.
        enabled: Whether the adaptive component should be activated.

    Behavior:
        The component kind is always ``"adaptive"``.
    """

    config: AdaptiveConfig | JsonObject
    enabled: bool = True

    def to_dict(self) -> JsonObject:
        """Serialize this component to the canonical plugin shape."""
        return {
            "kind": ADAPTIVE_PLUGIN_KIND,
            "enabled": self.enabled,
            "config": _normalize_object(self.config),
        }


def validate_config(config: AdaptiveConfig | JsonObject) -> ConfigReport:
    """Validate an adaptive config document without constructing a runtime."""
    return cast(ConfigReport, _validate_adaptive_config(_normalize_object(config)))


def build_cache_telemetry_event(
    *,
    provider: str,
    request_id: str,
    usage: JsonObject | None = None,
    request_facts: JsonObject | None = None,
    agent_id: str,
    template_version: str,
    toolset_hash: str,
    model_family: str,
    tenant_scope: str,
    timestamp: str | None = None,
) -> JsonObject | None:
    """Build one canonical cache telemetry event from usage plus request facts."""
    return cast(
        JsonObject | None,
        _build_cache_telemetry_event(
            provider=provider,
            request_id=request_id,
            usage=usage,
            request_facts=request_facts,
            agent_id=agent_id,
            template_version=template_version,
            toolset_hash=toolset_hash,
            model_family=model_family,
            tenant_scope=tenant_scope,
            timestamp=timestamp,
        ),
    )


def set_latency_sensitivity(level: int) -> None:
    """Set a request-local latency-sensitivity hint.

    Args:
        level: Positive integer sensitivity value for the current execution
            context.

    Returns:
        `None`.

    Behavior:
        This is an execution-time hint for the current request/scope context,
        not persistent adaptive configuration. The native adaptive layer stores
        this as a positive integer.
    """
    _set_latency_sensitivity(level)


__all__ = [
    "AcgConfig",
    "AcgStabilityThresholds",
    "AdaptiveConfig",
    "AdaptiveHintsConfig",
    "ADAPTIVE_PLUGIN_KIND",
    "BackendSpec",
    "ConfigDiagnostic",
    "ConfigPolicy",
    "ConfigReport",
    "ComponentSpec",
    "StateConfig",
    "TelemetryConfig",
    "ToolParallelismConfig",
    "set_latency_sensitivity",
    "UnsupportedBehavior",
]
