"""cortex/api/routes/memory/helpers.py — Shared utilities for memory routes."""

from __future__ import annotations

import dataclasses
import logging
from typing import Any, Dict, List, Optional

from fastapi import HTTPException, status

logger = logging.getLogger(__name__)

_MAX_REMEMBER_TOTAL_CHARS = 50000
_MAX_REMEMBER_MESSAGE_CHARS = 10000
# Issue #486 / #821 — Hard cap on serialised JSON body size for /remember endpoints.
# Tightened from 1MB to 512KB (#821). Configurable via [cortex.api]
# max_remember_body_bytes in TOML; this constant is the fallback default.
_MAX_REMEMBER_BYTES = 524_288  # 512 KB

# Map API-advertised aliases to engine-supported retrieval modes
_MODE_ALIASES: dict[str, str] = {"fast": "lightweight", "thorough": "agentic", "multi-hop": "chained"}


def _normalize_and_validate_remember_conversation(conversation_input: Any) -> Any:
    """Normalize remember payload + enforce API-level length limits."""
    if isinstance(conversation_input, list):
        if len(conversation_input) == 0:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="Conversation must not be empty",
            )

        normalized = []
        total_len = 0
        for idx, msg in enumerate(conversation_input):
            content = msg.content
            if len(content) > _MAX_REMEMBER_MESSAGE_CHARS:
                raise HTTPException(
                    status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                    detail=(
                        f"conversation[{idx}].content too long "
                        f"(max {_MAX_REMEMBER_MESSAGE_CHARS} characters)"
                    ),
                )
            total_len += len(content)
            normalized.append({"role": msg.role, "content": content})

        if total_len > _MAX_REMEMBER_TOTAL_CHARS:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail=(
                    "Conversation too long "
                    f"(max {_MAX_REMEMBER_TOTAL_CHARS} characters total)"
                ),
            )
        return normalized

    if conversation_input == "":
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="Conversation must not be empty",
        )

    if len(conversation_input) > _MAX_REMEMBER_MESSAGE_CHARS:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail=(
                "Conversation text too long "
                f"(max {_MAX_REMEMBER_MESSAGE_CHARS} characters)"
            ),
        )

    return conversation_input


# ---------------------------------------------------------------------------
# Shadow mode helpers
# ---------------------------------------------------------------------------

async def _shadow_remember(
    plugin: Any,
    conversation: Any,
    session_id: Optional[str],
    owner_id: Optional[str],
) -> dict:
    """Run extraction pipeline in shadow mode: extract claims but do NOT persist.

    Conversation should already be validated/normalized by the caller.
    Returns a dict with extraction results suitable for the shadow response.
    """
    import time as _time

    t0 = _time.monotonic()

    extractor = getattr(plugin, "extractor", None)
    if extractor is None:
        return {
            "claims_extracted": 0,
            "would_store": [],
            "entities_resolved": 0,
            "noise_filtered": 0,
            "processing_time_ms": 0,
            "errors": ["Extraction pipeline not available"],
        }

    sid = session_id or "shadow"
    if isinstance(conversation, str):
        messages = [{"role": "user", "content": conversation}]
    else:
        messages = conversation

    result = await extractor.extract(messages=messages, session_id=sid)

    would_store: List[str] = []
    claims_detail: List[Dict[str, Any]] = []
    entities_resolved_set: set = set()
    for claim in result.claims:
        would_store.append(f"{claim.subject} {claim.predicate} {claim.object_value}")
        # Serialize ALL ClaimExtraction fields for shadow visibility (#1285)
        detail = dataclasses.asdict(claim)
        if "entities_mentioned" in detail:
            detail["entities_mentioned"] = [
                dataclasses.asdict(e) if dataclasses.is_dataclass(e) else e
                for e in detail.get("entities_mentioned", [])
            ]
        claims_detail.append(detail)
        for ent in getattr(claim, "entities_mentioned", []):
            entities_resolved_set.add(getattr(ent, "name", str(ent)))

    duration_ms = int((_time.monotonic() - t0) * 1000)

    _emit_shadow_event(
        endpoint="/api/v1/remember",
        session_id=sid,
        claims_extracted=len(result.claims),
        would_store=would_store,
        entities_resolved=len(entities_resolved_set),
        noise_filtered=result.noise_filtered,
    )

    return {
        "claims_extracted": len(result.claims),
        "would_store": would_store,
        "claims_detail": claims_detail,
        "entities_resolved": len(entities_resolved_set),
        "noise_filtered": result.noise_filtered,
        "processing_time_ms": duration_ms,
        "errors": [],
    }


def _emit_shadow_event(
    endpoint: str,
    session_id: str,
    claims_extracted: int = 0,
    would_store: Optional[List[str]] = None,
    entities_resolved: int = 0,
    noise_filtered: int = 0,
) -> None:
    """Emit a structured shadow-mode debug event.

    If cortex.debug exists (ring buffer from #452), use it.
    Otherwise fall back to structured Python logging.
    """
    import datetime

    event = {
        "event_type": "shadow",
        "endpoint": endpoint,
        "session_id": session_id,
        "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
        "claims_extracted": claims_extracted,
        "would_store": would_store or [],
        "entities_resolved": entities_resolved,
        "noise_filtered": noise_filtered,
    }

    try:
        from cortex.debug import emit_event
        emit_event(event)
    except (ImportError, AttributeError):
        logger.info("shadow_event: %s", event)
