"""
InsAIts Anchor Trigger Engine
==============================
Determines WHEN to inject anchor messages based on session state,
tool call patterns, anomaly thresholds, and randomized cadences.

Three layers:
  Layer 1 (Opus/orchestrator) — periodic + anomaly + session start + critical
  Layer 2 (Agents)            — agent spawn + anomaly threshold
  Layer 3 (Subagents)         — subagent spawn only

Premium implementation — proprietary, not published to GitHub.
Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import logging
import random
import time
from typing import Any, Dict, List, Optional, Set

# Import TriggerType from the shared _types module to avoid circular imports.
# Both facade and premium import from the same source → enum identity guaranteed.
from insa_its.anchor._types import TriggerType

logger = logging.getLogger("insaits.anchor.trigger")


class AnchorTriggerEngine:
    """Evaluates tool call context and returns which layers to inject.

    Stateful: tracks tool call count, anomaly deltas, pending delayed
    injections, and injection history for dedup.
    """

    def __init__(
        self,
        opus_refresh_min: int = 20,
        opus_refresh_max: int = 40,
        anomaly_delta_threshold: int = 25,
        anomaly_delay_max_seconds: int = 45,
        stealth_disables_injection: bool = True,
        layer1_enabled: bool = True,
        layer2_enabled: bool = True,
        layer3_enabled: bool = True,
        subagent_refresh_min: int = 8,
        subagent_refresh_max: int = 15,
        subagent_time_interval_seconds: int = 240,  # 4 minutes
        agent_refresh_min: int = 15,
        agent_refresh_max: int = 30,
        agent_time_interval_seconds: int = 180,  # 3 minutes
        agent_task_start_injection: bool = True,
        subagent_periodic_enabled: bool = False,
    ) -> None:
        self._opus_min = opus_refresh_min
        self._opus_max = opus_refresh_max
        self._subagent_min = subagent_refresh_min
        self._subagent_max = subagent_refresh_max
        self._anomaly_delta = anomaly_delta_threshold
        self._anomaly_delay_max = anomaly_delay_max_seconds
        self._stealth_disables = stealth_disables_injection
        self._layer1_enabled = layer1_enabled
        self._layer2_enabled = layer2_enabled
        self._layer3_enabled = layer3_enabled
        self._subagent_time_interval = subagent_time_interval_seconds
        self._subagent_periodic_enabled = subagent_periodic_enabled
        self._agent_min = agent_refresh_min
        self._agent_max = agent_refresh_max
        self._agent_time_interval = agent_time_interval_seconds
        self._agent_task_start = agent_task_start_injection

        # Session state
        self._tool_call_count: int = 0
        self._subagent_call_count: int = 0
        self._agent_call_count: int = 0
        self._next_opus_refresh_at: int = self._random_opus_interval()
        self._next_subagent_refresh_at: int = self._random_subagent_interval()
        self._next_agent_refresh_at: int = self._random_agent_interval()
        self._last_anomaly_count: int = 0
        self._pending_injections: List[Dict[str, Any]] = []
        self._injection_history: List[Dict[str, Any]] = []
        self._is_first_call: bool = True
        self._seen_agents: Set[str] = set()
        self._last_subagent_injection_ts: float = 0.0  # time-based periodic
        self._last_agent_injection_ts: float = 0.0

    def _random_opus_interval(self) -> int:
        """Generate a random interval for next Opus refresh.

        Never reuses the same interval twice in a row.
        """
        interval = random.randint(self._opus_min, self._opus_max)
        return self._tool_call_count + interval

    def _random_subagent_interval(self) -> int:
        """Generate a random interval for next subagent (L3) refresh.

        Subagents get more frequent anchoring (8-15 calls) since they
        operate with less context and are more prone to drift.
        """
        interval = random.randint(self._subagent_min, self._subagent_max)
        return self._subagent_call_count + interval

    def _random_agent_interval(self) -> int:
        """Generate a random interval for next agent (L2) periodic refresh.

        Agents get periodic re-anchoring (15-30 calls) to prevent drift
        during long-running agent tasks.
        """
        interval = random.randint(self._agent_min, self._agent_max)
        return self._agent_call_count + interval

    def evaluate(
        self,
        tool_name: str,
        tool_input: Dict[str, Any],
        anomaly_count: int = 0,
        is_subagent: bool = False,
        agent_id: str = "",
        stealth: bool = False,
        has_critical: bool = False,
    ) -> List[Dict[str, Any]]:
        """Evaluate current context and return list of triggered injections.

        Returns a list of dicts:
            [{"layers": [1,2,3], "trigger": TriggerType, "delay": float}]

        Empty list = no injection needed.
        """
        self._tool_call_count += 1

        if stealth and self._stealth_disables:
            self._is_first_call = False
            return []

        triggers: List[Dict[str, Any]] = []

        # 1. Session start: all 3 layers
        if self._is_first_call:
            self._is_first_call = False
            layers = self._enabled_layers([1, 2, 3])
            if layers:
                triggers.append({
                    "layers": layers,
                    "trigger": TriggerType.SESSION_START,
                    "delay": 0.0,
                })
            return triggers

        # 2. Agent spawn: Layer 2 — fires at the BEGINNING of each agent task
        if tool_name == "Agent" and not is_subagent:
            subagent_type = tool_input.get("subagent_type", "unknown")
            agent_key = f"{subagent_type}:{tool_input.get('description', '')[:50]}"
            if agent_key not in self._seen_agents:
                self._seen_agents.add(agent_key)
                layers = self._enabled_layers([2])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.AGENT_SPAWN,
                        "delay": 0.0,
                    })
                    self._last_agent_injection_ts = time.time()

        # 2b. Agent task start: Layer 1+2 — inject at beginning of EVERY agent task
        #     (not just unique agents — every new Agent tool call gets an anchor)
        if tool_name == "Agent" and not is_subagent and self._agent_task_start:
            # Check if this specific tool_use_id was already injected (dedup within same call)
            already_triggered = any(
                t["trigger"] == TriggerType.AGENT_SPAWN for t in triggers
            )
            if not already_triggered:
                layers = self._enabled_layers([1, 2])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.AGENT_SPAWN,
                        "delay": 0.0,
                    })
                    self._last_agent_injection_ts = time.time()

        # 3. Subagent context: Layer 3
        if is_subagent and agent_id and agent_id not in self._seen_agents:
            self._seen_agents.add(agent_id)
            layers = self._enabled_layers([3])
            if layers:
                triggers.append({
                    "layers": layers,
                    "trigger": TriggerType.SUBAGENT_SPAWN,
                    "delay": 0.0,
                })
                self._last_subagent_injection_ts = time.time()

        # 4. Critical event: Layer 1 + human pause
        if has_critical:
            layers = self._enabled_layers([1])
            if layers:
                triggers.append({
                    "layers": layers,
                    "trigger": TriggerType.CRITICAL_EVENT,
                    "delay": 0.0,
                })

        # 5. Anomaly threshold crossed: Layer 1+2 with random delay
        if anomaly_count > 0:
            delta = anomaly_count - self._last_anomaly_count
            if delta >= self._anomaly_delta:
                self._last_anomaly_count = anomaly_count
                delay = random.uniform(0, self._anomaly_delay_max)
                layers = self._enabled_layers([1, 2])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.ANOMALY_THRESHOLD,
                        "delay": delay,
                    })

        # 6. Periodic Opus refresh: Layer 1 only
        if self._tool_call_count >= self._next_opus_refresh_at:
            self._next_opus_refresh_at = self._random_opus_interval()
            layers = self._enabled_layers([1])
            if layers:
                triggers.append({
                    "layers": layers,
                    "trigger": TriggerType.OPUS_PERIODIC,
                    "delay": 0.0,
                })

        # 7. Periodic subagent refresh: Layer 3 only
        #    Disabled by default — subagents get anchored on spawn + anomaly only.
        #    Enable via subagent_periodic_enabled=True for extra safety at token cost.
        #    When enabled: dual trigger (call-count 8-15 + time-based 4 min).
        if is_subagent and self._subagent_periodic_enabled:
            self._subagent_call_count += 1
            now = time.time()
            fired_periodic = False

            # 7a. Call-count-based periodic
            if self._subagent_call_count >= self._next_subagent_refresh_at:
                self._next_subagent_refresh_at = self._random_subagent_interval()
                layers = self._enabled_layers([3])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.SUBAGENT_PERIODIC,
                        "delay": 0.0,
                    })
                    self._last_subagent_injection_ts = now
                    fired_periodic = True

            # 7b. Time-based periodic (every N seconds, default 240s = 4min)
            if (not fired_periodic
                    and self._subagent_time_interval > 0
                    and self._last_subagent_injection_ts > 0
                    and (now - self._last_subagent_injection_ts) >= self._subagent_time_interval):
                layers = self._enabled_layers([3])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.SUBAGENT_PERIODIC,
                        "delay": 0.0,
                    })
                    self._last_subagent_injection_ts = now

        # 8. Periodic agent refresh: Layer 2 only
        #    Agents get periodic re-anchoring (15-30 calls or every 3 min)
        #    to prevent drift during long-running agent tasks.
        if is_subagent or tool_name == "Agent":
            self._agent_call_count += 1
            now_agent = time.time()
            fired_agent_periodic = False

            # 8a. Call-count-based periodic
            if self._agent_call_count >= self._next_agent_refresh_at:
                self._next_agent_refresh_at = self._random_agent_interval()
                layers = self._enabled_layers([2])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.OPUS_PERIODIC,
                        "delay": 0.0,
                    })
                    self._last_agent_injection_ts = now_agent
                    fired_agent_periodic = True

            # 8b. Time-based periodic (every N seconds, default 180s = 3min)
            if (not fired_agent_periodic
                    and self._agent_time_interval > 0
                    and self._last_agent_injection_ts > 0
                    and (now_agent - self._last_agent_injection_ts) >= self._agent_time_interval):
                layers = self._enabled_layers([2])
                if layers:
                    triggers.append({
                        "layers": layers,
                        "trigger": TriggerType.OPUS_PERIODIC,
                        "delay": 0.0,
                    })
                    self._last_agent_injection_ts = now_agent

        # Process pending delayed injections
        self._check_pending(triggers)

        # Record to history (dedup)
        for t in triggers:
            self._injection_history.append({
                "ts": time.time(),
                "layers": t["layers"],
                "trigger": t["trigger"].value,
                "tool_call_count": self._tool_call_count,
            })
        # Keep history bounded
        self._injection_history = self._injection_history[-10:]

        return triggers

    def schedule_delayed(
        self, layers: List[int], trigger: TriggerType, delay: float
    ) -> None:
        """Queue a delayed injection for later firing."""
        fire_at = time.time() + delay
        self._pending_injections.append({
            "layers": layers,
            "trigger": trigger,
            "fire_at": fire_at,
        })

    def _check_pending(self, triggers: List[Dict[str, Any]]) -> None:
        """Fire any pending delayed injections whose time has come."""
        now = time.time()
        still_pending = []
        for p in self._pending_injections:
            if now >= p["fire_at"]:
                triggers.append({
                    "layers": p["layers"],
                    "trigger": p["trigger"],
                    "delay": 0.0,
                })
            else:
                still_pending.append(p)
        self._pending_injections = still_pending

    def _enabled_layers(self, requested: List[int]) -> List[int]:
        """Filter requested layers to only those enabled in config."""
        enabled_map = {
            1: self._layer1_enabled,
            2: self._layer2_enabled,
            3: self._layer3_enabled,
        }
        return [l for l in requested if enabled_map.get(l, False)]

    def tick(self) -> None:
        """Increment tool call counter without evaluating triggers.

        Use this for PostToolUse events where we want the counter to stay
        accurate for periodic cadence but don't want to consume triggers.
        """
        self._tool_call_count += 1

    @property
    def tool_call_count(self) -> int:
        return self._tool_call_count

    @property
    def next_opus_refresh_at(self) -> int:
        return self._next_opus_refresh_at

    @property
    def injection_history(self) -> List[Dict[str, Any]]:
        return list(self._injection_history)

    def get_state(self) -> Dict[str, Any]:
        """Serialize current state for dashboard/debugging."""
        return {
            "tool_call_count": self._tool_call_count,
            "subagent_call_count": self._subagent_call_count,
            "agent_call_count": self._agent_call_count,
            "next_opus_refresh_at": self._next_opus_refresh_at,
            "next_subagent_refresh_at": self._next_subagent_refresh_at,
            "next_agent_refresh_at": self._next_agent_refresh_at,
            "last_anomaly_count": self._last_anomaly_count,
            "pending_injections": len(self._pending_injections),
            "injection_count": len(self._injection_history),
            "seen_agents": len(self._seen_agents),
        }

    def save_state_to_file(self, path: str) -> None:
        """Persist session state to a JSON file for cross-process continuity.

        The hook runner spawns a new process per invocation — this preserves
        tool_call_count, seen_agents, refresh intervals, and is_first_call
        so anchors fire at the correct cadence instead of resetting each time.
        """
        import json as _json
        state = {
            "tool_call_count": self._tool_call_count,
            "subagent_call_count": self._subagent_call_count,
            "agent_call_count": self._agent_call_count,
            "next_opus_refresh_at": self._next_opus_refresh_at,
            "next_subagent_refresh_at": self._next_subagent_refresh_at,
            "next_agent_refresh_at": self._next_agent_refresh_at,
            "last_anomaly_count": self._last_anomaly_count,
            "is_first_call": self._is_first_call,
            "seen_agents": list(self._seen_agents),
            "last_subagent_injection_ts": self._last_subagent_injection_ts,
            "last_agent_injection_ts": self._last_agent_injection_ts,
        }
        try:
            with open(path, "w", encoding="utf-8") as fh:
                _json.dump(state, fh)
        except (IOError, OSError):
            pass

    def load_state_from_file(self, path: str) -> bool:
        """Restore session state from file. Returns True if state was loaded."""
        import json as _json
        try:
            with open(path, "r", encoding="utf-8") as fh:
                state = _json.load(fh)
            self._tool_call_count = state.get("tool_call_count", 0)
            self._subagent_call_count = state.get("subagent_call_count", 0)
            self._agent_call_count = state.get("agent_call_count", 0)
            self._next_opus_refresh_at = state.get("next_opus_refresh_at", self._next_opus_refresh_at)
            self._next_subagent_refresh_at = state.get("next_subagent_refresh_at", self._next_subagent_refresh_at)
            self._next_agent_refresh_at = state.get("next_agent_refresh_at", self._next_agent_refresh_at)
            self._last_anomaly_count = state.get("last_anomaly_count", 0)
            self._is_first_call = state.get("is_first_call", True)
            self._seen_agents = set(state.get("seen_agents", []))
            self._last_subagent_injection_ts = state.get("last_subagent_injection_ts", 0.0)
            self._last_agent_injection_ts = state.get("last_agent_injection_ts", 0.0)
            return True
        except (IOError, OSError, ValueError, KeyError):
            return False
