"""
# flake8: noqa
requests.py
------------
Pydantic request models for the HIEROMEM API.

These schemas validate all incoming requests for:
  • Episodic memory operations (remember, recall, forget)
  • Governance parameter updates (λ, τ, θ, Nmax, b)
  • Narration and event introspection

Author: Sai Rishi Kiran Mannava
"""

from __future__ import annotations
from typing import Any, Optional, Tuple, Literal, List

from pydantic import BaseModel, Field, root_validator


# --------------------------------------------------------------------------- #
# Core Cognitive Operations
# --------------------------------------------------------------------------- #

class RememberRequest(BaseModel):
    """
    Represents an episodic insertion request — the 'remember' operation.
    Supports multi-modal payloads and optional tenant namespaces.
    """

    text: Optional[str] = Field(
        default=None,
        description="Natural language experience to encode.",
        max_length=10000,  # Prevent extremely long text
    )
    images: Optional[list[str]] = Field(
        default=None,
        description="Optional list of image references (URLs or identifiers).",
        max_items=10,  # Limit number of images
    )
    audio: Optional[list[str]] = Field(
        default=None,
        description="Optional list of audio references (URLs or identifiers).",
        max_items=10,  # Limit number of audio files
    )
    embedding: Optional[list[float]] = Field(
        default=None,
        description="Pre-computed embedding vector to bypass encoding.",
        max_items=2048,  # Reasonable embedding dimension limit
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace for isolation.",
        min_length=1,
        max_length=100,  # Prevent extremely long namespace names
    )
    meta: Optional[dict[str, Any]] = Field(
        default=None,
        description="Optional metadata (tags, privacy level, context markers).",
    )

    @root_validator(skip_on_failure=True)
    def _ensure_payload_present(cls, values: dict[str, Any]) -> dict[str, Any]:
        if not any(
            values.get(field) for field in (
                "text",
                "embedding",
                "images",
                "audio")):
            raise ValueError(
                "At least one of text, embedding, images, or audio must be provided.")
        return values
    
    @root_validator(skip_on_failure=True)
    def _validate_metadata_size(cls, values: dict[str, Any]) -> dict[str, Any]:
        """Prevent excessively large metadata objects."""
        meta = values.get("meta")
        if meta:
            import json
            meta_str = json.dumps(meta)
            if len(meta_str) > 50000:  # 50KB limit
                raise ValueError("Metadata size exceeds 50KB limit")
        return values
    
    class Config:
        schema_extra = {
            "examples": [
                {
                    "text": "User completed the onboarding tutorial successfully",
                    "namespace": "production",
                    "meta": {
                        "user_id": "user_123",
                        "event_type": "onboarding",
                        "duration_seconds": 45
                    }
                },
                {
                    "text": "Customer reported a bug in the payment flow",
                    "namespace": "support",
                    "meta": {
                        "ticket_id": "TICKET-456",
                        "priority": "high",
                        "category": "bug"
                    }
                }
            ]
        }


class RecallRequest(BaseModel):
    """
    Represents a query for content-addressable retrieval.
    """
    query: str = Field(
        ...,
        description="User query or semantic cue for recall.",
        min_length=1,
        max_length=5000,  # Prevent extremely long queries
    )
    k: int = Field(
        3,
        description="Number of relevant episodes to return.",
        ge=1,
        le=100,  # Reasonable upper limit
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace to query within.",
        min_length=1,
        max_length=100,
    )
    
    class Config:
        schema_extra = {
            "examples": [
                {
                    "query": "What did the user do during onboarding?",
                    "k": 5,
                    "namespace": "production"
                },
                {
                    "query": "Find all payment-related issues",
                    "k": 10,
                    "namespace": "support"
                }
            ]
        }


class QueryGraphEdge(BaseModel):
    """Lightweight edge specification used to build a query scene graph."""

    source: str = Field(..., description="Source node identifier.")
    target: str = Field(..., description="Target node identifier.")
    relation: Optional[str] = Field(
        default=None,
        description="Optional relation label applied to the directed edge.",
    )


class DualRecallRequest(BaseModel):
    """Request body for dual (dense + symbolic) retrieval."""

    query: Optional[str] = Field(
        default=None,
        description="Natural language cue to encode with the default encoder.",
    )
    embedding: Optional[list[float]] = Field(
        default=None,
        description="Optional pre-computed embedding to bypass encoding.",
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace to query within.",
        min_length=1,
    )
    k: int = Field(3, ge=1, le=50, description="Number of episodes to return.")
    alpha_dense: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Optional manual override for dense similarity weighting (α).",
    )
    beta_symbolic: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Optional manual override for symbolic weighting (β).",
    )
    lambda_decay: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Optional manual override for trust decay λ during this call.",
    )
    temperature: float = Field(
        1.0,
        gt=0.0,
        description="Softmax temperature used for both dense and symbolic weights.",
    )
    max_iterations: int = Field(
        6,
        ge=1,
        le=50,
        description="Maximum refinement iterations for the dual retriever.",
    )
    tolerance: float = Field(
        1e-4,
        gt=0.0,
        description="Convergence tolerance for the combined weight updates.",
    )
    query_graph_edges: Optional[list[QueryGraphEdge]] = Field(
        default=None,
        description="Optional list of edges used to construct a symbolic query graph.",
    )

    @root_validator(skip_on_failure=True)
    def _require_query_vector(cls, values: dict[str, Any]) -> dict[str, Any]:
        if not values.get("query") and values.get("embedding") is None:
            raise ValueError(
                "Provide either a query string or an embedding for dual recall.")
        return values


# --------------------------------------------------------------------------- #
# Policy operations
# --------------------------------------------------------------------------- #


class PolicyUpsertRequest(BaseModel):
    """Request payload for creating or updating a memory policy."""

    policy: dict[str, Any] = Field(
        ...,
        description="Policy document matching the MemoryPolicy schema.",
    )
    validate_only: bool = Field(
        default=False,
        description="If true, validate only without persisting.",
    )


class PolicySimulationRequest(BaseModel):
    """Request payload for policy simulation."""

    policy: dict[str, Any] = Field(
        ...,
        description="Policy document matching the MemoryPolicy schema.",
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Namespace used for simulation (defaults to policy namespace).",
    )
    total_episodes: int = Field(
        0,
        ge=0,
        description="Total episode count before the simulated insert.",
    )
    episode_count: Optional[int] = Field(
        default=None,
        ge=0,
        description="Active episode count for summarization checks.",
    )
    oldest_episode_age_s: Optional[int] = Field(
        default=None,
        ge=0,
        description="Age (in seconds) of the oldest episode for summarization checks.",
    )
    graph_edges: Optional[list[QueryGraphEdge]] = Field(
        default=None,
        description="Optional graph edges for insert policy evaluation.",
    )


class BatchDualRecallQuery(BaseModel):
    """Single query specification for batch dual recall."""

    query_id: Optional[str] = Field(
        default=None,
        description="Optional client-supplied identifier echoed in the response.",
    )
    query: Optional[str] = Field(
        default=None,
        description="Natural language cue to encode with the default encoder.",
    )
    embedding: Optional[list[float]] = Field(
        default=None,
        description="Optional pre-computed embedding to bypass encoding.",
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace to query within.",
        min_length=1,
    )
    k: Optional[int] = Field(
        default=None,
        ge=1,
        le=50,
        description="Override for the number of episodes to return for this query.",
    )
    alpha_dense: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Override for the dense similarity weight.",
    )
    beta_symbolic: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Override for the symbolic alignment weight.",
    )
    lambda_decay: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Override for trust decay λ during this batch item.",
    )
    temperature: Optional[float] = Field(
        default=None,
        gt=0.0,
        description="Override for the softmax temperature for this query.",
    )
    max_iterations: Optional[int] = Field(
        default=None,
        ge=1,
        le=50,
        description="Override for the maximum refinement iterations.",
    )
    tolerance: Optional[float] = Field(
        default=None,
        gt=0.0,
        description="Override for the convergence tolerance.",
    )
    query_graph_edges: Optional[list[QueryGraphEdge]] = Field(
        default=None,
        description="Optional list of edges used to construct a symbolic query graph.",
    )

    @root_validator(skip_on_failure=True)
    def _require_payload(cls, values: dict[str, Any]) -> dict[str, Any]:
        if not values.get("query") and values.get("embedding") is None:
            raise ValueError(
                "Provide either a query string or an embedding for dual recall.")
        return values


class BatchDualRecallRequest(BaseModel):
    """Request payload for executing multiple dual recall queries at once."""

    namespace: Optional[str] = Field(
        default=None,
        description="Default namespace applied when individual queries omit one.",
        min_length=1,
    )
    k: int = Field(
        3,
        ge=1,
        le=50,
        description="Default number of episodes to return per query.")
    alpha_dense: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Default dense similarity weight when queries do not override it.",
    )
    beta_symbolic: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Default symbolic alignment weight when queries do not override it.",
    )
    lambda_decay: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Default trust decay override when individual queries omit λ.",
    )
    temperature: float = Field(
        1.0,
        gt=0.0,
        description="Default softmax temperature used for weighting.",
    )
    max_iterations: int = Field(
        6,
        ge=1,
        le=50,
        description="Default maximum refinement iterations per query.",
    )
    tolerance: float = Field(
        1e-4,
        gt=0.0,
        description="Default convergence tolerance for combined weights.",
    )
    queries: list[BatchDualRecallQuery] = Field(
        ...,
        min_length=1,
        description="List of dual recall queries to execute sequentially.",
    )


class ForgetRequest(BaseModel):
    """
    Represents an explicit or policy-based forgetting command.
    """
    episode_ids: Optional[list[str]] = Field(
        default=None,
        description="Explicit list of episode IDs to forget. Optional.",
    )
    threshold: Optional[float] = Field(
        default=None,
        description="Optional trust threshold for pruning (if IDs not given).",
        ge=0.0,
        le=1.0,
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace scope.",
        min_length=1,
    )


# --------------------------------------------------------------------------- #
# Governance and Administration
# --------------------------------------------------------------------------- #

class ParamUpdateRequest(BaseModel):
    """
    Request model for dynamic tuning of Governance Safety Region parameters.
    """
    lam: Optional[float] = Field(
        None, ge=0.0, le=1.0, description="Trust decay factor λ.")
    tau: Optional[float] = Field(
        None,
        gt=0.0,
        le=1.0,
        description="Retrieval temperature τ.")
    theta: Optional[float] = Field(
        None, ge=0.0, le=1.0, description="Forgetting threshold θ.")
    Nmax: Optional[int] = Field(
        None, gt=0, description="Maximum active memory capacity.")
    b: Optional[int] = Field(
        None, gt=0, description="Summarization batch size.")


class DegradationToggleRequest(BaseModel):
    """Request model to force health/readiness probes into a degraded state."""

    reason: str = Field(
        ..., min_length=3, max_length=200, description="Why the service is degraded."
    )
    ttl_seconds: Optional[int] = Field(
        default=None,
        ge=30,
        le=3600,
        description=(
            "Optional duration for the override; defaults to indefinite until"
            " restored."
        ),
    )
    include_health: bool = Field(
        default=True, description="Apply the override to /system/health responses."
    )
    include_readiness: bool = Field(
        default=True, description="Apply the override to /system/ready responses."
    )


# --------------------------------------------------------------------------- #
# Event Narration and Introspection
# --------------------------------------------------------------------------- #

class NarrateRequest(BaseModel):
    """
    Request model for event narration queries.
    """
    limit: int = Field(
        20, description="Number of events to include in the narration.")
    mode: Optional[str] = Field(
        "recent",
        pattern="^(recent|summary)$",
        description="Narration mode: 'recent' for chronological view, 'summary' for aggregated insight.",
    )


class SymbolicSearchRequest(BaseModel):
    """Request body for symbolic graph queries."""

    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace to constrain the search.",
        min_length=1,
    )
    path: Optional[list[str]] = Field(
        default=None,
        description="Ordered list of node IDs representing a required path.",
        min_length=2,
    )
    pattern_edges: Optional[list[Tuple[str, str]]] = Field(
        default=None,
        description="Directed edges that define a subgraph pattern to match.",
    )
    limit: int = Field(
        5,
        ge=1,
        le=50,
        description="Maximum number of matches per query.")

    @root_validator(skip_on_failure=True)
    def _validate_symbolic_payload(
            cls, values: dict[str, Any]) -> dict[str, Any]:
        if not values.get("path") and not values.get("pattern_edges"):
            values["path"] = ["experience", "experience"]
        return values


class BatchSymbolicSearchQuery(BaseModel):
    """Single symbolic search specification used within a batch request."""

    query_id: Optional[str] = Field(
        default=None,
        description="Optional client identifier that will be echoed in responses.",
    )
    namespace: Optional[str] = Field(
        default=None,
        description="Tenant or workspace namespace to constrain the search.",
        min_length=1,
    )
    path: Optional[list[str]] = Field(
        default=None,
        description="Ordered list of node IDs representing a required path.",
        min_length=2,
    )
    pattern_edges: Optional[list[Tuple[str, str]]] = Field(
        default=None,
        description="Directed edges that define a subgraph pattern to match.",
    )
    limit: Optional[int] = Field(
        default=None,
        ge=1,
        le=50,
        description="Override for the maximum number of matches per query.",
    )

    @root_validator(skip_on_failure=True)
    def _validate_symbolic_batch_payload(
            cls, values: dict[str, Any]) -> dict[str, Any]:
        if not values.get("path") and not values.get("pattern_edges"):
            values["path"] = ["experience", "experience"]
        return values


class BatchSymbolicSearchRequest(BaseModel):
    """Request payload for executing multiple symbolic searches in a single call."""

    namespace: Optional[str] = Field(
        default=None,
        description="Default namespace applied when a query omits one.",
        min_length=1,
    )
    limit: int = Field(
        5,
        ge=1,
        le=50,
        description="Default maximum number of matches per query when unspecified.",
    )
    queries: list[BatchSymbolicSearchQuery] = Field(
        ...,
        min_length=1,
        description="List of symbolic search specifications to evaluate.",
    )

class RealmParticipantRequest(BaseModel):
    agent_id: str
    permissions: List[str] = ["read"]

class CreateRealmRequest(BaseModel):
    name: str = Field(..., description="Unique name of the realm")
    type: Literal["shared", "private", "hybrid"] = "private"
    participants: Optional[List[RealmParticipantRequest]] = None
    isolation_level: Literal["logical", "cryptographic"] = "logical"

class JoinRealmRequest(BaseModel):
    agent_id: str
    permissions: List[str] = ["read"]
