#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Agent capability routing and scoring for PULSE dispatcher.
Handles capability matching, agent fitness scoring, model rotation, and divergence detection.
Split from dispatcher_lifecycle.py per Law 7.

Dependencies: Python stdlib only (Law 4)
"""
import logging as _dl
import random as _random_sr
from datetime import datetime, timezone

from pulse.core.brain_utils import (
    AGENT_REGISTRY_FILE,
    load_json_dict,
)
from pulse.tasks.dispatcher_maintenance import STALE_THRESHOLD

_disp_logger = _dl.getLogger('pulse.dispatcher')
_DIVERGENCE_WARNING_THRESHOLD = 0.4


def apply_capability_routing(board: dict, verbose: bool = False) -> bool:
    """Mark tasks with eligible agents based on capabilities.
    If no agents match, falls back to tier-only matching (eligible_agents=None)."""
    registry = load_json_dict(AGENT_REGISTRY_FILE)
    modified = False

    for dispatch in board.get("dispatches", []):
        for task in dispatch.get("tasks", []):
            if task.get("status") != "open":
                continue

            req_caps = set(task.get("required_capabilities", []))
            if not req_caps:
                if "eligible_agents" in task:
                    task.pop("eligible_agents")
                    modified = True
                continue

            eligible = []
            for agent_id, agent in registry.items():
                # Only consider agents that have heartbeated recently
                heartbeat = agent.get("last_heartbeat", "")
                if not heartbeat:
                    continue
                try:
                    hb_time = datetime.fromisoformat(heartbeat)
                    if (datetime.now(timezone.utc) - hb_time).total_seconds() > STALE_THRESHOLD:
                        continue
                except Exception:
                    continue

                agent_caps = set(agent.get("capabilities", []))
                if req_caps.intersection(agent_caps):
                    eligible.append(agent_id)

            # Fallback: if no match, remove eligible_agents to allow any matching tier
            if not eligible:
                if "eligible_agents" in task:
                    task.pop("eligible_agents")
                    modified = True
            else:
                if task.get("eligible_agents") != sorted(eligible):
                    task["eligible_agents"] = sorted(eligible)
                    modified = True

    return modified


def score_agent(agent: dict, task: dict) -> float:
    """Compute fitness score for agent-task pairing."""
    score = 1.0

    # Capability overlap
    agent_caps = set(agent.get("capabilities", []))
    task_caps = set(task.get("required_capabilities", []))
    if task_caps:
        overlap = len(agent_caps & task_caps) / len(task_caps)
        score += overlap
    else:
        score += 0.5  # No requirements = any agent can do it

    # Domain history (legacy — reads dead data until registry schema is fixed)
    domain = task.get("domain", "")
    history = agent.get("history", {}).get("domains", {}).get(domain, {})
    wins = history.get("wins", 0)
    fails = history.get("fails", 0)
    score += min(wins * 0.1, 1.0)
    score -= min(fails * 0.15, 0.5)

    # D12: Hebbian model-domain fitness boost
    model = (agent.get("model") or "").strip()
    if model and domain:
        try:
            from pulse.daemons.synthesis.learning.hebbian_balancer import get_model_fitness
            hw = get_model_fitness(model, domain)
            # Scale: 0.5 (cold start) adds +0, 0.95 (proven) adds +0.45
            score += (hw - 0.5) * 1.0
        except Exception:
            pass  # D12 not available = no boost

    return round(score, 3)


def score_agents_for_task(task: dict) -> list[tuple[str, float]]:
    """Score all registered agents for a given task. Returns sorted list."""
    registry = load_json_dict(AGENT_REGISTRY_FILE)
    scores = []
    for agent_id, agent in registry.items():
        if agent.get("status") == "offline":
            continue
        s = score_agent(agent, task)
        scores.append((agent_id, s))
    scores.sort(key=lambda x: x[1], reverse=True)
    return scores


# P3-3 + Thompson Sampling: model rotation logic (imported by pulse_dispatcher)
def should_rotate(model: str = '', domain: str = '') -> bool:
    """Thompson Sampling: rotate if sampled quality < 0.3 (fails back to 10%)."""
    try:
        from pulse.daemons.synthesis.learning.hebbian_balancer import get_model_fitness
        fit = get_model_fitness(model, domain)
        if isinstance(fit, dict):
            successes = max(1, int(fit.get('successes', 1)))
            failures = max(1, int(fit.get('failures', 1)))
        else:
            successes = max(1, int(float(fit) * 10))
            failures = max(1, 10 - successes)
        if _random_sr.betavariate(successes, failures) < 0.3:
            return True
    except Exception:
        if _random_sr.random() < 0.10:
            return True
    try:
        from pulse.core.reward.volatility import get_volatility_multiplier
        vol = get_volatility_multiplier()
        if vol > 1.0 and _random_sr.random() < 0.15 * vol:
            return True
    except Exception:
        pass
    return False


def _detect_agent_divergence(outcomes: dict) -> None:
    """Scan agent outcomes for divergence (same domain, different confidence). Fail-open."""
    try:
        for key, records in outcomes.items():
            if len(records) < 4:
                continue
            agents = {}
            for r in records:
                ag = r.get('agent', '')
                if ag:
                    agents.setdefault(ag, []).append(r.get('confidence', 0.5))
            if len(agents) < 2:
                continue
            avg_per_agent = {ag: sum(c) / len(c) for ag, c in agents.items()}
            vals = list(avg_per_agent.values())
            spread = max(vals) - min(vals)
            if spread >= _DIVERGENCE_WARNING_THRESHOLD:
                best = max(avg_per_agent, key=avg_per_agent.get)
                worst = min(avg_per_agent, key=avg_per_agent.get)
                _disp_logger.warning(
                    '[Dispatcher] Agent divergence in %s: %s (%.2f) vs %s (%.2f), spread=%.2f',
                    key, best, avg_per_agent[best], worst, avg_per_agent[worst], spread)
    except Exception:
        pass
