"""Observable cognitive state for Timmy.

Tracks Timmy's internal cognitive signals — focus, engagement, mood,
and active commitments — so external systems (Matrix avatar, dashboard)
can render observable behaviour.

State is published via ``workshop_state.py`` → ``presence.json`` and the
WebSocket relay.  The old ``~/.tower/timmy-state.txt`` file has been
deprecated (see #384).
"""

import asyncio
import json
import logging
from dataclasses import asdict, dataclass, field

from timmy.confidence import estimate_confidence

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------

ENGAGEMENT_LEVELS = ("idle", "surface", "deep")
MOOD_VALUES = ("curious", "settled", "hesitant", "energized")


@dataclass
class CognitiveState:
    """Observable snapshot of Timmy's cognitive state."""

    focus_topic: str | None = None
    engagement: str = "idle"  # idle | surface | deep
    mood: str = "settled"  # curious | settled | hesitant | energized
    conversation_depth: int = 0
    last_initiative: str | None = None
    active_commitments: list[str] = field(default_factory=list)

    # Internal tracking (not written to state file)
    _confidence_sum: float = field(default=0.0, repr=False)
    _confidence_count: int = field(default=0, repr=False)

    # ------------------------------------------------------------------
    # Serialisation helpers
    # ------------------------------------------------------------------

    def to_dict(self) -> dict:
        """Public fields only (exclude internal tracking)."""
        d = asdict(self)
        d.pop("_confidence_sum", None)
        d.pop("_confidence_count", None)
        return d


# ---------------------------------------------------------------------------
# Cognitive signal extraction
# ---------------------------------------------------------------------------

# Keywords that suggest deep engagement
_DEEP_KEYWORDS = frozenset(
    {
        "architecture",
        "design",
        "implement",
        "refactor",
        "debug",
        "analyze",
        "investigate",
        "deep dive",
        "explain how",
        "walk me through",
        "step by step",
    }
)

# Keywords that suggest initiative / commitment
_COMMITMENT_KEYWORDS = frozenset(
    {
        "i will",
        "i'll",
        "let me",
        "i'm going to",
        "plan to",
        "commit to",
        "i propose",
        "i suggest",
    }
)


def _infer_engagement(message: str, response: str) -> str:
    """Classify engagement level from the exchange."""
    combined = (message + " " + response).lower()
    if any(kw in combined for kw in _DEEP_KEYWORDS):
        return "deep"
    # Short exchanges are surface-level
    if len(response.split()) < 15:
        return "surface"
    return "surface"


def _infer_mood(response: str, confidence: float) -> str:
    """Derive mood from response signals."""
    lower = response.lower()
    if confidence < 0.4:
        return "hesitant"
    if "!" in response and any(w in lower for w in ("great", "exciting", "love", "awesome")):
        return "energized"
    if "?" in response or any(w in lower for w in ("wonder", "interesting", "curious", "hmm")):
        return "curious"
    return "settled"


def _extract_topic(message: str) -> str | None:
    """Best-effort topic extraction from the user message.

    Takes the first meaningful clause (up to 60 chars) as a topic label.
    """
    text = message.strip()
    if not text:
        return None
    # Strip leading question words
    for prefix in ("what is ", "how do ", "can you ", "please ", "hey timmy "):
        if text.lower().startswith(prefix):
            text = text[len(prefix) :]
    # Truncate
    if len(text) > 60:
        text = text[:57] + "..."
    return text.strip() or None


def _extract_commitments(response: str) -> list[str]:
    """Pull commitment phrases from Timmy's response."""
    commitments: list[str] = []
    lower = response.lower()
    for kw in _COMMITMENT_KEYWORDS:
        idx = lower.find(kw)
        if idx == -1:
            continue
        # Grab the rest of the sentence (up to period/newline, max 80 chars)
        start = idx
        end = len(lower)
        for sep in (".", "\n", "!"):
            pos = lower.find(sep, start)
            if pos != -1:
                end = min(end, pos)
        snippet = response[start : min(end, start + 80)].strip()
        if snippet:
            commitments.append(snippet)
    return commitments[:3]  # Cap at 3


# ---------------------------------------------------------------------------
# Tracker singleton
# ---------------------------------------------------------------------------


class CognitiveTracker:
    """Maintains Timmy's cognitive state.

    State is consumed via ``to_json()`` / ``get_state()`` and published
    externally by ``workshop_state.py`` → ``presence.json``.
    """

    def __init__(self) -> None:
        self.state = CognitiveState()

    def update(self, user_message: str, response: str) -> CognitiveState:
        """Update cognitive state from a chat exchange.

        Called after each chat round-trip in ``session.py``.
        Emits a ``cognitive_state_changed`` event to the sensory bus so
        downstream consumers (WorkshopHeartbeat, etc.) react immediately.
        """
        confidence = estimate_confidence(response)

        prev_mood = self.state.mood
        prev_engagement = self.state.engagement

        # Track running confidence average
        self.state._confidence_sum += confidence
        self.state._confidence_count += 1

        self.state.conversation_depth += 1
        self.state.focus_topic = _extract_topic(user_message) or self.state.focus_topic
        self.state.engagement = _infer_engagement(user_message, response)
        self.state.mood = _infer_mood(response, confidence)

        # Extract commitments from response
        new_commitments = _extract_commitments(response)
        if new_commitments:
            self.state.last_initiative = new_commitments[0]
            # Merge, keeping last 5
            seen = set(self.state.active_commitments)
            for c in new_commitments:
                if c not in seen:
                    self.state.active_commitments.append(c)
                    seen.add(c)
            self.state.active_commitments = self.state.active_commitments[-5:]

        # Emit cognitive_state_changed to close the sense → react loop
        self._emit_change(prev_mood, prev_engagement)

        return self.state

    def _emit_change(self, prev_mood: str, prev_engagement: str) -> None:
        """Fire-and-forget sensory event for cognitive state change."""
        try:
            from timmy.event_bus import get_sensory_bus
            from timmy.events import SensoryEvent

            event = SensoryEvent(
                source="cognitive",
                event_type="cognitive_state_changed",
                data={
                    "mood": self.state.mood,
                    "engagement": self.state.engagement,
                    "focus_topic": self.state.focus_topic or "",
                    "depth": self.state.conversation_depth,
                    "mood_changed": self.state.mood != prev_mood,
                    "engagement_changed": self.state.engagement != prev_engagement,
                },
            )
            bus = get_sensory_bus()
            # Fire-and-forget — don't block the chat response
            try:
                loop = asyncio.get_running_loop()
                loop.create_task(bus.emit(event))
            except RuntimeError:
                # No running loop (sync context / tests) — skip emission
                pass
        except Exception as exc:
            logger.debug("Cognitive event emission skipped: %s", exc)

    def get_state(self) -> CognitiveState:
        """Return current cognitive state."""
        return self.state

    def reset(self) -> None:
        """Reset to idle state (e.g. on session reset)."""
        self.state = CognitiveState()

    def to_json(self) -> str:
        """Serialise current state as JSON (for API / WebSocket consumers)."""
        return json.dumps(self.state.to_dict())


# Module-level singleton
cognitive_tracker = CognitiveTracker()
