"""
cortex/sleep/s3_handoff.py — S3: Handoff Packet Builder

Issue #360, Stage 3 of the Sleep Pipeline.

Responsibilities:
  1. Build a compact, boot-ready JSON packet for the next session wake
  2. Packet contents:
       - session_summary (from S1)
       - key_facts: top cornerstones (up to 5)
       - latest_decisions: session claims tagged as 'goal' or 'decision'
         (last 5 by created_at)
       - open_loops: open_loop rows with status='open' (up to 10)
       - promoted_ids: claim IDs promoted in S2
       - metadata: session_id, owner_id, generated_at
  3. Persist to handoff_packet table (status='active') as the boot context
  4. Return the packet dict and persistence id

Design principles:
  - All methods are async
  - Missing data → empty list (graceful degradation)
  - No LLM calls — purely data assembly
  - Packet is intentionally compact to fit inside a startup token budget
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------

@dataclass
class S3Result:
    """Result of the S3 handoff packet build stage."""

    session_id: str
    owner_id: str
    packet_id: Optional[str] = None
    packet: Dict[str, Any] = field(default_factory=dict)
    completed_at: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )
    success: bool = True
    error: Optional[str] = None

    def to_dict(self) -> dict:
        return {
            "session_id": self.session_id,
            "owner_id": self.owner_id,
            "packet_id": self.packet_id,
            "packet": self.packet,
            "completed_at": self.completed_at,
            "success": self.success,
            "error": self.error,
        }


# ---------------------------------------------------------------------------
# S3HandoffBuilder
# ---------------------------------------------------------------------------


class S3HandoffBuilder:
    """
    S3: Handoff Packet Builder.

    Assembles a compact JSON snapshot of the session state that the next
    session can load at boot to resume context quickly.

    Packet schema::

        {
          "schema_version": "1.0",
          "session_id": "<str>",
          "owner_id": "<str>",
          "generated_at": "<ISO 8601>",
          "session_summary": "<text from S1>",
          "key_facts": [{"label": str, "content": str}, ...],        # cornerstones
          "latest_decisions": [{"subject": str, "predicate": str,    # top-5 goals/decisions
                                "object": str, "confidence": float}],
          "open_loops": [{"id": str, "title": str,                   # open threads
                          "description": str, "priority": int}],
          "promoted_ids": ["<claim_id>", ...]                        # S2 output
        }

    Args:
        storage:            StorageAdapter for reading cornerstones, claims, open loops.
        max_key_facts:      Max cornerstones to include.
        max_decisions:      Max decision claims to include.
        max_open_loops:     Max open_loop rows to include.
    """

    def __init__(
        self,
        storage: "StorageAdapter",
        max_key_facts: int = 5,
        max_decisions: int = 5,
        max_open_loops: int = 10,
    ) -> None:
        self._storage = storage
        self._max_key_facts = max_key_facts
        self._max_decisions = max_decisions
        self._max_open_loops = max_open_loops

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def run(
        self,
        session_id: str,
        owner_id: str = "default",
        session_summary: str = "",
        promoted_ids: Optional[List[str]] = None,
    ) -> S3Result:
        """
        Build and persist the handoff packet for this session.

        Args:
            session_id:      The closing session ID.
            owner_id:        Owner/tenant identifier.
            session_summary: Text summary from S1 (empty string if S1 failed).
            promoted_ids:    List of claim IDs promoted in S2 (empty if S2 failed).

        Returns:
            S3Result with the assembled packet and its persistence ID.
        """
        logger.info("[S3] building handoff for session=%s owner=%s", session_id, owner_id)
        result = S3Result(session_id=session_id, owner_id=owner_id)
        promoted_ids = promoted_ids or []

        # Assemble packet components (each is non-fatal)
        key_facts = await self._get_key_facts(owner_id)
        latest_decisions = await self._get_latest_decisions(session_id)
        open_loops = await self._get_open_loops(owner_id)

        now = datetime.now(timezone.utc).isoformat()
        packet: Dict[str, Any] = {
            "schema_version": "1.0",
            "session_id": session_id,
            "owner_id": owner_id,
            "generated_at": now,
            "session_summary": session_summary,
            "key_facts": key_facts,
            "latest_decisions": latest_decisions,
            "open_loops": open_loops,
            "promoted_ids": promoted_ids,
        }
        result.packet = packet

        # Persist
        try:
            packet_id = await self._storage.create_handoff_packet(
                owner_id=owner_id,
                session_id=session_id,
                status="active",
                summary_text=session_summary,
                payload_json=json.dumps(packet, ensure_ascii=False),
                generated_at=now,
            )
            result.packet_id = packet_id
        except Exception as exc:
            logger.error("[S3] persist failed for session=%s: %s", session_id, exc)
            result.success = False
            result.error = str(exc)

        result.completed_at = datetime.now(timezone.utc).isoformat()
        logger.info(
            "[S3] done: session=%s key_facts=%d decisions=%d loops=%d promoted=%d packet=%s",
            session_id, len(key_facts), len(latest_decisions),
            len(open_loops), len(promoted_ids), result.packet_id,
        )
        return result

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    async def _get_key_facts(self, owner_id: str) -> List[Dict[str, Any]]:
        """Return compact cornerstone records (label + content)."""
        try:
            cornerstones = await self._storage.get_cornerstones(
                owner_id=owner_id, status="active"
            )
            return [
                {
                    "label": getattr(cs, "label", ""),
                    "content": getattr(cs, "content", ""),
                }
                for cs in cornerstones[: self._max_key_facts]
            ]
        except Exception as exc:
            logger.warning("[S3] get_cornerstones failed: %s", exc)
            return []

    async def _get_latest_decisions(self, session_id: str) -> List[Dict[str, Any]]:
        """
        Return the last N claims from this session that look like
        decisions/goals (intrinsic_type='goal' or category='goals').
        Falls back to any session claim sorted by created_at.
        """
        try:
            claims = await self._storage.get_session_claims(session_id)
            # Prefer goal-type claims
            decision_claims = [
                c for c in claims
                if getattr(c, "intrinsic_type", "fact") == "goal"
                or getattr(c, "category", "") == "goals"
            ]
            # Fallback: all claims if no decisions found
            pool = decision_claims or claims
            # Sort by created_at descending and cap
            pool = sorted(
                pool,
                key=lambda c: getattr(c, "created_at", "") or "",
                reverse=True,
            )[: self._max_decisions]
            return [
                {
                    "id": c.id,
                    "subject": c.subject,
                    "predicate": c.predicate,
                    "object": c.object_value,
                    "confidence": round(getattr(c, "confidence", 0.5), 3),
                }
                for c in pool
            ]
        except Exception as exc:
            logger.warning("[S3] get_latest_decisions failed: %s", exc)
            return []

    async def _get_open_loops(self, owner_id: str) -> List[Dict[str, Any]]:
        """Return open_loop rows with status='open', ordered by priority."""
        try:
            conn = await self._storage._get_conn()
            async with conn.execute(
                """
                SELECT id, title, description, priority, due_at, created_at
                FROM open_loop
                WHERE owner_id = ? AND status = 'open'
                ORDER BY priority DESC, created_at ASC
                LIMIT ?
                """,
                (owner_id, self._max_open_loops),
            ) as cur:
                rows = await cur.fetchall()
            return [
                {
                    "id": r["id"],
                    "title": r["title"],
                    "description": r["description"],
                    "priority": r["priority"],
                    "due_at": r["due_at"],
                }
                for r in rows
            ]
        except Exception as exc:
            logger.warning("[S3] get_open_loops failed: %s", exc)
            return []
