"""
cortex/core/boot_loader.py — Session Boot Loader (Issue #361).

Constructs a deterministic boot sequence when a new session starts, ensuring
the agent has continuity context from previous sessions. The boot loader
retrieves identity claims, active commitments, stable preferences, recent
session summaries, and the last session handoff — then pre-warms working
memory with these items.

Boot Sequence (deterministic order):
    1. Load last session handoff (from sleep pipeline output)
    2. Load identity claims (who is the user, who is the agent)
    3. Load active commitments/open loops
    4. Load high-stability preferences/facts
    5. Load recent session summaries (last 3 sessions)
    6. Pre-warm working memory with boot items

Dependencies:
    - cortex.storage.adapter.StorageAdapter (all data access)
    - cortex.core.context_compiler.ContextCompiler (token counting)
    - cortex.core.working_memory.WorkingMemoryCache (cache pre-warming)
    - cortex.config.CortexConfig (boot configuration)

Dependents:
    - cortex.api.plugin.CortexPlugin (calls boot_session() at session start)
"""
from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# Warn threshold for any single boot step (milliseconds).
_STEP_WARN_MS = 500.0


@dataclass
class BootSequence:
    """Result of session boot — all context needed for continuity.

    Attributes:
        handoff: Last session's handoff summary dict, or None if unavailable.
        identity_claims: User/agent identity facts (SemanticClaim-like dicts).
        active_commitments: Open loops and pending tasks.
        stable_preferences: High-stability preferences and facts.
        recent_summaries: Last N session summaries (EpisodeEvent-like dicts).
        boot_tokens: Total estimated token count of all boot items.
        boot_time_ms: Wall-clock time to build the boot sequence.
    """

    handoff: Optional[Dict[str, Any]] = None
    identity_claims: List[Any] = field(default_factory=list)
    active_commitments: List[Any] = field(default_factory=list)
    stable_preferences: List[Any] = field(default_factory=list)
    recent_summaries: List[Any] = field(default_factory=list)
    boot_tokens: int = 0
    boot_time_ms: float = 0.0


class SessionBootLoader:
    """Builds a deterministic boot sequence for session continuity.

    Retrieves identity, commitments, preferences, summaries, and handoff
    data from storage, respecting a token budget, and pre-warms working
    memory with the results.

    Args:
        storage: StorageAdapter for all data access.
        compiler: ContextCompiler for token estimation.
        working_memory: WorkingMemoryCache for pre-warming.
        config: CortexConfig with boot.* settings.
    """

    def __init__(
        self,
        storage: Any,
        compiler: Any,
        working_memory: Any,
        config: Any,
    ) -> None:
        self._storage = storage
        # TODO(#574): ContextCompiler is injected but never used. The boot loader
        # uses _estimate_tokens() (rough 4-chars-per-token heuristic) instead of
        # delegating to the compiler for accurate token counting. Either wire
        # self._compiler.count_tokens() into _estimate_tokens() or remove the
        # compiler parameter from the constructor if it's truly unnecessary.
        self._compiler = compiler
        self._working_memory = working_memory
        self._config = config

        # Read boot config with safe defaults
        self._enabled: bool = getattr(config, "boot_enabled", True)
        self._max_recent: int = getattr(config, "boot_max_recent_sessions", 3)
        self._min_stability: float = getattr(config, "boot_min_stability_threshold", 0.7)
        self._budget_fraction: float = getattr(config, "boot_budget_fraction", 0.15)

    async def boot(
        self,
        session_id: str,
        owner_id: str,
        context_budget: int,
    ) -> BootSequence:
        """Run the full boot sequence and return a BootSequence.

        Steps are executed in deterministic order. Each step is timed and
        logged; warnings are emitted for any step exceeding 500ms.

        Args:
            session_id: The new session being booted.
            owner_id: Memory owner namespace.
            context_budget: Total context window tokens available.

        Returns:
            BootSequence with all loaded boot items.
        """
        if not self._enabled:
            logger.info("Boot loader disabled — returning empty boot sequence")
            return BootSequence()

        boot_budget = int(context_budget * self._budget_fraction)
        if boot_budget <= 0:
            logger.warning(
                "Boot budget is 0 (context_budget=%d, fraction=%.2f) — empty boot",
                context_budget, self._budget_fraction,
            )
            return BootSequence()

        t_start = time.monotonic()
        tokens_used = 0

        # Step 1: Load last session handoff
        handoff, step_tokens = await self._timed_step(
            "handoff",
            self._load_handoff(owner_id),
            boot_budget - tokens_used,
        )
        tokens_used += step_tokens

        # Step 2: Load identity claims
        identity, step_tokens = await self._timed_step(
            "identity",
            self._load_identity(owner_id),
            boot_budget - tokens_used,
        )
        tokens_used += step_tokens

        # Step 3: Load active commitments
        commitments, step_tokens = await self._timed_step(
            "commitments",
            self._load_commitments(owner_id),
            boot_budget - tokens_used,
        )
        tokens_used += step_tokens

        # Step 4: Load stable preferences
        preferences, step_tokens = await self._timed_step(
            "preferences",
            self._load_preferences(owner_id),
            boot_budget - tokens_used,
        )
        tokens_used += step_tokens

        # Step 5: Load recent session summaries
        summaries, step_tokens = await self._timed_step(
            "summaries",
            self._load_recent_summaries(owner_id, limit=self._max_recent),
            boot_budget - tokens_used,
        )
        tokens_used += step_tokens

        boot_time_ms = (time.monotonic() - t_start) * 1000.0

        sequence = BootSequence(
            handoff=handoff if isinstance(handoff, dict) else None,
            identity_claims=identity if isinstance(identity, list) else [],
            active_commitments=commitments if isinstance(commitments, list) else [],
            stable_preferences=preferences if isinstance(preferences, list) else [],
            recent_summaries=summaries if isinstance(summaries, list) else [],
            boot_tokens=tokens_used,
            boot_time_ms=round(boot_time_ms, 2),
        )

        logger.info(
            "Boot complete: session=%s tokens=%d/%d time=%.1fms "
            "handoff=%s identity=%d commitments=%d prefs=%d summaries=%d",
            session_id,
            tokens_used,
            boot_budget,
            boot_time_ms,
            "yes" if sequence.handoff else "no",
            len(sequence.identity_claims),
            len(sequence.active_commitments),
            len(sequence.stable_preferences),
            len(sequence.recent_summaries),
        )

        # Step 6: Pre-warm working memory
        await self._warm_cache(session_id, sequence)

        return sequence

    # ------------------------------------------------------------------
    # Private boot steps
    # ------------------------------------------------------------------

    async def _load_handoff(self, owner_id: str) -> Optional[Dict[str, Any]]:
        """Load the most recent session handoff from storage.

        Uses StorageAdapter.get_handoff_packets() which returns handoff_packet
        rows as plain dicts.
        """
        if self._storage is None:
            return None
        try:
            packets = await self._storage.get_handoff_packets(
                owner_id=owner_id,
                status="active",
                limit=1,
            )
            if packets:
                return packets[0]
        except NotImplementedError:
            logger.debug("Storage does not support get_handoff_packets — skipping handoff")
        except Exception as exc:
            logger.warning("Boot: failed to load handoff: %s", exc)
        return None

    async def _load_identity(self, owner_id: str) -> List[Any]:
        """Load identity claims (type=identity or type=agent_identity).

        Uses get_claims_by_status('active') and filters in Python by
        checking the predicate/subject for identity-related patterns, since
        SemanticClaim doesn't have a direct 'type' field — we use the
        subject field matching identity patterns.
        """
        if self._storage is None:
            return []
        try:
            # Use FTS search for identity-related claims
            results = await self._storage.fts_search(
                query="identity",
                table="claims",
                top_k=50,
                owner_id=owner_id,
            )
            identity_claims = []
            for r in results:
                # FTSResult has item_id and content
                claim = await self._storage.get_claim(r.item_id)
                if claim is None:
                    continue
                if claim.status != "active":
                    continue
                # Check if claim is identity-related by predicate/subject patterns
                pred_lower = (claim.predicate or "").lower()
                subj_lower = (claim.subject or "").lower()
                if any(kw in pred_lower or kw in subj_lower for kw in (
                    "identity", "name", "is called", "goes by", "agent_identity",
                )):
                    identity_claims.append(claim)
            return identity_claims
        except Exception as exc:
            logger.warning("Boot: failed to load identity claims: %s", exc)
            return []

    async def _load_commitments(self, owner_id: str) -> List[Any]:
        """Load active commitments/open loops.

        Uses get_claims_by_status('active') and filters for commitment-related
        predicates in Python.
        """
        if self._storage is None:
            return []
        try:
            results = await self._storage.fts_search(
                query="commitment OR promise OR todo OR open_loop",
                table="claims",
                top_k=50,
                owner_id=owner_id,
            )
            commitments = []
            for r in results:
                claim = await self._storage.get_claim(r.item_id)
                if claim is None:
                    continue
                if claim.status != "active":
                    continue
                pred_lower = (claim.predicate or "").lower()
                if any(kw in pred_lower for kw in (
                    "commit", "promise", "todo", "will", "should",
                    "needs to", "open_loop", "pending", "owes",
                )):
                    commitments.append(claim)
            return commitments
        except Exception as exc:
            logger.warning("Boot: failed to load commitments: %s", exc)
            return []

    async def _load_preferences(self, owner_id: str) -> List[Any]:
        """Load high-stability preferences and facts.

        Uses get_claims_by_status('active') and filters for preference/fact
        claims with high confidence (as proxy for stability since SemanticClaim
        uses confidence rather than stability).
        """
        if self._storage is None:
            return []
        try:
            active_claims = await self._storage.get_claims_by_status(
                status="active",
                owner_id=owner_id,
            )
            preferences = []
            for claim in active_claims:
                # Use confidence as stability proxy; threshold from config
                if claim.confidence < self._min_stability:
                    continue
                pred_lower = (claim.predicate or "").lower()
                if any(kw in pred_lower for kw in (
                    "prefer", "like", "dislike", "use", "fact",
                    "always", "never", "favorite", "favourite",
                )):
                    preferences.append(claim)
            return preferences
        except Exception as exc:
            logger.warning("Boot: failed to load preferences: %s", exc)
            return []

    async def _load_recent_summaries(
        self, owner_id: str, limit: int = 3
    ) -> List[Any]:
        """Load the last N session summaries from episodes.

        Uses get_events() to fetch recent session summary events, since
        session summaries are stored as events with type 'session_summary'.
        """
        if self._storage is None:
            return []
        try:
            events = await self._storage.get_events(
                owner_id=owner_id,
                event_type="session_summary",
                limit=limit,
            )
            return events if events else []
        except Exception as exc:
            logger.warning("Boot: failed to load recent summaries: %s", exc)
            return []

    async def _warm_cache(
        self, session_id: str, boot_sequence: BootSequence
    ) -> None:
        """Pre-warm working memory with boot sequence items.

        Maps boot sections to working memory categories:
            handoff        → session_facts
            identity       → entities
            commitments    → open_loops
            preferences    → session_facts
            summaries      → turn_summaries
        """
        if self._working_memory is None:
            return

        try:
            # Handoff → session_facts
            if boot_sequence.handoff:
                await self._working_memory.put(
                    key="boot_handoff",
                    value=boot_sequence.handoff,
                    category="session_facts",
                    session_id=session_id,
                    ttl=0,  # persist for session lifetime
                )

            # Identity claims → entities
            for i, claim in enumerate(boot_sequence.identity_claims):
                await self._working_memory.put(
                    key=f"boot_identity_{i}",
                    value=claim,
                    category="entities",
                    session_id=session_id,
                    ttl=0,
                )

            # Commitments → open_loops
            for i, claim in enumerate(boot_sequence.active_commitments):
                await self._working_memory.put(
                    key=f"boot_commitment_{i}",
                    value=claim,
                    category="open_loops",
                    session_id=session_id,
                    ttl=0,
                )

            # Preferences → session_facts
            for i, claim in enumerate(boot_sequence.stable_preferences):
                await self._working_memory.put(
                    key=f"boot_preference_{i}",
                    value=claim,
                    category="session_facts",
                    session_id=session_id,
                    ttl=0,
                )

            # Summaries → turn_summaries
            for i, summary in enumerate(boot_sequence.recent_summaries):
                await self._working_memory.put(
                    key=f"boot_summary_{i}",
                    value=summary,
                    category="turn_summaries",
                    session_id=session_id,
                    ttl=0,
                )

            total_items = (
                (1 if boot_sequence.handoff else 0)
                + len(boot_sequence.identity_claims)
                + len(boot_sequence.active_commitments)
                + len(boot_sequence.stable_preferences)
                + len(boot_sequence.recent_summaries)
            )
            logger.info(
                "Boot: pre-warmed %d items into working memory for session %s",
                total_items, session_id,
            )
        except Exception as exc:
            logger.warning("Boot: failed to warm working memory: %s", exc)

    # ------------------------------------------------------------------
    # Timing helper
    # ------------------------------------------------------------------

    async def _timed_step(
        self,
        step_name: str,
        coro: Any,
        remaining_budget: int,
    ) -> tuple[Any, int]:
        """Execute a boot step with timing and budget tracking.

        Args:
            step_name: Human-readable name for logging.
            coro: The awaitable to execute.
            remaining_budget: Tokens remaining in boot budget.

        Returns:
            (result, tokens_used) tuple. tokens_used is estimated from
            the result content.
        """
        if remaining_budget <= 0:
            logger.debug("Boot step '%s' skipped — no budget remaining", step_name)
            return None, 0

        t0 = time.monotonic()
        result = await coro
        elapsed_ms = (time.monotonic() - t0) * 1000.0

        if elapsed_ms > _STEP_WARN_MS:
            logger.warning(
                "Boot step '%s' took %.1fms (threshold: %.0fms)",
                step_name, elapsed_ms, _STEP_WARN_MS,
            )
        else:
            logger.debug("Boot step '%s' completed in %.1fms", step_name, elapsed_ms)

        # Estimate tokens from result
        tokens = self._estimate_tokens(result)
        return result, min(tokens, remaining_budget)

    def _estimate_tokens(self, data: Any) -> int:
        """Rough token estimate for boot data (~4 chars per token)."""
        if data is None:
            return 0
        if isinstance(data, list):
            return sum(self._estimate_tokens(item) for item in data)
        if isinstance(data, dict):
            text = str(data)
        elif hasattr(data, "__dict__"):
            text = str(vars(data))
        else:
            text = str(data)
        # ~4 chars per token approximation
        return max(1, len(text) // 4)
