"""cortex/api/routes/memory/remember.py — Canonical remember (v2) endpoints."""

from __future__ import annotations

import copy
import logging
from typing import Any, Optional

from fastapi import Depends, HTTPException, Query, Request, Response, status
from fastapi.responses import JSONResponse

from cortex.api.deps import PREFIX, _UNAUTHENTICATED, _resolve_owner_id, get_plugin
from cortex.api.deprecation import add_deprecation_headers
from cortex.api.models import (
    EnhancedRememberRequest,
    EnhancedRememberResponse,
)

from cortex.api.routes.memory.helpers import (
    _MAX_REMEMBER_BYTES,
    _normalize_and_validate_remember_conversation,
    _shadow_remember,
)

logger = logging.getLogger(__name__)



def _sanitize_auth_owner(request) -> Optional[str]:
    """Return the authenticated owner_id, treating _UNAUTHENTICATED sentinel as None.

    The _UNAUTHENTICATED sentinel is a truthy object set by auth middleware
    when no real user is identified.  It must be treated as None so that
    owner_id validation guards fire correctly (#1544).
    """
    _auth_owner = getattr(request.state, "owner_id", None)
    if _auth_owner is _UNAUTHENTICATED:
        return None
    return _auth_owner


def _register(router, verify_api_key):
    """Register canonical remember (v2) endpoints."""

    # ---- Canonical: /memories/remember  (issue #297) ----
    @router.post(
        f"{PREFIX}/memories/remember",
        response_model=EnhancedRememberResponse,
        status_code=status.HTTP_200_OK,
        summary="Enhanced memory write with detailed response",
        tags=["memory"],
        operation_id="memories_remember",
    )
    async def memories_remember(
        request: Request,
        body: EnhancedRememberRequest,
        shadow: bool = Query(False, description="Shadow mode — extract but do not persist"),
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Extract and store memories with enhanced response details.

        Canonical path: ``POST /api/v1/memories/remember``.
        Pass ``?shadow=true`` to run extraction without persisting (dry run).
        """
        # Validation: owner_id is required — body must supply it or auth middleware
        # must set request.state.owner_id. Falling through to the hardcoded "default"
        # string is a silent data-loss bug (claims stored under wrong namespace).
        _auth_owner = _sanitize_auth_owner(request)
        _has_auth_middleware = hasattr(request.state, "owner_id")
        if not body.owner_id and _has_auth_middleware and not _auth_owner:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="owner_id is required. Provide it in the request body or authenticate with a bearer token that includes an owner scope.",
            )

        # Validation: conversation must be a list of message dicts, not a raw string.
        # Strings were accepted by the deprecated /remember endpoint for backward compat,
        # but the canonical /memories/remember endpoint enforces structured input.
        if isinstance(body.conversation, str):
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="conversation must be a list of {role, content} message dicts, not a string. Use the deprecated POST /api/v1/remember endpoint if you need raw-text input.",
            )

        body.owner_id = _resolve_owner_id(request, body.owner_id)
        return await _do_remember_v2(body, shadow, plugin)

    # ---- DEPRECATED: /remember/v2  (issue #297) ----
    @router.post(
        f"{PREFIX}/remember/v2",
        response_model=EnhancedRememberResponse,
        status_code=status.HTTP_200_OK,
        summary="Enhanced memory write — DEPRECATED, use /memories/remember",
        tags=["memory (deprecated)"],
        operation_id="remember_v2",
        deprecated=True,
    )
    async def remember_v2(
        request: Request,
        body: EnhancedRememberRequest,
        response: Response,
        shadow: bool = Query(False, description="Shadow mode — extract but do not persist"),
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """DEPRECATED — use ``POST /api/v1/memories/remember``.

        .. deprecated:: sunset 2026-06-01  (issue #297)

        Security (#818): owner_id resolved from auth middleware, not body.
        """
        add_deprecation_headers(response, f"{PREFIX}/memories/remember")
        # Sentinel guard: when auth IS configured but resolved to _UNAUTHENTICATED,
        # and no body owner_id is provided, reject the request. In open-access mode
        # (no auth middleware at all), _resolve_owner_id falls back to "default".
        _auth_owner = _sanitize_auth_owner(request)
        _has_auth_middleware = hasattr(request.state, "owner_id")
        if not body.owner_id and _has_auth_middleware and not _auth_owner:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="owner_id is required. Provide it in the request body or authenticate with a bearer token that includes an owner scope.",
            )
        body.owner_id = _resolve_owner_id(request, body.owner_id)
        return await _do_remember_v2(body, shadow, plugin)

    async def _do_remember_v2(
        body: EnhancedRememberRequest, shadow: bool, plugin: Any
    ) -> JSONResponse:
        """Shared implementation for remember v2 / memories/remember endpoints."""
        import json as _json_v2
        import time as _time

        # Issue #486 — body size guard
        try:
            _cfg_limit_v2 = getattr(getattr(plugin, "config", None), "max_remember_body_bytes", None)
            _body_limit_v2 = int(_cfg_limit_v2) if isinstance(_cfg_limit_v2, int) else _MAX_REMEMBER_BYTES
        except (TypeError, ValueError):
            _body_limit_v2 = _MAX_REMEMBER_BYTES
        if _body_limit_v2 > 0:
            _body_bytes_v2 = len(_json_v2.dumps(
                body.conversation if not hasattr(body.conversation, "__iter__")
                or isinstance(body.conversation, str)
                else [m.model_dump() if hasattr(m, "model_dump") else (m.dict() if hasattr(m, "dict") else m) for m in body.conversation]
            ).encode("utf-8"))
            if _body_bytes_v2 > _body_limit_v2:
                raise HTTPException(
                    status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                    detail=f"Request body too large ({_body_bytes_v2} bytes > {_body_limit_v2} limit)",
                )

        conversation = _normalize_and_validate_remember_conversation(body.conversation)

        # Thread-safe per-request category exclusion: shallow-copy the extractor
        # instead of mutating shared state (#1271).
        _excl = getattr(body, "exclude_categories", None)
        _request_plugin = plugin
        if _excl and hasattr(plugin, "extractor") and plugin.extractor is not None:
            _request_plugin = copy.copy(plugin)
            _request_plugin.extractor = copy.copy(plugin.extractor)
            _request_plugin.extractor.exclude_categories = {c.lower().strip() for c in _excl}

        if shadow:
            shadow_result = await _shadow_remember(
                _request_plugin, conversation, body.session_id, body.owner_id,
            )
            return JSONResponse(content={
                "session_id": body.session_id or "shadow",
                "shadow": True,
                "claims_extracted": shadow_result["claims_extracted"],
                "claims_added": 0,
                "claims_updated": 0,
                "claims_stored": 0,
                "would_store": shadow_result["would_store"],
                "claims_detail": shadow_result.get("claims_detail", []),
                "entities_resolved": shadow_result["entities_resolved"],
                "noise_filtered": shadow_result["noise_filtered"],
                "events_logged": 0,
                "processing_time_ms": shadow_result["processing_time_ms"],
                "errors": shadow_result["errors"],
                "warnings": [],
                "zero_claim_warning": False,
                "ok": True,
            })

        t0 = _time.monotonic()

        try:
            report = await _request_plugin.remember(
                conversation=conversation,
                session_id=body.session_id,
                owner_id=body.owner_id,
                source_session_id=getattr(body, "source_session_id", None),
                source_message_id=getattr(body, "source_message_id", None),
                source_channel=getattr(body, "source_channel", None),
                source_type=getattr(body, "source_type", None),
                entity_id=getattr(body, "entity_id", None),
                entity_type=getattr(body, "entity_type", None),
                source_agent=getattr(body, "source_agent", None),
            )
        except (ValueError, TypeError) as exc:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail=f"Invalid conversation payload: {exc}",
            ) from exc

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

        claims_added = getattr(report, "claims_added", 0) or report.claims_stored
        claims_updated = getattr(report, "claims_updated", 0) or 0
        events_logged = getattr(report, "events_logged", 0) or 0

        return JSONResponse(content={
            "session_id": report.session_id,
            "claims_extracted": report.claims_extracted,
            "claims_added": claims_added,
            "claims_updated": claims_updated,
            "claims_stored": report.claims_stored,
            "entities_resolved": report.entities_resolved,
            "noise_filtered": report.noise_filtered,
            "events_logged": events_logged,
            "processing_time_ms": duration_ms,
            "errors": report.errors,
            "warnings": getattr(report, "warnings", []),
            "zero_claim_warning": getattr(report, "zero_claim_warning", False),
            "ok": report.ok,
        })
