#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Evidence Accumulation for Agent-Task Assignment (Phase 4, Architecture 6).

Drift-Diffusion Model (Ratcliff & McKoon 2008): instead of instantly
assigning tasks to agents, accumulate evidence from multiple streams
until confidence crosses a threshold.

Evidence streams:
1. Hebbian fitness (model-domain success rate)
2. Domain competence (metamemory Wilson lower bound)
3. Recent task success rate (agent registry)
4. Current workload (active claims count)
5. Capability overlap score

When any agent's accumulated evidence crosses the decision threshold,
assign the task. Urgency signal lowers threshold over time (collapsing
bound) to prevent indefinite deliberation.

Single-agent and urgent tasks bypass DDM for instant assignment.
"""
import json
import logging
from datetime import datetime, timezone
from pathlib import Path

from pulse.core.paths import get_state_dir
import random

log = logging.getLogger("pulse.evidence_accumulator")

DECISION_THRESHOLD = 0.75  # Evidence sum needed to commit to assignment
URGENCY_DECAY = 0.05       # Threshold drops by this per second
MIN_THRESHOLD = 0.5        # Floor — always need some evidence
MAX_DELIBERATION_S = 30    # Maximum seconds before forced decision


def score_agent_for_task(agent_id: str, task: dict) -> dict:
    """Compute multi-stream evidence score for an agent-task pair.

    Returns dict with per-stream scores and total evidence.
    """
    domain = task.get("domain", "general")
    required_caps = set(task.get("required_capabilities", []))
    scores = {}

    # Stream 1: Hebbian fitness (model-domain success rate)
    scores["hebbian"] = _get_hebbian_fitness(agent_id, domain)

    # Stream 2: Domain competence (metamemory)
    scores["competence"] = _get_domain_competence(domain)

    # Stream 3: Recent success rate from agent registry
    scores["success_rate"] = _get_agent_success_rate(agent_id)

    # Stream 4: Workload (fewer active tasks = higher score)
    scores["availability"] = _get_availability_score(agent_id)

    # Stream 5: Capability overlap
    scores["capability"] = _get_capability_overlap(agent_id, required_caps)
    # H19-20: DDM stochastic — add Gaussian noise to each stream
    try:
        for _k in ("hebbian", "competence", "success_rate", "availability", "capability"):
            scores[_k] = min(1.0, max(0.0, scores[_k] + random.gauss(0, 0.05)))
    except Exception:
        pass


    # Total evidence = weighted sum
    total = (
        0.30 * scores["hebbian"] +
        0.20 * scores["competence"] +
        0.20 * scores["success_rate"] +
        0.15 * scores["availability"] +
        0.15 * scores["capability"]
    )
    scores["total"] = round(total, 4)
    return scores


def select_best_agent(candidates: list[str], task: dict) -> tuple[str, dict]:
    """Run DDM: accumulate evidence for each candidate, return best.

    For single-candidate or urgent tasks, returns immediately.
    Otherwise, scores all candidates and returns the highest scorer.

    Returns (agent_id, evidence_dict).
    """
    if not candidates:
        return "", {}

    # Single candidate: instant assignment
    if len(candidates) == 1:
        scores = score_agent_for_task(candidates[0], task)
        return candidates[0], scores

    # Urgent tasks: instant best-score assignment
    if task.get("priority", "normal") == "urgent":
        return _instant_best(candidates, task)

    # DDM: score all candidates
    all_scores = {}
    for agent_id in candidates:
        all_scores[agent_id] = score_agent_for_task(agent_id, task)

    # Check if any crosses threshold
    best_agent = max(all_scores, key=lambda a: all_scores[a]["total"])
    best_score = all_scores[best_agent]["total"]

    # With collapsing bound: if best score > threshold, commit
    # In practice (synchronous scoring), we just pick the highest scorer
    # The threshold acts as a quality gate
    if best_score >= DECISION_THRESHOLD:
        log.info("DDM: %s selected with evidence %.3f (threshold=%.1f)",
                 best_agent, best_score, DECISION_THRESHOLD)
    else:
        log.info("DDM: %s selected (best available, evidence=%.3f < threshold=%.1f)",
                 best_agent, best_score, DECISION_THRESHOLD)

    return best_agent, all_scores[best_agent]


def _instant_best(candidates: list[str], task: dict) -> tuple[str, dict]:
    """Fast path: score all, pick best, no threshold check."""
    best, best_scores = "", {}
    best_total = -1.0
    for agent_id in candidates:
        scores = score_agent_for_task(agent_id, task)
        if scores["total"] > best_total:
            best, best_scores, best_total = agent_id, scores, scores["total"]
    return best, best_scores


def _get_hebbian_fitness(agent_id: str, domain: str) -> float:
    """Get Hebbian model-domain fitness score [0, 1]."""
    try:
        hebb_file = get_state_dir() / "hebbian_matrix.json"
        if not hebb_file.exists():
            return 0.5
        data = json.loads(hebb_file.read_text(encoding="utf-8"))
        agent_data = data.get("matrix", {}).get(agent_id, {})
        domain_data = agent_data.get(domain, {})
        successes = domain_data.get("successes", 0)
        failures_count = domain_data.get("failures", 0)
        attempts = successes + failures_count
        if attempts < 3:
            return 0.5  # Insufficient data
        return round(successes / attempts, 3)
    except Exception:
        return 0.5


def _get_domain_competence(domain: str) -> float:
    """Get brain's competence in this domain [0, 1]."""
    try:
        from pulse.brain.metacognition.metamemory import get_metamemory
        meta = get_metamemory()
        if domain in meta:
            return meta[domain].get("wilson_lower", 0.5)
    except Exception:
        pass
    return 0.5


def _get_agent_success_rate(agent_id: str) -> float:
    """Get recent task success rate from agent registry [0, 1]."""
    try:
        reg_file = get_state_dir() / "agent_registry.json"
        if not reg_file.exists():
            return 0.5
        data = json.loads(reg_file.read_text(encoding="utf-8"))
        agent = data.get(agent_id, {})
        completed = agent.get("history", {}).get("tasks_completed", 0)
        if completed < 3:
            return 0.5
        # Estimate from domain success/failure (registry schema uses these keys)
        domains = agent.get("history", {}).get("domains", {})
        total_wins = sum(d.get("success", 0) for d in domains.values() if isinstance(d, dict))
        total_fails = sum(d.get("failure", 0) for d in domains.values() if isinstance(d, dict))
        total = total_wins + total_fails
        return round(total_wins / total, 3) if total > 0 else 0.5
    except Exception:
        return 0.5


def _get_capability_overlap(agent_id: str, required: set) -> float:
    """Score based on capability match [0, 1]."""
    if not required:
        return 1.0  # No requirements: any agent can do it
    try:
        reg_file = get_state_dir() / "agent_registry.json"
        if not reg_file.exists():
            return 0.5
        data = json.loads(reg_file.read_text(encoding="utf-8"))
        agent = data.get(agent_id, {})
        caps = set(agent.get("capabilities", []))
        if not caps:
            return 0.5
        overlap = len(caps & required)
        return round(overlap / len(required), 3)
    except Exception:
        return 0.5


def _get_availability_score(agent_id: str) -> float:
    """Score based on current workload [0, 1]. Idle = 1.0, busy = 0.2."""
    try:
        reg_file = get_state_dir() / "agent_registry.json"
        if not reg_file.exists():
            return 0.8
        data = json.loads(reg_file.read_text(encoding="utf-8"))
        agent = data.get(agent_id, {})
        status = agent.get("status", "idle")
        if status == "idle":
            return 1.0
        elif status == "active":
            return 0.3
        return 0.5
    except Exception:
        return 0.8
