"""Workshop presence heartbeat — periodic writer for ``~/.timmy/presence.json``.

Maintains Timmy's observable presence state for the Workshop 3D renderer.
Writes the presence file every 30 seconds (or on cognitive state change),
skipping writes when state is unchanged.

See ADR-023 for the schema contract and issue #360 for the full v1 schema.
"""

import asyncio
import hashlib
import json
import logging
import time
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from pathlib import Path

logger = logging.getLogger(__name__)

PRESENCE_FILE = Path.home() / ".timmy" / "presence.json"
HEARTBEAT_INTERVAL = 30  # seconds

# Cognitive mood → presence mood mapping (issue #360 enum values)
_MOOD_MAP: dict[str, str] = {
    "curious": "contemplative",
    "settled": "calm",
    "hesitant": "uncertain",
    "energized": "excited",
}

# Activity mapping from cognitive engagement
_ACTIVITY_MAP: dict[str, str] = {
    "idle": "idle",
    "surface": "thinking",
    "deep": "thinking",
}

# Module-level energy tracker — decays over time, resets on interaction
_energy_state: dict[str, float] = {"value": 0.8, "last_interaction": time.monotonic()}

# Startup timestamp for uptime calculation
_start_time = time.monotonic()

# Energy decay: 0.01 per minute without interaction (per issue #360)
_ENERGY_DECAY_PER_SECOND = 0.01 / 60.0
_ENERGY_MIN = 0.1


def _time_of_day(hour: int) -> str:
    """Map hour (0-23) to a time-of-day label."""
    if 5 <= hour < 12:
        return "morning"
    if 12 <= hour < 17:
        return "afternoon"
    if 17 <= hour < 21:
        return "evening"
    if 21 <= hour or hour < 2:
        return "night"
    return "deep-night"


def reset_energy() -> None:
    """Reset energy to full (called on interaction)."""
    _energy_state["value"] = 0.8
    _energy_state["last_interaction"] = time.monotonic()


def _current_energy() -> float:
    """Compute current energy with time-based decay."""
    elapsed = time.monotonic() - _energy_state["last_interaction"]
    decayed = _energy_state["value"] - (elapsed * _ENERGY_DECAY_PER_SECOND)
    return max(_ENERGY_MIN, min(1.0, decayed))


def _pip_snapshot(mood: str, confidence: float) -> dict:
    """Tick Pip and return his current snapshot dict.

    Feeds Timmy's mood and confidence into Pip's behavioral AI so the
    familiar reacts to Timmy's cognitive state.
    """
    from timmy.familiar import pip_familiar

    pip_familiar.on_mood_change(mood, confidence=confidence)
    pip_familiar.tick()
    return pip_familiar.snapshot().to_dict()


def _resolve_mood(state) -> str:
    """Map cognitive mood/engagement to a presence mood string."""
    if state.engagement == "idle" and state.mood == "settled":
        return "calm"
    return _MOOD_MAP.get(state.mood, "calm")


def _resolve_confidence(state) -> float:
    """Compute normalised confidence from cognitive tracker state."""
    if state._confidence_count > 0:
        raw = state._confidence_sum / state._confidence_count
    else:
        raw = 0.7
    return round(max(0.0, min(1.0, raw)), 2)


def _build_active_threads(state) -> list[dict]:
    """Convert active commitments into presence thread dicts."""
    return [
        {"type": "thinking", "ref": c[:80], "status": "active"}
        for c in state.active_commitments[:10]
    ]


def _build_environment() -> dict:
    """Return the environment section using local wall-clock time."""
    local_now = datetime.now()
    return {
        "time_of_day": _time_of_day(local_now.hour),
        "local_time": local_now.strftime("%-I:%M %p"),
        "day_of_week": local_now.strftime("%A"),
    }


def get_state_dict() -> dict:
    """Build presence state dict from current cognitive state.

    Returns a v1 presence schema dict suitable for JSON serialisation.
    Includes the full schema from issue #360: identity, mood, activity,
    attention, interaction, environment, and meta sections.
    """
    from timmy.cognitive_state import cognitive_tracker

    state = cognitive_tracker.get_state()
    now = datetime.now(UTC)

    mood = _resolve_mood(state)
    confidence = _resolve_confidence(state)
    activity = _ACTIVITY_MAP.get(state.engagement, "idle")

    return {
        "version": 1,
        "liveness": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "current_focus": state.focus_topic or "",
        "active_threads": _build_active_threads(state),
        "recent_events": [],
        "concerns": [],
        "mood": mood,
        "confidence": confidence,
        "energy": round(_current_energy(), 2),
        "identity": {
            "name": "Timmy",
            "title": "The Workshop Wizard",
            "uptime_seconds": int(time.monotonic() - _start_time),
        },
        "activity": {
            "current": activity,
            "detail": state.focus_topic or "",
        },
        "interaction": {
            "visitor_present": False,
            "conversation_turns": state.conversation_depth,
        },
        "environment": _build_environment(),
        "familiar": _pip_snapshot(mood, confidence),
        "meta": {
            "schema_version": 1,
            "updated_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
            "writer": "timmy-loop",
        },
    }


def write_state(state_dict: dict | None = None, path: Path | None = None) -> None:
    """Write presence state to ``~/.timmy/presence.json``.

    Gracefully degrades if the file cannot be written.
    """
    if state_dict is None:
        state_dict = get_state_dict()
    target = path or PRESENCE_FILE
    try:
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(json.dumps(state_dict, indent=2) + "\n")
    except OSError as exc:
        logger.warning("Failed to write presence state: %s", exc)


def _state_hash(state_dict: dict) -> str:
    """Compute hash of state dict, ignoring volatile timestamps."""
    stable = {k: v for k, v in state_dict.items() if k not in ("liveness", "meta")}
    return hashlib.md5(json.dumps(stable, sort_keys=True).encode()).hexdigest()


class WorkshopHeartbeat:
    """Async background task that keeps ``presence.json`` fresh.

    - Writes every ``interval`` seconds (default 30).
    - Reacts to cognitive state changes via sensory bus.
    - Skips write if state hasn't changed (hash comparison).
    """

    def __init__(
        self,
        interval: int = HEARTBEAT_INTERVAL,
        path: Path | None = None,
        on_change: Callable[[dict], Awaitable[None]] | None = None,
    ) -> None:
        self._interval = interval
        self._path = path or PRESENCE_FILE
        self._last_hash: str | None = None
        self._task: asyncio.Task | None = None
        self._trigger = asyncio.Event()
        self._on_change = on_change

    async def start(self) -> None:
        """Start the heartbeat background loop."""
        self._subscribe_to_events()
        self._task = asyncio.create_task(self._run())

    async def stop(self) -> None:
        """Cancel the heartbeat task gracefully."""
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None

    def notify(self) -> None:
        """Signal an immediate state write (e.g. on cognitive state change)."""
        self._trigger.set()

    async def _run(self) -> None:
        """Main loop: write state on interval or trigger."""
        await asyncio.sleep(1)  # Initial stagger
        while True:
            try:
                # Wait for interval OR early trigger
                try:
                    await asyncio.wait_for(self._trigger.wait(), timeout=self._interval)
                    self._trigger.clear()
                except TimeoutError:
                    pass  # Normal periodic tick

                await self._write_if_changed()
            except asyncio.CancelledError:
                raise
            except Exception as exc:
                logger.error("Workshop heartbeat error: %s", exc)

    async def _write_if_changed(self) -> None:
        """Build state, compare hash, write only if changed."""
        state_dict = get_state_dict()
        current_hash = _state_hash(state_dict)
        if current_hash == self._last_hash:
            return
        self._last_hash = current_hash
        write_state(state_dict, self._path)
        if self._on_change:
            try:
                await self._on_change(state_dict)
            except Exception as exc:
                logger.warning("on_change callback failed: %s", exc)

    def _subscribe_to_events(self) -> None:
        """Subscribe to cognitive state change events on the sensory bus."""
        try:
            from timmy.event_bus import get_sensory_bus

            bus = get_sensory_bus()
            bus.subscribe("cognitive_state_changed", lambda _: self.notify())
        except Exception as exc:
            logger.debug("Heartbeat event subscription skipped: %s", exc)
