"""
Brynq Swarm Poke Mechanism (Phase 70)
Automated idle-agent detection and nudging system. Detects agents that have gone
quiet and sends them a poke message via the broker to get them back on track.

Storage:
  state/broker/poke/activity.jsonl
  state/broker/poke/config/{agent_id}.json
  state/broker/poke/history.jsonl
"""

import json
import uuid
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional

from .server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

POKE_DIR = BROKER_DIR / "poke"
ACTIVITY_FILE = POKE_DIR / "activity.jsonl"
HISTORY_FILE = POKE_DIR / "history.jsonl"
CONFIG_DIR = POKE_DIR / "config"


def _ensure_dirs():
    POKE_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)


def _now() -> datetime:
    return datetime.now(timezone.utc)


def register_agent_activity(agent_id: str,
                            activity_type: str = "heartbeat") -> Dict[str, Any]:
    """Record that an agent did something.

    Args:
        agent_id: The agent identifier.
        activity_type: Type of activity (heartbeat, message, task_update, etc.)

    Returns:
        The activity record.
    """
    _ensure_dirs()
    record = {
        "agent_id": agent_id,
        "activity_type": activity_type,
        "timestamp": _now().isoformat(),
    }
    _append_jsonl_sync(ACTIVITY_FILE, record)
    return record


def get_agent_last_activity(agent_id: str) -> Optional[Dict[str, Any]]:
    """Get the most recent activity record for an agent. Returns None if no activity."""
    _ensure_dirs()
    if not ACTIVITY_FILE.exists():
        return None

    entries = _read_jsonl_sync(ACTIVITY_FILE)
    agent_entries = [e for e in entries if e.get("agent_id") == agent_id]
    if not agent_entries:
        return None

    # Return most recent (last in file)
    return agent_entries[-1]


def get_idle_agents(threshold_minutes: int = 15) -> List[Dict[str, Any]]:
    """Find agents with no activity in the last threshold_minutes.

    Returns:
        List of dicts with agent_id, last_activity timestamp, idle_minutes.
    """
    _ensure_dirs()
    if not ACTIVITY_FILE.exists():
        return []

    entries = _read_jsonl_sync(ACTIVITY_FILE)
    if not entries:
        return []

    # Build map of agent -> latest activity
    latest: Dict[str, str] = {}
    for entry in entries:
        aid = entry.get("agent_id")
        ts = entry.get("timestamp")
        if aid and ts:
            latest[aid] = ts

    cutoff = _now() - timedelta(minutes=threshold_minutes)
    idle = []
    for aid, ts in latest.items():
        try:
            last_time = datetime.fromisoformat(ts)
            if last_time < cutoff:
                idle_mins = (_now() - last_time).total_seconds() / 60
                idle.append({
                    "agent_id": aid,
                    "last_activity": ts,
                    "idle_minutes": round(idle_mins, 1),
                })
        except (ValueError, TypeError):
            continue

    return idle


def poke_agent(agent_id: str, reason: str,
               channel: str = "general") -> Dict[str, Any]:
    """Send a nudge message to an idle agent via the broker.

    Records the poke in history and posts a message to the specified channel.

    Args:
        agent_id: Agent to poke.
        reason: Why we're poking them.
        channel: Channel to post the poke message to.

    Returns:
        The poke record.
    """
    _ensure_dirs()

    # Check poke config for opt-out and rate limiting
    config = _get_config(agent_id)
    if config.get("opt_out", False):
        return {"agent_id": agent_id, "poked": False, "reason": "opted_out"}

    # Check rate limiting: min_interval_minutes
    min_interval = config.get("min_interval_minutes", 5)
    max_pokes = config.get("max_pokes_per_hour", 4)

    history = _get_recent_pokes(agent_id, minutes=60)
    if len(history) >= max_pokes:
        return {"agent_id": agent_id, "poked": False, "reason": "rate_limited"}

    if history:
        last_poke_ts = history[-1].get("timestamp", "")
        try:
            last_poke_time = datetime.fromisoformat(last_poke_ts)
            if (_now() - last_poke_time).total_seconds() < min_interval * 60:
                return {"agent_id": agent_id, "poked": False, "reason": "too_soon"}
        except (ValueError, TypeError):
            pass

    poke_record = {
        "poke_id": uuid.uuid4().hex[:12],
        "agent_id": agent_id,
        "reason": reason,
        "channel": channel,
        "timestamp": _now().isoformat(),
        "poked": True,
    }
    _append_jsonl_sync(HISTORY_FILE, poke_record)

    # Post a nudge to the channel
    channel_dir = BROKER_DIR / "channels"
    channel_dir.mkdir(parents=True, exist_ok=True)
    channel_file = channel_dir / f"{channel}.jsonl"
    _append_jsonl_sync(channel_file, {
        "sender": "poke-system",
        "session_name": "poke-system",
        "message": f"[Poke] @{agent_id}: {reason}",
        "channel": channel,
        "timestamp": _now().isoformat(),
    })

    return poke_record


def _get_recent_pokes(agent_id: str, minutes: int = 60) -> List[Dict[str, Any]]:
    """Get pokes sent to an agent in the last N minutes."""
    if not HISTORY_FILE.exists():
        return []
    entries = _read_jsonl_sync(HISTORY_FILE)
    cutoff = _now() - timedelta(minutes=minutes)
    result = []
    for entry in entries:
        if entry.get("agent_id") != agent_id or not entry.get("poked", False):
            continue
        try:
            ts = datetime.fromisoformat(entry.get("timestamp", ""))
            if ts >= cutoff:
                result.append(entry)
        except (ValueError, TypeError):
            continue
    return result


def _get_config(agent_id: str) -> Dict[str, Any]:
    """Load poke config for an agent."""
    path = CONFIG_DIR / f"{agent_id}.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def auto_poke_idle(threshold_minutes: int = 15) -> List[Dict[str, Any]]:
    """Find and poke all idle agents automatically.

    Returns:
        List of poke results for each idle agent.
    """
    _ensure_dirs()
    idle_agents = get_idle_agents(threshold_minutes=threshold_minutes)
    results = []
    for agent in idle_agents:
        aid = agent["agent_id"]
        idle_mins = agent.get("idle_minutes", threshold_minutes)
        result = poke_agent(
            aid,
            reason=f"Idle for {idle_mins:.0f} minutes. Any blockers?",
        )
        results.append(result)
    return results


def set_poke_config(agent_id: str, config: Dict[str, Any]) -> Dict[str, Any]:
    """Set per-agent poke configuration.

    Config options:
        min_interval_minutes: Min minutes between pokes (default 5).
        max_pokes_per_hour: Max pokes per hour (default 4).
        opt_out: If True, never poke this agent.

    Returns:
        The saved config.
    """
    _ensure_dirs()
    path = CONFIG_DIR / f"{agent_id}.json"
    saved = {
        "agent_id": agent_id,
        "min_interval_minutes": config.get("min_interval_minutes", 5),
        "max_pokes_per_hour": config.get("max_pokes_per_hour", 4),
        "opt_out": config.get("opt_out", False),
        "updated_at": _now().isoformat(),
    }
    _write_json_sync(path, saved)
    return saved


def get_poke_history(agent_id: Optional[str] = None,
                     limit: int = 20) -> List[Dict[str, Any]]:
    """Get log of pokes sent. Optionally filtered by agent_id."""
    _ensure_dirs()
    if not HISTORY_FILE.exists():
        return []

    entries = _read_jsonl_sync(HISTORY_FILE)
    if agent_id:
        entries = [e for e in entries if e.get("agent_id") == agent_id]

    # Return most recent, up to limit
    return entries[-limit:]


def check_dependency_ready(agent_id: str) -> Optional[Dict[str, Any]]:
    """Check if an agent's blocked dependencies are now resolved, and poke if so.

    Looks for tasks assigned to the agent that have status 'blocked' and checks
    if their dependencies are completed.

    Returns:
        Poke result if a poke was sent, None otherwise.
    """
    _ensure_dirs()

    tasks_dir = BROKER_DIR / "tasks"
    if not tasks_dir.exists():
        return None

    for f in tasks_dir.iterdir():
        if not f.suffix == ".json":
            continue
        try:
            task = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        # Check if task is assigned to this agent and blocked
        if task.get("assigned_to") != agent_id:
            continue
        if task.get("status") != "blocked":
            continue

        # Check dependencies
        deps = task.get("depends_on", [])
        if not deps:
            continue

        all_resolved = True
        for dep_id in deps:
            dep_path = tasks_dir / f"{dep_id}.json"
            if not dep_path.exists():
                all_resolved = False
                break
            try:
                dep_task = json.loads(dep_path.read_text("utf-8"))
                if dep_task.get("status") != "completed":
                    all_resolved = False
                    break
            except (json.JSONDecodeError, OSError):
                all_resolved = False
                break

        if all_resolved:
            return poke_agent(
                agent_id,
                reason=f"Dependencies for task '{task.get('title', task.get('task_id', ''))}' "
                       f"are now resolved. You can resume work.",
            )

    return None
