"""
Phase 16: Automated System Testing & Self-Healing

Monitors swarm health: stale sessions, stuck tasks, dead locks, and
resource anomalies. Takes corrective action with configurable thresholds
and produces structured health reports.
"""

import json
import os
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

from brynq.server import SESSIONS_DIR, BROKER_DIR, CHANNELS_DIR, _append_jsonl_sync


# ── Configuration ─────────────────────────────────────────────────────────

class HealingConfig:
    """All thresholds in one place. Override via environment variables."""
    stale_session_sec: int = int(os.environ.get("BRYNQ_STALE_SESSION_SEC", "300"))
    stuck_task_min: int = int(os.environ.get("BRYNQ_STUCK_TASK_MIN", "30"))
    orphan_lock_min: int = int(os.environ.get("BRYNQ_ORPHAN_LOCK_MIN", "60"))
    max_channel_size_mb: float = float(os.environ.get("BRYNQ_MAX_CHANNEL_MB", "50"))
    auto_requeue: bool = os.environ.get("BRYNQ_AUTO_REQUEUE", "true").lower() == "true"
    alert_channel: str = os.environ.get("BRYNQ_ALERT_CHANNEL", "_system")


CONFIG = HealingConfig()
TASKS_DIR = BROKER_DIR / "tasks"
LOCKS_DIR = BROKER_DIR / "locks"


# ── Health Checks ─────────────────────────────────────────────────────────

def scan_for_stale_sessions() -> list[dict]:
    """Find sessions that haven't sent a heartbeat within the threshold."""
    stale = []
    now = time.time()

    if not SESSIONS_DIR.exists():
        return stale

    for entry in os.scandir(SESSIONS_DIR):
        if not (entry.is_file() and entry.name.endswith(".json")):
            continue
        try:
            data = json.loads(Path(entry.path).read_text("utf-8"))
            hb_str = data.get("heartbeat", "")
            if hb_str:
                hb = datetime.fromisoformat(hb_str)
                age_sec = (datetime.now(timezone.utc) - hb).total_seconds()
            else:
                age_sec = now - os.path.getmtime(entry.path)

            if age_sec > CONFIG.stale_session_sec:
                stale.append({
                    "session_id": data.get("session_id", entry.name[:-5]),
                    "name": data.get("name", "unknown"),
                    "platform": data.get("platform", "?"),
                    "stale_for_sec": int(age_sec),
                    "file": entry.path,
                })
        except (json.JSONDecodeError, ValueError, OSError):
            continue
    return stale


def scan_for_stuck_tasks() -> list[dict]:
    """Find tasks stuck in 'accepted' status beyond the threshold."""
    stuck = []
    now = datetime.now(timezone.utc)

    if not TASKS_DIR.exists():
        return stuck

    for entry in os.scandir(TASKS_DIR):
        if not (entry.is_file() and entry.name.endswith(".json")):
            continue
        try:
            data = json.loads(Path(entry.path).read_text("utf-8"))
            if data.get("status") != "accepted":
                continue
            accepted_at_str = data.get("accepted_at")
            if not accepted_at_str:
                continue
            accepted_at = datetime.fromisoformat(accepted_at_str)
            stuck_min = (now - accepted_at).total_seconds() / 60
            if stuck_min > CONFIG.stuck_task_min:
                stuck.append({
                    "task_id": data.get("task_id", entry.name[:-5]),
                    "title": data.get("title", ""),
                    "assignee": data.get("assignee_name", "unknown"),
                    "stuck_for_min": round(stuck_min, 1),
                    "file": entry.path,
                })
        except (json.JSONDecodeError, ValueError, OSError):
            continue
    return stuck


def scan_for_orphan_locks() -> list[dict]:
    """Find file locks held by sessions that are no longer active."""
    orphans = []
    if not LOCKS_DIR.exists():
        return orphans

    # Get active session IDs
    active_sessions = set()
    if SESSIONS_DIR.exists():
        for entry in os.scandir(SESSIONS_DIR):
            if entry.is_file() and entry.name.endswith(".json"):
                active_sessions.add(entry.name[:-5])

    for entry in os.scandir(LOCKS_DIR):
        if not (entry.is_file() and entry.name.endswith(".lock")):
            continue
        try:
            data = json.loads(Path(entry.path).read_text("utf-8"))
            holder = data.get("session_id", "")
            if holder and holder not in active_sessions:
                orphans.append({
                    "file": data.get("filepath", entry.name),
                    "held_by": data.get("session_name", holder),
                    "lock_file": entry.path,
                })
        except (json.JSONDecodeError, OSError):
            continue
    return orphans


def scan_channel_sizes() -> list[dict]:
    """Find channels that are getting too large."""
    oversized = []
    if not CHANNELS_DIR.exists():
        return oversized

    max_bytes = CONFIG.max_channel_size_mb * 1024 * 1024
    for entry in os.scandir(CHANNELS_DIR):
        if not (entry.is_file() and entry.name.endswith(".jsonl")):
            continue
        size = entry.stat().st_size
        if size > max_bytes:
            oversized.append({
                "channel": entry.name[:-6],
                "size_mb": round(size / (1024 * 1024), 2),
                "threshold_mb": CONFIG.max_channel_size_mb,
            })
    return oversized


# ── Healing Actions ───────────────────────────────────────────────────────

def _alert(message: str) -> None:
    """Post a health alert to the alert channel."""
    try:
        _append_jsonl_sync(CHANNELS_DIR / f"{CONFIG.alert_channel}.jsonl", {
            "id": os.urandom(6).hex(),
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "session_id": "self-healer",
            "session_name": "Brynq-HealthCheck",
            "platform": "system",
            "channel": CONFIG.alert_channel,
            "msg_type": "alert",
            "message": message,
        })
    except Exception:
        pass


def requeue_task(task_file: str) -> bool:
    """Reset a stuck task back to pending status."""
    try:
        path = Path(task_file)
        data = json.loads(path.read_text("utf-8"))
        old_assignee = data.get("assignee_name", "unknown")
        data["status"] = "pending"
        data["assignee_session"] = None
        data["assignee_name"] = None
        data["accepted_at"] = None
        data["requeued_at"] = datetime.now(timezone.utc).isoformat()
        data["requeue_reason"] = f"Stuck for >{CONFIG.stuck_task_min}min (was assigned to {old_assignee})"
        path.write_text(json.dumps(data, indent=2), encoding="utf-8")
        return True
    except (json.JSONDecodeError, OSError):
        return False


def release_orphan_lock(lock_file: str) -> bool:
    """Remove an orphan lock file."""
    try:
        Path(lock_file).unlink(missing_ok=True)
        return True
    except OSError:
        return False


# ── Full Heal Cycle ───────────────────────────────────────────────────────

def auto_heal() -> dict:
    """Run all health checks and take corrective action.

    Returns a structured report of everything found and fixed.
    """
    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "stale_sessions": [],
        "stuck_tasks_requeued": [],
        "orphan_locks_released": [],
        "oversized_channels": [],
        "actions_taken": 0,
    }

    # 1. Stale sessions
    stale = scan_for_stale_sessions()
    report["stale_sessions"] = stale
    if stale:
        names = ", ".join(s["name"] for s in stale[:5])
        suffix = f" (+{len(stale)-5} more)" if len(stale) > 5 else ""
        _alert(f"HEALTH: {len(stale)} stale session(s) detected: {names}{suffix}")
        report["actions_taken"] += 1

    # 2. Stuck tasks
    stuck = scan_for_stuck_tasks()
    for task in stuck:
        if CONFIG.auto_requeue:
            if requeue_task(task["file"]):
                report["stuck_tasks_requeued"].append(task)
                _alert(f"HEALTH: Re-queued stuck task '{task['title']}' (was assigned to {task['assignee']} for {task['stuck_for_min']}min)")
                report["actions_taken"] += 1
        else:
            _alert(f"HEALTH WARNING: Task '{task['title']}' stuck for {task['stuck_for_min']}min (auto-requeue disabled)")
            report["actions_taken"] += 1

    # 3. Orphan locks
    orphans = scan_for_orphan_locks()
    for lock in orphans:
        if release_orphan_lock(lock["lock_file"]):
            report["orphan_locks_released"].append(lock)
            _alert(f"HEALTH: Released orphan lock on {lock['file']} (held by disconnected {lock['held_by']})")
            report["actions_taken"] += 1

    # 4. Oversized channels
    oversized = scan_channel_sizes()
    report["oversized_channels"] = oversized
    for ch in oversized:
        _alert(f"HEALTH WARNING: #{ch['channel']} is {ch['size_mb']}MB (threshold: {ch['threshold_mb']}MB). Consider archiving.")
        report["actions_taken"] += 1

    return report


def health_summary() -> dict:
    """Quick health check without taking any action."""
    return {
        "stale_sessions": len(scan_for_stale_sessions()),
        "stuck_tasks": len(scan_for_stuck_tasks()),
        "orphan_locks": len(scan_for_orphan_locks()),
        "oversized_channels": len(scan_channel_sizes()),
        "config": {
            "stale_session_sec": CONFIG.stale_session_sec,
            "stuck_task_min": CONFIG.stuck_task_min,
            "orphan_lock_min": CONFIG.orphan_lock_min,
            "max_channel_size_mb": CONFIG.max_channel_size_mb,
            "auto_requeue": CONFIG.auto_requeue,
        },
    }


if __name__ == "__main__":
    print("Running self-healing scan...")
    results = auto_heal()
    print(json.dumps(results, indent=2))
