"""
Event Dispatcher — watches server log events and triggers AI God responses.

Parses deaths, joins, leaves, advancements, PvP kills, and chat blasphemy
from the Minecraft server log. Each event type has a configurable trigger
probability and cooldown. When fired, formats the event as a god_system
prompt and sends it to the model.

Usage:
    from agent.tools.event_dispatcher import EventDispatcher

    dispatcher = EventDispatcher(config, llm_callback, rcon_fn)
    dispatcher.process_log_line(line)  # call for each new log line

Config keys:
    event_triggers_enabled: bool (default true)
    event_cooldown_global: int seconds (default 30)
    event_cooldown_per_player: int seconds (default 120)
    event_triggers: dict of event_type -> {probability, cooldown_override, enabled}
"""

import json
import logging
import random
import re
import threading
import time
from collections import defaultdict
from typing import Any, Callable, Dict, List, Optional

log = logging.getLogger(__name__)

# ── Event patterns ─────────────────────────────────────────────────────────

DEATH_PATTERNS = [
    (re.compile(r'(\w+) fell from a high place'), "fall", "fell from a high place"),
    (re.compile(r'(\w+) hit the ground too hard'), "fall", "hit the ground too hard"),
    (re.compile(r'(\w+) was slain by (\w+)'), "mob_kill", "was slain by {1}"),
    (re.compile(r'(\w+) was shot by (\w+)'), "ranged_kill", "was shot by {1}"),
    (re.compile(r'(\w+) drowned'), "drown", "drowned"),
    (re.compile(r'(\w+) tried to swim in lava'), "lava", "tried to swim in lava"),
    (re.compile(r'(\w+) burned to death'), "fire", "burned to death"),
    (re.compile(r'(\w+) went up in flames'), "fire", "went up in flames"),
    (re.compile(r'(\w+) blew up'), "explosion", "blew up"),
    (re.compile(r'(\w+) was blown up by (\w+)'), "explosion", "was blown up by {1}"),
    (re.compile(r'(\w+) suffocated in a wall'), "suffocate", "suffocated in a wall"),
    (re.compile(r'(\w+) starved to death'), "starve", "starved to death"),
    (re.compile(r'(\w+) was killed by (\w+)'), "pvp_or_mob", "was killed by {1}"),
    (re.compile(r'(\w+) was pricked to death'), "cactus", "was pricked to death"),
    (re.compile(r'(\w+) withered away'), "wither", "withered away"),
    (re.compile(r'(\w+) was impaled by (\w+)'), "trident", "was impaled by {1}"),
    (re.compile(r'(\w+) fell out of the world'), "void", "fell out of the world"),
]

JOIN_PATTERN = re.compile(r'(\w+) joined the game')
LEAVE_PATTERN = re.compile(r'(\w+) left the game')
ADVANCEMENT_PATTERN = re.compile(r'(\w+) has made the advancement \[(.+?)\]')
CHALLENGE_PATTERN = re.compile(r'(\w+) has completed the challenge \[(.+?)\]')

# Players that killed other players — detect from death messages
PVP_KILLERS = {"was slain by", "was shot by", "was killed by"}

# ── Default trigger config ─────────────────────────────────────────────────

DEFAULT_TRIGGERS = {
    "death_fall":       {"probability": 0.40, "enabled": True},
    "death_lava":       {"probability": 0.35, "enabled": True},
    "death_mob":        {"probability": 0.25, "enabled": True},
    "death_pvp":        {"probability": 0.60, "enabled": True},
    "death_drown":      {"probability": 0.20, "enabled": True},
    "death_fire":       {"probability": 0.25, "enabled": True},
    "death_explosion":  {"probability": 0.30, "enabled": True},
    "death_void":       {"probability": 0.50, "enabled": True},
    "death_starve":     {"probability": 0.30, "enabled": True},
    "death_other":      {"probability": 0.15, "enabled": True},
    "join_first":       {"probability": 1.00, "enabled": True},
    "join_returning":   {"probability": 0.30, "enabled": True},
    "leave_long":       {"probability": 0.20, "enabled": True},  # played 2+ hours
    "advancement":      {"probability": 0.50, "enabled": True},
    "challenge":        {"probability": 0.70, "enabled": True},
    "mass_death":       {"probability": 0.80, "enabled": True},  # 2+ deaths in 30s
}

GLOBAL_COOLDOWN = 30      # seconds between any event trigger
PLAYER_COOLDOWN = 120     # seconds between triggers for same player
MASS_DEATH_WINDOW = 30    # seconds to detect mass deaths


class EventDispatcher:
    def __init__(self, config: dict, llm_callback: Callable, rcon_fn: Callable):
        """
        config: server config dict
        llm_callback: function(prompt: str, mode: str) -> dict with {message, commands, reasoning}
        rcon_fn: function(command: str) -> str
        """
        self.config = config
        self.llm_callback = llm_callback
        self.rcon = rcon_fn
        self.enabled = config.get("event_triggers_enabled", True)

        # Merge user trigger config with defaults
        self.triggers = dict(DEFAULT_TRIGGERS)
        user_triggers = config.get("event_triggers", {})
        for k, v in user_triggers.items():
            if k in self.triggers:
                self.triggers[k].update(v)

        self.global_cooldown = config.get("event_cooldown_global", GLOBAL_COOLDOWN)
        self.player_cooldown = config.get("event_cooldown_per_player", PLAYER_COOLDOWN)

        # State
        self._lock = threading.Lock()
        self._last_global_trigger = 0.0
        self._last_player_trigger: Dict[str, float] = defaultdict(float)
        self._recent_deaths: List[tuple] = []  # (timestamp, player, cause)
        self._player_join_times: Dict[str, float] = {}
        self._known_players: set = set()  # players we've seen before
        self._known_players_path = config.get("event_known_players_path",
            config.get("log_path", "").replace("logs/latest.log", "aigod_known_players.json"))

        self._load_known_players()

    def _load_known_players(self):
        try:
            with open(self._known_players_path) as f:
                self._known_players = set(json.load(f))
        except (FileNotFoundError, json.JSONDecodeError):
            self._known_players = set()

    def _save_known_players(self):
        try:
            with open(self._known_players_path, "w") as f:
                json.dump(list(self._known_players), f)
        except Exception:
            pass

    def _can_trigger(self, player: str) -> bool:
        """Check global and per-player cooldowns."""
        now = time.time()
        with self._lock:
            if now - self._last_global_trigger < self.global_cooldown:
                return False
            if now - self._last_player_trigger[player] < self.player_cooldown:
                return False
        return True

    def _mark_triggered(self, player: str):
        now = time.time()
        with self._lock:
            self._last_global_trigger = now
            self._last_player_trigger[player] = now

    def _should_fire(self, event_type: str) -> bool:
        """Roll probability for this event type."""
        trigger = self.triggers.get(event_type, {"probability": 0.0, "enabled": False})
        if not trigger.get("enabled", False):
            return False
        return random.random() < trigger.get("probability", 0.0)

    def _fire_event(self, event_text: str, player: str, event_type: str):
        """Send the event to the LLM and execute the response."""
        self._mark_triggered(player)

        log.info(f"[event] firing {event_type} for {player}: {event_text}")

        try:
            result = self.llm_callback(event_text, "god_system")
            if not result:
                return

            message = result.get("message", "")
            commands = result.get("commands", [])

            # Send God's message
            if message:
                # Format for Minecraft chat
                safe_msg = message.replace('"', '\\"')
                prefix = self.config.get("god_chat_prefix", "[\u00a7d\u00a7lGOD\u00a7r]")
                self.rcon(f'tellraw @a {{"text":"{prefix} {safe_msg}"}}')

            # Execute commands
            for cmd in commands[:8]:  # cap at 8 commands
                if isinstance(cmd, str) and cmd.strip():
                    try:
                        self.rcon(cmd.strip())
                    except Exception as e:
                        log.warning(f"[event] command failed: {cmd} — {e}")

            log.info(f"[event] {event_type}: sent message + {len(commands)} commands")

        except Exception as e:
            log.error(f"[event] LLM callback failed for {event_type}: {e}")

    # ── Log line processing ────────────────────────────────────────────────

    def process_log_line(self, line: str):
        """Parse a log line and dispatch any events."""
        if not self.enabled:
            return

        # Strip the log prefix to get the message
        # Format: [HH:MM:SS INFO]: message
        msg_match = re.search(r'INFO\]: (.+)$', line)
        if not msg_match:
            return
        msg = msg_match.group(1).strip()

        self._check_death(msg)
        self._check_join(msg)
        self._check_leave(msg)
        self._check_advancement(msg)

    def _check_death(self, msg: str):
        now = time.time()

        for pattern, death_type, desc_template in DEATH_PATTERNS:
            m = pattern.search(msg)
            if not m:
                continue

            player = m.group(1)
            killer = m.group(2) if m.lastindex and m.lastindex >= 2 else None
            description = desc_template.format(**{str(i): m.group(i+1) for i in range(m.lastindex or 0)}) if m.lastindex else desc_template

            # Track for mass death detection
            with self._lock:
                self._recent_deaths.append((now, player, death_type))
                # Prune old deaths
                self._recent_deaths = [(t, p, c) for t, p, c in self._recent_deaths if now - t < MASS_DEATH_WINDOW]

                # Check for mass death
                recent_count = len(self._recent_deaths)

            # Determine event type
            if killer and killer in self._known_players:
                event_type = "death_pvp"
            elif death_type == "fall":
                event_type = "death_fall"
            elif death_type == "lava":
                event_type = "death_lava"
            elif death_type == "drown":
                event_type = "death_drown"
            elif death_type in ("fire",):
                event_type = "death_fire"
            elif death_type == "explosion":
                event_type = "death_explosion"
            elif death_type == "void":
                event_type = "death_void"
            elif death_type == "starve":
                event_type = "death_starve"
            elif death_type in ("mob_kill", "ranged_kill", "pvp_or_mob"):
                event_type = "death_mob"
            else:
                event_type = "death_other"

            # Mass death override
            if recent_count >= 2 and self._should_fire("mass_death") and self._can_trigger(player):
                victims = list(set(p for _, p, _ in self._recent_deaths))
                event_text = f"[EVENT:MASS_DEATH] {recent_count} players died within {MASS_DEATH_WINDOW} seconds: {', '.join(victims)}"
                threading.Thread(target=self._fire_event, args=(event_text, player, "mass_death"), daemon=True).start()
                with self._lock:
                    self._recent_deaths.clear()
                return

            if not self._can_trigger(player):
                return
            if not self._should_fire(event_type):
                return

            event_text = f"[EVENT:DEATH] Player {player} {description}"
            if killer:
                event_text += f" (killer: {killer})"

            threading.Thread(target=self._fire_event, args=(event_text, player, event_type), daemon=True).start()
            return

    def _check_join(self, msg: str):
        m = JOIN_PATTERN.search(msg)
        if not m:
            return

        player = m.group(1)
        self._player_join_times[player] = time.time()

        is_first = player not in self._known_players
        if is_first:
            self._known_players.add(player)
            self._save_known_players()
            event_type = "join_first"
            event_text = f"[EVENT:JOIN] Player {player} joined the game for the first time"
        else:
            event_type = "join_returning"
            event_text = f"[EVENT:JOIN] Player {player} joined the game (returning player)"

        if not self._can_trigger(player):
            return
        if not self._should_fire(event_type):
            return

        threading.Thread(target=self._fire_event, args=(event_text, player, event_type), daemon=True).start()

    def _check_leave(self, msg: str):
        m = LEAVE_PATTERN.search(msg)
        if not m:
            return

        player = m.group(1)
        join_time = self._player_join_times.pop(player, None)

        if join_time:
            session_minutes = (time.time() - join_time) / 60
            if session_minutes >= 120:  # 2+ hours
                event_type = "leave_long"
                event_text = f"[EVENT:LEAVE] Player {player} left the game after {session_minutes:.0f} minutes"

                if not self._can_trigger(player):
                    return
                if not self._should_fire(event_type):
                    return

                threading.Thread(target=self._fire_event, args=(event_text, player, event_type), daemon=True).start()

    def _check_advancement(self, msg: str):
        m = ADVANCEMENT_PATTERN.search(msg) or CHALLENGE_PATTERN.search(msg)
        if not m:
            return

        player = m.group(1)
        advancement = m.group(2)
        is_challenge = bool(CHALLENGE_PATTERN.search(msg))

        event_type = "challenge" if is_challenge else "advancement"
        event_text = f"[EVENT:ADVANCEMENT] Player {player} earned [{advancement}]"

        if not self._can_trigger(player):
            return
        if not self._should_fire(event_type):
            return

        threading.Thread(target=self._fire_event, args=(event_text, player, event_type), daemon=True).start()
