"""Loop monitoring and directive logic for Grukk.

The LoopSupervisor watches over running peon-loop instances by polling their
heartbeats and issuing directives when intervention is needed (e.g., a loop
is stuck, a task has timed out, or the King requests a pause).
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Any

from peon_mcp.grukk.api import GrukkAPIClient

log = logging.getLogger("peon-grukk")

# If a loop has been on the same task for longer than this (in seconds), flag it.
_STUCK_THRESHOLD_SECONDS = 30 * 60  # 30 minutes


class IssueType(Enum):
    """Types of loop issues Grukk can detect."""

    STALE_HEARTBEAT = "stale_heartbeat"
    TASK_TIMEOUT = "task_timeout"
    CIRCUIT_BREAKER = "circuit_breaker"
    REPEATED_FAILURE = "repeated_failure"
    LOOP_CRASHED = "loop_crashed"


class DirectiveType(Enum):
    """Types of directives Grukk can issue to a peon-loop."""

    PAUSE = "pause"
    RESUME = "resume"
    CANCEL_TASK = "cancel_task"
    RESTART = "restart"
    CHANGE_PRIORITY = "change_priority"


@dataclass
class LoopIssue:
    """A detected problem with a peon-loop instance."""

    loop_id: str
    issue_type: IssueType
    description: str
    project_id: str | None = None
    task_id: int | None = None
    duration_seconds: float | None = None
    recommended_action: DirectiveType | None = None


class LoopSupervisor:
    """Monitors peon-loop heartbeats and issues directives.

    The supervisor polls ``GET /api/loops`` to track active loops, detects
    anomalies (stale heartbeats, stuck tasks), and can issue directives to
    individual loops via the API.

    Parameters
    ----------
    client:
        :class:`GrukkAPIClient` instance for API access.
    heartbeat_ttl:
        Maximum age of a heartbeat (in seconds) before it's considered stale.
        Defaults to 90s to match the peon-loop heartbeat interval (30s write,
        90s TTL).
    """

    def __init__(
        self,
        client: GrukkAPIClient,
        heartbeat_ttl: float = 90.0,
    ) -> None:
        self.client = client
        self.heartbeat_ttl = heartbeat_ttl

        # In-memory tracking for stuck detection:
        # loop_id -> {"task_id": <id>, "first_seen_at": <monotonic time>}
        self._loop_task_tracking: dict[str, dict[str, Any]] = {}

    async def check_loops(self) -> list[LoopIssue]:
        """Poll heartbeats and return a list of detected issues.

        Fetches active loops from the API and checks:
        1. Heartbeat staleness (last_heartbeat > TTL)
        2. Stuck tasks (same task_id for > 30 minutes)
        """
        loops = self.client.get_active_loops()
        issues: list[LoopIssue] = []
        now = time.monotonic()
        utc_now = datetime.now(timezone.utc)

        seen_loop_ids: set[str] = set()

        for loop in loops:
            loop_id = loop.get("id", "unknown")
            seen_loop_ids.add(loop_id)
            project_id = loop.get("project_id")
            task_id = loop.get("task_id")

            # --- Check heartbeat staleness ---
            last_hb_str = loop.get("last_heartbeat")
            if last_hb_str:
                try:
                    last_hb = datetime.fromisoformat(last_hb_str.replace("Z", "+00:00"))
                    age_seconds = (utc_now - last_hb).total_seconds()
                    if age_seconds > self.heartbeat_ttl:
                        log.warning(
                            "Loop %s heartbeat is stale (%.0fs old, TTL=%.0fs).",
                            loop_id, age_seconds, self.heartbeat_ttl,
                        )
                        issues.append(LoopIssue(
                            loop_id=loop_id,
                            issue_type=IssueType.STALE_HEARTBEAT,
                            description=(
                                f"Heartbeat stale for {age_seconds:.0f}s "
                                f"(TTL {self.heartbeat_ttl:.0f}s).  "
                                f"Peon may have crashed."
                            ),
                            project_id=project_id,
                            task_id=task_id,
                            duration_seconds=age_seconds,
                            recommended_action=DirectiveType.RESTART,
                        ))
                except (ValueError, TypeError):
                    log.debug("Could not parse heartbeat timestamp for loop %s.", loop_id)

            # --- Check for stuck tasks ---
            # Skip idle loops (no task assigned).
            if task_id is None:
                self._loop_task_tracking.pop(loop_id, None)
                continue

            tracking = self._loop_task_tracking.get(loop_id)
            if tracking is None or tracking.get("task_id") != task_id:
                # New task or first time seeing this loop -- start tracking.
                self._loop_task_tracking[loop_id] = {
                    "task_id": task_id,
                    "first_seen_at": now,
                    "alerted": False,
                }
            else:
                # Same task as before -- check duration.
                first_seen = tracking["first_seen_at"]
                duration = now - first_seen
                if duration > _STUCK_THRESHOLD_SECONDS and not tracking.get("alerted"):
                    log.warning(
                        "Loop %s stuck on task %s for %.0f minutes.",
                        loop_id, task_id, duration / 60,
                    )
                    tracking["alerted"] = True
                    issues.append(LoopIssue(
                        loop_id=loop_id,
                        issue_type=IssueType.TASK_TIMEOUT,
                        description=(
                            f"Loop stuck on task {task_id} for "
                            f"{duration / 60:.0f} minutes (threshold "
                            f"{_STUCK_THRESHOLD_SECONDS / 60:.0f} min)."
                        ),
                        project_id=project_id,
                        task_id=task_id,
                        duration_seconds=duration,
                        recommended_action=DirectiveType.CANCEL_TASK,
                    ))

        # Clean up tracking for loops that are no longer active.
        stale_ids = set(self._loop_task_tracking) - seen_loop_ids
        for stale_id in stale_ids:
            del self._loop_task_tracking[stale_id]

        return issues

    async def issue_directive(
        self,
        loop_id: str,
        directive_type: str,
        payload: dict[str, Any] | None = None,
    ) -> bool:
        """Send a directive to a running peon-loop.

        Parameters
        ----------
        loop_id:
            Identifier of the target peon-loop instance.
        directive_type:
            One of the DirectiveType values (pause, resume, cancel_task, etc.).
        payload:
            Optional additional data for the directive (e.g., new priority).

        Returns
        -------
        bool
            True if the directive was accepted, False otherwise.
        """
        log.info("Issuing directive '%s' to loop %s.", directive_type, loop_id)
        result = self.client.create_directive(loop_id, directive_type, payload)
        if result is not None:
            log.info("Directive accepted for loop %s.", loop_id)
            return True
        log.warning("Failed to issue directive '%s' to loop %s.", directive_type, loop_id)
        return False

    async def get_loop_summary(self) -> dict[str, Any]:
        """Return a summary of all active loops for status reporting.

        Fetches heartbeats and returns a dict suitable for Grukk to relay
        to the King.
        """
        loops = self.client.get_active_loops()
        return {
            "active_loops": len(loops),
            "loops": [
                {
                    "loop_id": l.get("id", "unknown"),
                    "project_id": l.get("project_id"),
                    "pid": l.get("pid"),
                    "hostname": l.get("hostname"),
                    "last_heartbeat": l.get("last_heartbeat"),
                    "started_at": l.get("started_at"),
                }
                for l in loops
            ],
        }
