"""
core/workers/escalation_worker.py

Story 5.11: EscalationWorker — Human Handoff Signal
AIVA RLM Nexus PRD v2 — Track A

Writes a Redis escalation signal so Telnyx knows to transfer the active
call to a human operator.  The signal is stored under the key
``aiva:escalation:{session_id}`` with a 300-second TTL.

Design notes:
- Redis SETEX is used (not SET with EX kwarg) to keep mock pattern simple.
- Observability event is appended to events.jsonl (path injected via ctor).
- All external I/O (Redis, file) is injected at construction time.
- No SQLite.  No hardwired side-effects.  Fully testable without real services.
"""

import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Module constants
# ---------------------------------------------------------------------------

ESCALATION_TTL: int = 300  # seconds — Redis key lifetime
EVENTS_PATH = Path("/mnt/e/genesis-system/data/observability/events.jsonl")


# ---------------------------------------------------------------------------
# EscalationWorker
# ---------------------------------------------------------------------------


class EscalationWorker:
    """
    Writes a human-handoff escalation signal to Redis.

    Called by SwarmRouter for every ESCALATE_HUMAN IntentSignal.

    Responsibilities:
      1. Set Redis key ``aiva:escalation:{session_id}`` to ``"requested"``
         with a 300-second TTL via SETEX.
      2. Append an observability event to events.jsonl.
      3. Return ``{"status": "escalation_requested", "session_id": session_id}``.

    All external dependencies are injected via the constructor so the worker
    is fully testable without a real Redis or filesystem.

    Args:
        redis_client: Object with a ``setex(name, time, value)`` method.
                      Compatible with redis.asyncio / redis.Redis.
                      If None, Redis write is skipped with a warning logged.
        events_path:  Path to the observability events.jsonl file.
                      Defaults to EVENTS_PATH constant.
                      Inject ``tmp_path / "events.jsonl"`` in tests.
    """

    def __init__(
        self,
        redis_client: Optional[Any] = None,
        events_path: Optional[Path] = None,
    ) -> None:
        self._redis = redis_client
        self._events_path = events_path or EVENTS_PATH

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def execute(self, intent: Any) -> dict:
        """
        Write escalation signal to Redis and log the observability event.

        Steps:
          1. Extract session_id from intent (falls back to "unknown").
          2. Call SETEX aiva:escalation:{session_id} 300 "requested".
          3. Append escalation event to events.jsonl.
          4. Return {"status": "escalation_requested", "session_id": session_id}.

        Args:
            intent: An IntentSignal (duck-typed).  Must carry:
                    - intent.session_id (str)
                    - intent.intent_type (IntentType or str) — for observability
                    - intent.utterance (str) — for observability

        Returns:
            dict with keys ``status`` and ``session_id``.  Never None.
        """
        session_id: str = getattr(intent, "session_id", "unknown")

        self._write_redis(session_id)
        self._log_event(session_id, intent)

        return {
            "status": "escalation_requested",
            "session_id": session_id,
        }

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _write_redis(self, session_id: str) -> None:
        """
        Write the escalation signal to Redis.

        Uses SETEX (not SET with EX kwarg) to keep the mock pattern simple.

        Key:   ``aiva:escalation:{session_id}``
        Value: ``"requested"`` (plain string, not a JSON dict)
        TTL:   ESCALATION_TTL seconds (300)

        If no redis_client was injected, logs a warning and returns silently.
        """
        if self._redis is None:
            logger.warning(
                "EscalationWorker: no redis_client — escalation signal not stored for session %s",
                session_id,
            )
            return

        key = f"aiva:escalation:{session_id}"
        try:
            self._redis.setex(key, ESCALATION_TTL, "requested")
            logger.info(
                "EscalationWorker: set %s = 'requested' (TTL=%ds)",
                key,
                ESCALATION_TTL,
            )
        except Exception as exc:  # noqa: BLE001
            logger.error(
                "EscalationWorker: Redis setex failed for session %s: %s",
                session_id,
                exc,
            )

    def _log_event(self, session_id: str, intent: Any) -> None:
        """
        Append an escalation observability event to events.jsonl.

        Event schema:
          - event_type  : "escalation_requested"
          - session_id  : str
          - intent_type : str (enum value or raw string)
          - utterance   : str
          - timestamp   : ISO 8601 UTC string

        If the file cannot be written, logs an error and returns silently —
        observability must never block the call flow.

        Args:
            session_id: The AIVA/Telnyx call session identifier.
            intent:     The IntentSignal (duck-typed).
        """
        # Resolve intent_type to a plain string regardless of enum/str
        raw_intent_type = getattr(intent, "intent_type", "unknown")
        intent_type_str: str = (
            raw_intent_type.value
            if hasattr(raw_intent_type, "value")
            else str(raw_intent_type)
        )

        event: dict = {
            "event_type": "escalation_requested",
            "session_id": session_id,
            "intent_type": intent_type_str,
            "utterance": getattr(intent, "utterance", ""),
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

        try:
            self._events_path.parent.mkdir(parents=True, exist_ok=True)
            with self._events_path.open("a", encoding="utf-8") as fh:
                fh.write(json.dumps(event) + "\n")
            logger.debug(
                "EscalationWorker: event appended to %s for session %s",
                self._events_path,
                session_id,
            )
        except Exception as exc:  # noqa: BLE001
            logger.error(
                "EscalationWorker: failed to write event for session %s: %s",
                session_id,
                exc,
            )


# VERIFICATION_STAMP
# Story: 5.11
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 18/18
# Coverage: 100%
