"""Idle-based evolution trigger.

A single background thread periodically scans live agent sessions and runs an
evolution pass for any session that is idle for >= idle_minutes AND has enough
accumulated signal, where "enough signal" is EITHER:
  - >= min_turns user turns since the last evolution, OR
  - the live context has grown past _CONTEXT_RATIO of the agent's token budget
    (mirrors how OpenClacky / Claude Code consolidate under context pressure).

Turn counting is per user turn (not per message), measured from the last
evolution (or session start). After a pass runs, the baseline resets so a long
session can evolve multiple times without re-judging old content.

Per-session evolution state is stored on the agent instance via lightweight
attributes set by AgentBridge.agent_reply (see _note_user_turn).
"""

from __future__ import annotations

import threading
import time

from common.log import logger

from agent.evolution.config import get_evolution_config
from agent.evolution.executor import run_evolution_for_session

_SCAN_INTERVAL_SECONDS = 60

# Context-pressure trigger: evolve once the live context exceeds this fraction
# of the agent's token budget, even if min_turns hasn't been reached. Kept as a
# module constant (not user config) for now. Fallback budget matches
# agent_initializer / config.py (agent_max_context_tokens default = 50000).
_CONTEXT_RATIO = 0.8
_FALLBACK_CONTEXT_BUDGET = 50000


def _context_pressure_reached(agent) -> bool:
    """True if the agent's live context exceeds _CONTEXT_RATIO of its budget.

    Uses the agent's own (estimated) token accounting so behavior matches the
    existing context-trimming path. Best-effort: any error -> False.
    """
    try:
        with agent.messages_lock:
            messages = list(agent.messages)
        if not messages:
            return False
        est = sum(agent._estimate_message_tokens(m) for m in messages)
        budget = getattr(agent, "max_context_tokens", None) or _FALLBACK_CONTEXT_BUDGET
        return est / budget > _CONTEXT_RATIO
    except Exception:
        return False


def note_user_turn(agent, channel_type: str = "", receiver: str = "") -> None:
    """Record activity for a session's agent. Called once per real user turn.

    Maintains, on the agent instance:
      _evo_last_active   : epoch seconds of the last user turn
      _evo_turns         : user turns since the last evolution
      _evo_channel_type  : originating channel (for later notify)
      _evo_receiver      : push target for notify
    """
    try:
        agent._evo_last_active = time.time()
        agent._evo_turns = int(getattr(agent, "_evo_turns", 0)) + 1
        if channel_type:
            agent._evo_channel_type = channel_type
        if receiver:
            agent._evo_receiver = receiver
    except Exception:
        pass


def mark_run_active(agent, active: bool) -> None:
    """Flag whether the agent is mid-run, so idle scans skip a busy session.

    Without this, a single run that lasts longer than idle_minutes would let
    the scanner fire an evolution pass concurrently with the live turn.
    """
    try:
        agent._evo_run_active = bool(active)
        if active:
            agent._evo_last_active = time.time()
    except Exception:
        pass


def start_evolution_trigger(agent_bridge) -> None:
    """Start the idle-scan thread once per process (idempotent)."""
    if getattr(agent_bridge, "_evolution_trigger_started", False):
        return
    agent_bridge._evolution_trigger_started = True

    t = threading.Thread(
        target=_scan_loop, args=(agent_bridge,), daemon=True, name="evolution-trigger"
    )
    t.start()
    logger.info("[Evolution] Idle trigger started")


def _scan_loop(agent_bridge) -> None:
    while True:
        try:
            time.sleep(_SCAN_INTERVAL_SECONDS)
            cfg = get_evolution_config()
            if not cfg.enabled:
                continue
            _scan_once(agent_bridge, cfg)
        except Exception as e:
            logger.warning(f"[Evolution] Scan loop error: {e}")
            time.sleep(_SCAN_INTERVAL_SECONDS)


def _scan_once(agent_bridge, cfg) -> None:
    now = time.time()
    # Snapshot to avoid holding the dict while running long evolutions.
    sessions = list(getattr(agent_bridge, "agents", {}).items())
    for session_id, agent in sessions:
        try:
            # Skip sessions whose agent is mid-run: a long turn must not be
            # reviewed while it is still producing the answer.
            if getattr(agent, "_evo_run_active", False):
                continue
            last_active = getattr(agent, "_evo_last_active", 0)
            turns = int(getattr(agent, "_evo_turns", 0))
            # Enough signal = enough turns OR enough context pressure.
            enough_signal = turns >= cfg.min_turns or _context_pressure_reached(agent)
            if not enough_signal:
                continue
            idle = now - last_active if last_active > 0 else -1
            if last_active <= 0 or idle < cfg.idle_seconds:
                continue

            channel_type = getattr(agent, "_evo_channel_type", "") or ""
            receiver = getattr(agent, "_evo_receiver", "") or ""

            # Reset baseline BEFORE running so a long pass / new messages during
            # it don't double-trigger; turns accrue fresh from here.
            agent._evo_turns = 0

            run_evolution_for_session(
                agent_bridge,
                session_id=session_id,
                channel_type=channel_type,
                receiver=receiver,
                idle_minutes=(now - last_active) / 60 if last_active > 0 else 0.0,
            )
        except Exception as e:
            logger.warning(f"[Evolution] Failed to evaluate session={session_id}: {e}")
