"""cortex/api/routes/sessions.py — Session lifecycle endpoints.

Canonical paths (``/sessions/`` prefix — plural, REST-standard):
  POST /api/v1/sessions/wake
  POST /api/v1/sessions/sleep
  POST /api/v1/sessions/nap
  POST /api/v1/sessions/nap/wake
  POST /api/v1/sessions/dream
  GET  /api/v1/sessions
  GET  /api/v1/sessions/{session_id}

Legacy paths (deprecated, sunset 2026-06-01 — issue #301):
  POST /api/v1/session/wake     → /sessions/wake
  POST /api/v1/session/sleep    → /sessions/sleep
  POST /api/v1/session/nap      → /sessions/nap
  POST /api/v1/session/nap/wake → /sessions/nap/wake
  POST /api/v1/dream            → /sessions/dream
"""

from __future__ import annotations

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

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

from cortex.api.deps import PREFIX, _serialise, get_plugin


def _resolve_owner_id(request: Request, body_owner_id: str | None = None) -> str:
    """Return the effective owner_id: explicit override → auth middleware → config fallback."""
    if body_owner_id:
        return body_owner_id
    return getattr(request.state, "owner_id", None) or "default"
from cortex.api.deprecation import add_deprecation_headers
from cortex.api.models import (
    DreamRequest,
    DreamResponse,
    SessionNapRequest,
    SessionNapResponse,
    SessionNapWakeRequest,
    SessionNapWakeResponse,
    SessionSleepRequest,
    SessionSleepResponse,
    SessionWakeRequest,
    SessionWakeResponse,
)

logger = logging.getLogger(__name__)

router = APIRouter()

# Per-session locks to prevent concurrent sleep/consolidation (R-CRIT-1)
_sleep_locks: Dict[str, asyncio.Lock] = {}


def _get_sleep_lock(session_id: str) -> asyncio.Lock:
    """Return (creating if needed) the asyncio.Lock for the given session_id."""
    if session_id not in _sleep_locks:
        _sleep_locks[session_id] = asyncio.Lock()
    return _sleep_locks[session_id]


async def _do_wake(body: SessionWakeRequest, owner_id: Optional[str], plugin: Any) -> SessionWakeResponse:
    """Shared implementation for session wake endpoints."""
    report = await plugin.wake(session_id=body.session_id, owner_id=owner_id)
    return SessionWakeResponse(
        session_id=report.session_id,
        cornerstones_loaded=report.cornerstones_loaded,
        errors=report.errors,
    )


async def _do_sleep(body: SessionSleepRequest, plugin: Any) -> SessionSleepResponse:
    """Shared implementation for session sleep endpoints."""
    import time

    session_id = body.session_id or getattr(plugin, "session_id", "default")
    lock = _get_sleep_lock(str(session_id))

    async with lock:
        t0 = time.monotonic()
        report = await plugin.sleep(session_id=body.session_id, consolidate=body.consolidate)
        duration_ms = int((time.monotonic() - t0) * 1000)

    _sleep_locks.pop(str(session_id), None)

    claims_consolidated = getattr(report, "memories_summarised", 0) or getattr(
        report, "claims_consolidated", 0
    )
    return SessionSleepResponse(
        session_id=report.session_id,
        claims_consolidated=claims_consolidated,
        duration_ms=duration_ms,
        errors=report.errors,
    )


async def _do_nap(body: SessionNapRequest, plugin: Any) -> SessionNapResponse:
    """Shared implementation for session nap endpoints."""
    errors: List[str] = []
    storage = getattr(plugin, "storage", None)
    if storage is None:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Storage not available",
        )
    try:
        await storage.update_session(body.session_id, status="napping")
        logger.info("Session %s status -> napping", body.session_id)
    except Exception as exc:
        err = f"Failed to set session napping: {exc}"
        logger.error(err, exc_info=True)
        errors.append(err)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=err,
        )
    return SessionNapResponse(session_id=body.session_id, errors=errors)


async def _do_nap_wake(body: SessionNapWakeRequest, plugin: Any) -> SessionNapWakeResponse:
    """Shared implementation for session nap-wake endpoints."""
    errors: List[str] = []
    storage = getattr(plugin, "storage", None)
    if storage is None:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Storage not available",
        )
    try:
        await storage.update_session(body.session_id, status="active")
        logger.info("Session %s status -> active (resumed from nap)", body.session_id)
    except Exception as exc:
        err = f"Failed to resume session from nap: {exc}"
        logger.error(err, exc_info=True)
        errors.append(err)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=err,
        )
    return SessionNapWakeResponse(session_id=body.session_id, errors=errors)


async def _do_dream(body: DreamRequest, plugin: Any) -> DreamResponse:
    """Shared implementation for dream-cycle endpoint."""
    import time
    t0 = time.monotonic()
    errors: List[str] = []

    try:
        report = await plugin.dream(owner_id=body.owner_id)
        duration_ms = int((time.monotonic() - t0) * 1000)
        return DreamResponse(
            steps_completed=getattr(report, "steps_completed", 0),
            curiosity_seeds=getattr(report, "curiosity_seeds", 0),
            claims_consolidated=getattr(report, "claims_consolidated", 0),
            journal=getattr(report, "journal", ""),
            duration_ms=duration_ms,
            errors=getattr(report, "errors", []),
        )
    except Exception as exc:
        err = f"Dream cycle failed: {exc}"
        logger.error(err, exc_info=True)
        errors.append(err)
        duration_ms = int((time.monotonic() - t0) * 1000)
        return DreamResponse(duration_ms=duration_ms, errors=errors)


def _make_routes(verify_api_key):
    """Register session routes with the given auth dependency."""

    # =========================================================================
    # CANONICAL PATHS — /sessions/ prefix (plural, REST-standard)  Issue #301
    # =========================================================================

    @router.post(
        f"{PREFIX}/sessions/wake",
        response_model=SessionWakeResponse,
        status_code=status.HTTP_200_OK,
        summary="Start a new session (wake)",
        tags=["sessions"],
        operation_id="sessions_wake",
    )
    async def sessions_wake(
        request: Request,
        body: SessionWakeRequest,
        owner_id: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionWakeResponse:
        """Wake Cortex for a new session.

        Canonical path: ``POST /api/v1/sessions/wake``.
        """
        effective_owner = _resolve_owner_id(request, owner_id)
        return await _do_wake(body, effective_owner, plugin)

    @router.post(
        f"{PREFIX}/sessions/sleep",
        response_model=SessionSleepResponse,
        status_code=status.HTTP_200_OK,
        summary="End the current session (sleep)",
        tags=["sessions"],
        operation_id="sessions_sleep",
    )
    async def sessions_sleep(
        body: SessionSleepRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionSleepResponse:
        """Gracefully close a session.

        Uses a per-session lock to prevent concurrent calls from double-running
        consolidation (R-CRIT-1).

        Canonical path: ``POST /api/v1/sessions/sleep``.
        """
        return await _do_sleep(body, plugin)

    @router.post(
        f"{PREFIX}/sessions/nap",
        response_model=SessionNapResponse,
        status_code=status.HTTP_200_OK,
        summary="Pause a session (nap)",
        tags=["sessions"],
        operation_id="sessions_nap",
    )
    async def sessions_nap(
        body: SessionNapRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionNapResponse:
        """Put a session into napping state without closing it.

        Canonical path: ``POST /api/v1/sessions/nap``.
        """
        return await _do_nap(body, plugin)

    @router.post(
        f"{PREFIX}/sessions/nap/wake",
        response_model=SessionNapWakeResponse,
        status_code=status.HTTP_200_OK,
        summary="Resume a napping session",
        tags=["sessions"],
        operation_id="sessions_nap_wake",
    )
    async def sessions_nap_wake(
        body: SessionNapWakeRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionNapWakeResponse:
        """Resume a napping session by setting its status back to active.

        Canonical path: ``POST /api/v1/sessions/nap/wake``.
        """
        return await _do_nap_wake(body, plugin)

    @router.post(
        f"{PREFIX}/sessions/dream",
        response_model=DreamResponse,
        status_code=status.HTTP_200_OK,
        summary="Trigger a dream cycle",
        tags=["sessions"],
        operation_id="sessions_dream",
    )
    async def sessions_dream(
        body: DreamRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> DreamResponse:
        """Run a full dream-cycle consolidation (Electric Sheep).

        Canonical path: ``POST /api/v1/sessions/dream``.
        """
        return await _do_dream(body, plugin)

    # ---- Browse sessions (M13 Part 2) ----

    @router.get(
        f"{PREFIX}/sessions",
        summary="Browse sessions",
        tags=["sessions"],
        operation_id="list_sessions",
    )
    async def browse_sessions(
        page: int = 1,
        per_page: int = 50,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return sessions list with turn_count, claims_created, status."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            return JSONResponse(
                content={
                    "sessions": [],
                    "total": 0,
                    "page": 1,
                    "per_page": 50,
                    "pages": 0,
                    "errors": ["Storage not available"],
                }
            )
        errors: List[str] = []
        try:
            if hasattr(storage, "list_sessions"):
                effective_per_page = max(1, min(limit if limit is not None else per_page, 200))
                effective_page = max(1, page)
                effective_offset = offset if offset is not None else (effective_page - 1) * effective_per_page
                effective_offset = max(0, effective_offset)

                owner_id = getattr(plugin.config, "owner_id", "default")
                sessions = await storage.list_sessions(
                    owner_id=owner_id,
                    limit=effective_per_page,
                    offset=effective_offset,
                )
                total = 0
                if hasattr(storage, "count_sessions"):
                    total = await storage.count_sessions(owner_id=owner_id)
                pages = max(1, (total + effective_per_page - 1) // effective_per_page) if total > 0 else 0
                items = [
                    _serialise(s.__dict__) if hasattr(s, "__dict__") else _serialise(s)
                    for s in sessions
                ]
                return JSONResponse(
                    content={
                        "sessions": items,
                        "total": total,
                        "page": effective_page,
                        "per_page": effective_per_page,
                        "pages": pages,
                        "errors": errors,
                    }
                )
            else:
                errors.append("Session listing not supported")
                return JSONResponse(
                    content={"sessions": [], "total": 0, "page": 1, "per_page": 50, "pages": 0, "errors": errors}
                )
        except Exception as exc:
            err = f"Session list failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)
            return JSONResponse(
                content={"sessions": [], "total": 0, "page": 1, "per_page": 50, "pages": 0, "errors": errors}
            )

    @router.get(
        f"{PREFIX}/sessions/{{session_id}}",
        summary="Session detail",
        tags=["sessions"],
        operation_id="get_session",
    )
    async def get_session_detail(
        session_id: str,
        owner_id: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return full session record with summary and episode count."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        try:
            session = await storage.get_session_by_sid(
                session_id,
                owner_id=owner_id or getattr(plugin.config, "owner_id", "default"),
            )
            if session is None:
                raise HTTPException(404, detail=f"Session {session_id} not found")
            data = _serialise(session.__dict__) if hasattr(session, "__dict__") else _serialise(session)

            episode_count = 0
            if hasattr(storage, "get_session_episode_count"):
                episode_count = await storage.get_session_episode_count(
                    session_id,
                    owner_id=owner_id or getattr(plugin.config, "owner_id", "default"),
                )
            data["episode_count"] = episode_count

            return JSONResponse(content={"session": data})
        except HTTPException:
            raise
        except Exception as exc:
            logger.error("get_session_detail failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    # =========================================================================
    # DEPRECATED PATHS — /session/ (singular) and /dream  Issue #301
    # Sunset: 2026-06-01
    # All legacy paths delegate to the shared _do_* helpers above.
    # =========================================================================

    @router.post(
        f"{PREFIX}/session/wake",
        response_model=SessionWakeResponse,
        status_code=status.HTTP_200_OK,
        summary="Start a new session — DEPRECATED, use /sessions/wake",
        tags=["sessions (deprecated)"],
        operation_id="session_wake_deprecated",
        deprecated=True,
        include_in_schema=True,
    )
    async def session_wake_deprecated(
        request: Request,
        body: SessionWakeRequest,
        response: Response,
        owner_id: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionWakeResponse:
        """DEPRECATED — use ``POST /api/v1/sessions/wake``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/sessions/wake")
        effective_owner = _resolve_owner_id(request, owner_id)
        return await _do_wake(body, effective_owner, plugin)

    @router.post(
        f"{PREFIX}/session/sleep",
        response_model=SessionSleepResponse,
        status_code=status.HTTP_200_OK,
        summary="End the current session — DEPRECATED, use /sessions/sleep",
        tags=["sessions (deprecated)"],
        operation_id="session_sleep_deprecated",
        deprecated=True,
        include_in_schema=True,
    )
    async def session_sleep_deprecated(
        body: SessionSleepRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionSleepResponse:
        """DEPRECATED — use ``POST /api/v1/sessions/sleep``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/sessions/sleep")
        return await _do_sleep(body, plugin)

    @router.post(
        f"{PREFIX}/session/nap",
        response_model=SessionNapResponse,
        status_code=status.HTTP_200_OK,
        summary="Pause a session — DEPRECATED, use /sessions/nap",
        tags=["sessions (deprecated)"],
        operation_id="session_nap_deprecated",
        deprecated=True,
        include_in_schema=True,
    )
    async def session_nap_deprecated(
        body: SessionNapRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionNapResponse:
        """DEPRECATED — use ``POST /api/v1/sessions/nap``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/sessions/nap")
        return await _do_nap(body, plugin)

    @router.post(
        f"{PREFIX}/session/nap/wake",
        response_model=SessionNapWakeResponse,
        status_code=status.HTTP_200_OK,
        summary="Resume a napping session — DEPRECATED, use /sessions/nap/wake",
        tags=["sessions (deprecated)"],
        operation_id="session_nap_wake_deprecated",
        deprecated=True,
        include_in_schema=True,
    )
    async def session_nap_wake_deprecated(
        body: SessionNapWakeRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> SessionNapWakeResponse:
        """DEPRECATED — use ``POST /api/v1/sessions/nap/wake``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/sessions/nap/wake")
        return await _do_nap_wake(body, plugin)

    @router.post(
        f"{PREFIX}/dream",
        response_model=DreamResponse,
        status_code=status.HTTP_200_OK,
        summary="Trigger a dream cycle — DEPRECATED, use /sessions/dream",
        tags=["sessions (deprecated)"],
        operation_id="dream_deprecated",
        deprecated=True,
        include_in_schema=True,
    )
    async def dream_deprecated(
        body: DreamRequest,
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> DreamResponse:
        """DEPRECATED — use ``POST /api/v1/sessions/dream``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/sessions/dream")
        return await _do_dream(body, plugin)
