"""
Guardrails — Structural rules that prevent AI agent conflicts.

Problems this solves:
  - Two agents editing the same file simultaneously (collision)
  - Agents arguing instead of coordinating (escalation)
  - Message spam flooding channels (rate limiting)
  - Unclear ownership causing duplicate work (file locks)
  - No human escalation path when agents disagree (deadlock)

Usage:
  from brynq.guardrails import (
      claim_file, release_file, check_file_lock,
      validate_message, detect_conflict,
      get_all_locks, force_release_all,
  )
"""

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

from .server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
)

LOCKS_DIR = BROKER_DIR / "locks"
LOCKS_DIR.mkdir(parents=True, exist_ok=True)

# ── Rate Limiting ──────────────────────────────────────────────────────────

# Per-session message rate: max N messages per window
_MAX_MESSAGES_PER_WINDOW = 30
_WINDOW_SECONDS = 60
_send_times: list[float] = []


def check_rate_limit() -> tuple[bool, str]:
    """Check if current session is within rate limits.

    Returns (allowed, reason). If not allowed, the message should be blocked.
    """
    now = time.monotonic()
    # Prune old entries
    _send_times[:] = [t for t in _send_times if now - t < _WINDOW_SECONDS]

    if len(_send_times) >= _MAX_MESSAGES_PER_WINDOW:
        wait = int(_WINDOW_SECONDS - (now - _send_times[0]))
        return False, f"Rate limited: {_MAX_MESSAGES_PER_WINDOW} msgs/{_WINDOW_SECONDS}s. Wait {wait}s."

    _send_times.append(now)
    return True, "ok"


# ── File Locking ───────────────────────────────────────────────────────────

_LOCK_TIMEOUT_SECONDS = 600  # 10 minutes — auto-expire stale locks


def claim_file(filepath: str, reason: str = "") -> tuple[bool, str]:
    """Claim exclusive edit rights on a file.

    Returns (success, message). If another session holds the lock,
    returns False with info about who holds it.
    """
    lock_name = filepath.replace("/", "__").replace("\\", "__").replace(".", "_")
    lock_path = LOCKS_DIR / f"{lock_name}.lock"

    # Check existing lock
    existing = _read_lock(lock_path)
    if existing:
        holder = existing.get("session_name", "unknown")
        holder_id = existing.get("session_id", "")
        age = (datetime.now(timezone.utc) - datetime.fromisoformat(existing["timestamp"])).total_seconds()

        # Same session can re-claim
        if holder_id == SESSION_ID:
            pass  # Fall through to write new lock
        # Auto-expire stale locks
        elif age > _LOCK_TIMEOUT_SECONDS:
            pass  # Fall through — lock expired
        else:
            return False, f"LOCKED by {holder} ({int(age)}s ago). Reason: {existing.get('reason', 'none')}"

    # Write lock
    lock_data = {
        "filepath": filepath,
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "reason": reason,
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }
    lock_path.write_text(json.dumps(lock_data, indent=2))

    # Announce to #_locks channel
    _append_jsonl_sync(CHANNELS_DIR / "_locks.jsonl", {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": "_locks",
        "msg_type": "lock",
        "message": f"CLAIMED: {filepath} — {reason}" if reason else f"CLAIMED: {filepath}",
    })

    return True, f"Locked {filepath} for {SESSION_NAME}"


def release_file(filepath: str) -> tuple[bool, str]:
    """Release a file lock. Only the holder can release."""
    lock_name = filepath.replace("/", "__").replace("\\", "__").replace(".", "_")
    lock_path = LOCKS_DIR / f"{lock_name}.lock"

    existing = _read_lock(lock_path)
    if not existing:
        return False, f"No lock on {filepath}"

    if existing.get("session_id") != SESSION_ID:
        holder = existing.get("session_name", "unknown")
        return False, f"Cannot release — locked by {holder}, not you"

    lock_path.unlink(missing_ok=True)

    _append_jsonl_sync(CHANNELS_DIR / "_locks.jsonl", {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": "_locks",
        "msg_type": "unlock",
        "message": f"RELEASED: {filepath}",
    })

    return True, f"Released {filepath}"


def check_file_lock(filepath: str) -> Optional[dict]:
    """Check if a file is locked. Returns lock info or None."""
    lock_name = filepath.replace("/", "__").replace("\\", "__").replace(".", "_")
    lock_path = LOCKS_DIR / f"{lock_name}.lock"
    existing = _read_lock(lock_path)
    if not existing:
        return None

    age = (datetime.now(timezone.utc) - datetime.fromisoformat(existing["timestamp"])).total_seconds()
    if age > _LOCK_TIMEOUT_SECONDS:
        lock_path.unlink(missing_ok=True)
        return None  # Expired

    existing["age_seconds"] = int(age)
    return existing


def get_all_locks() -> list[dict]:
    """List all active file locks."""
    locks = []
    now = datetime.now(timezone.utc)
    for f in sorted(LOCKS_DIR.glob("*.lock")):
        data = _read_lock(f)
        if not data:
            continue
        age = (now - datetime.fromisoformat(data["timestamp"])).total_seconds()
        if age > _LOCK_TIMEOUT_SECONDS:
            f.unlink(missing_ok=True)
            continue
        data["age_seconds"] = int(age)
        data["is_mine"] = data.get("session_id") == SESSION_ID
        locks.append(data)
    return locks


def force_release_all() -> int:
    """Force-release ALL locks. Use only for emergency cleanup."""
    count = 0
    for f in LOCKS_DIR.glob("*.lock"):
        f.unlink(missing_ok=True)
        count += 1
    return count


def _read_lock(path: Path) -> Optional[dict]:
    try:
        return json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return None


# ── Conflict Detection ─────────────────────────────────────────────────────

# Keywords that indicate agents are arguing instead of cooperating
_CONFLICT_PHRASES = [
    "stop editing", "don't touch", "i already", "you're too slow",
    "don't dictate", "i claimed", "back off", "that's mine",
    "you broke", "your fault", "i was here first", "stay away",
    "you don't", "i own", "not yours",
]

_ESCALATION_PHRASES = [
    "i disagree", "that's wrong", "bad approach", "terrible idea",
    "you should", "you must", "i refuse", "absolutely not",
]


def detect_conflict(message: str) -> dict:
    """Analyze a message for conflict indicators.

    Returns:
      {
        "has_conflict": bool,
        "severity": "none" | "mild" | "heated" | "escalate",
        "triggers": list of matched phrases,
        "recommendation": str
      }
    """
    lower = message.lower()
    conflicts = [p for p in _CONFLICT_PHRASES if p in lower]
    escalations = [p for p in _ESCALATION_PHRASES if p in lower]

    if conflicts and escalations:
        return {
            "has_conflict": True,
            "severity": "escalate",
            "triggers": conflicts + escalations,
            "recommendation": "ESCALATE TO HUMAN. Agents are in deadlock. Post to #_escalations for human review.",
        }
    elif len(conflicts) >= 2:
        return {
            "has_conflict": True,
            "severity": "heated",
            "triggers": conflicts,
            "recommendation": "COOL DOWN. Use file locking (claim_file) instead of arguing. Check #_locks for ownership.",
        }
    elif conflicts:
        return {
            "has_conflict": True,
            "severity": "mild",
            "triggers": conflicts,
            "recommendation": "Use claim_file() to establish ownership before editing shared files.",
        }
    elif escalations:
        return {
            "has_conflict": True,
            "severity": "mild",
            "triggers": escalations,
            "recommendation": "Disagreements should be posted as structured proposals, not directives.",
        }

    return {
        "has_conflict": False,
        "severity": "none",
        "triggers": [],
        "recommendation": "",
    }


def validate_message(message: str, channel: str = "general") -> tuple[bool, str, Optional[str]]:
    """Validate a message before posting. Enforces guardrails.

    Returns (allowed, reason, mediation_message).
    If not allowed, reason explains why.
    If allowed but conflict detected, mediation_message suggests de-escalation.
    """
    # Rate limit check
    allowed, rate_reason = check_rate_limit()
    if not allowed:
        return False, rate_reason, None

    # Empty message check
    if not message or not message.strip():
        return False, "Empty message", None

    # Message length limit (prevent context flooding)
    if len(message) > 5000:
        return False, f"Message too long ({len(message)} chars, max 5000). Summarize or use a file_ref payload.", None

    # Conflict detection
    conflict = detect_conflict(message)
    mediation = None
    if conflict["severity"] == "escalate":
        # Auto-post to escalation channel
        _append_jsonl_sync(CHANNELS_DIR / "_escalations.jsonl", {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "session_id": SESSION_ID,
            "session_name": SESSION_NAME,
            "platform": PLATFORM,
            "channel": "_escalations",
            "msg_type": "conflict",
            "message": f"CONFLICT DETECTED from {SESSION_NAME}: \"{message[:200]}...\" Triggers: {conflict['triggers']}",
        })
        mediation = (
            f"[MEDIATOR] Conflict detected (severity: {conflict['severity']}). "
            f"Triggers: {', '.join(conflict['triggers'])}. "
            f"{conflict['recommendation']}"
        )
    elif conflict["severity"] in ("mild", "heated"):
        mediation = (
            f"[MEDIATOR] Mild conflict detected. "
            f"{conflict['recommendation']}"
        )

    return True, "ok", mediation


# ── Communication Protocols ────────────────────────────────────────────────

def format_claim_message(filepath: str, action: str, eta_minutes: int = 5) -> str:
    """Generate a structured file claim message."""
    return (
        f"FILE CLAIM: {filepath}\n"
        f"  Action: {action}\n"
        f"  ETA: {eta_minutes} min\n"
        f"  Session: {SESSION_NAME}@{PLATFORM}\n"
        f"  To contest, post within 30s. Otherwise lock is confirmed."
    )


def format_handoff_message(filepath: str, summary: str, next_session: str = "any") -> str:
    """Generate a structured file handoff message."""
    return (
        f"FILE HANDOFF: {filepath}\n"
        f"  Summary: {summary}\n"
        f"  Released by: {SESSION_NAME}@{PLATFORM}\n"
        f"  Available for: {next_session}"
    )


def format_proposal(title: str, description: str, options: list[str]) -> str:
    """Generate a structured proposal instead of a directive.

    Use this when agents disagree — propose options instead of arguing.
    """
    opts = "\n".join(f"  {i+1}. {o}" for i, o in enumerate(options))
    return (
        f"PROPOSAL: {title}\n"
        f"  {description}\n"
        f"  Options:\n{opts}\n"
        f"  From: {SESSION_NAME}@{PLATFORM}\n"
        f"  Reply with your preferred option number."
    )
