#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Agent Procedural Memory — Brain V8 Phase 2 (D2)

Per-agent intelligence: profiles each agent's success/failure history,
identifies weak domains, and matches tailored procedures from the brain.

Used by: boot briefing (injection), API (/v1/agents/{id}/procedures), brain_search (future).
"""
from __future__ import annotations

import json
import logging
from datetime import datetime, timezone

from pulse.core.brain_utils import (
    AGENT_REGISTRY_FILE, HIPPOCAMPUS_DIR, load_json_dict,
)

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

PROCEDURES_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Metrics" / "procedural_log.json"
MIN_TASKS_FOR_PROFILE = 3
WEAK_THRESHOLD = 0.5
STRONG_THRESHOLD = 0.7


def _normalize_domain_stats(raw) -> dict:
    """Normalize domain entry: int → {"success": N, "failure": 0}."""
    if isinstance(raw, int):
        return {"success": raw, "failure": 0}
    if isinstance(raw, dict):
        return {"success": raw.get("success", 0), "failure": raw.get("failure", 0)}
    return {"success": 0, "failure": 0}


def _load_procedures() -> list[dict]:
    """Load generic procedures from procedural_log.json."""
    if not PROCEDURES_FILE.exists():
        return []
    try:
        data = json.loads(PROCEDURES_FILE.read_text(encoding="utf-8"))
        if isinstance(data, dict):
            return data.get("procedures", [])
        return data if isinstance(data, list) else []
    except Exception:
        return []


def _is_expired(proc: dict) -> bool:
    """Check if an agent-synthesized procedure has expired."""
    expires = proc.get("expires_at", "")
    if not expires:
        return False
    try:
        exp_dt = datetime.fromisoformat(expires)
        if exp_dt.tzinfo is None:
            exp_dt = exp_dt.replace(tzinfo=timezone.utc)
        return exp_dt < datetime.now(timezone.utc)
    except (ValueError, TypeError):
        return False


def _matches_model(proc: dict, agent_model: str) -> bool:
    """Check if an agent-synthesized procedure targets this agent's model."""
    synth_for = (proc.get("synthesized_for") or "").lower()
    if not synth_for or not agent_model:
        return False
    # Exact match or model-family prefix match
    return agent_model == synth_for or agent_model.startswith(synth_for.split("-")[0])


def _resolve_agent_id(agent_id: str) -> tuple[str, dict]:
    """Find agent in registry. Tries exact match, then common variants."""
    registry = load_json_dict(AGENT_REGISTRY_FILE)
    if agent_id in registry:
        return agent_id, registry[agent_id]
    # Try with _daemon suffix, or without it
    for variant in [f"{agent_id}_daemon", agent_id.replace("_daemon", "")]:
        if variant in registry:
            return variant, registry[variant]
    # Try prefix match (e.g., "claude" matches "claude_interactive_13480_daemon")
    for key, data in registry.items():
        if key.startswith(agent_id) and data.get("type") != "daemon":
            return key, data
    return agent_id, {}


def get_agent_profile(agent_id: str) -> dict:
    """Read registry and compute per-domain success rates for an agent.

    Returns dict with: agent_id, total_tasks, overall_success_rate, domains (with rates).
    Returns empty dict if agent not found.
    """
    resolved_id, entry = _resolve_agent_id(agent_id)
    if not entry:
        return {}

    history = entry.get("history", {})
    total = history.get("tasks_completed", 0)
    succeeded = history.get("tasks_succeeded", 0)
    failed = history.get("tasks_failed", 0)

    domain_profiles = {}
    for domain, raw_stats in history.get("domains", {}).items():
        stats = _normalize_domain_stats(raw_stats)
        s, f = stats["success"], stats["failure"]
        domain_total = s + f
        rate = s / domain_total if domain_total > 0 else 0.0
        domain_profiles[domain] = {
            "success": s, "failure": f, "total": domain_total,
            "success_rate": round(rate, 3),
        }

    overall_rate = succeeded / total if total > 0 else 0.0
    return {
        "agent_id": resolved_id,
        "model": entry.get("model", "unknown"),
        "total_tasks": total,
        "tasks_succeeded": succeeded,
        "tasks_failed": failed,
        "overall_success_rate": round(overall_rate, 3),
        "domains": domain_profiles,
    }


def get_weak_domains(agent_id: str, threshold: float = WEAK_THRESHOLD) -> list[dict]:
    """Return domains where the agent's success rate is below threshold.

    Only includes domains with at least MIN_TASKS_FOR_PROFILE tasks.
    Sorted by success_rate ascending (weakest first).
    """
    profile = get_agent_profile(agent_id)
    if not profile:
        return []
    weak = []
    for domain, stats in profile.get("domains", {}).items():
        if stats["total"] >= MIN_TASKS_FOR_PROFILE and stats["success_rate"] < threshold:
            weak.append({"domain": domain, **stats})
    weak.sort(key=lambda d: d["success_rate"])
    return weak


def get_strong_domains(agent_id: str, threshold: float = STRONG_THRESHOLD) -> list[dict]:
    """Return domains where the agent excels (success rate >= threshold, min tasks met)."""
    profile = get_agent_profile(agent_id)
    if not profile:
        return []
    strong = []
    for domain, stats in profile.get("domains", {}).items():
        if stats["total"] >= MIN_TASKS_FOR_PROFILE and stats["success_rate"] >= threshold:
            strong.append({"domain": domain, **stats})
    strong.sort(key=lambda d: -d["success_rate"])
    return strong


def match_procedures(agent_id: str, max_items: int = 8) -> list[dict]:
    """Match procedures from the brain to the agent's weak domains.

    Scoring: procedure.confidence * (1.0 - agent_success_rate_for_domain).
    Ghost procedures (source=ghost_resonance) are always eligible with a base score,
    boosted 1.5x when the successor shares the ghost's model family.
    Falls back to top confidence procedures if agent has no weak domains.
    """
    profile = get_agent_profile(agent_id)
    agent_model = (profile.get("model") or agent_id or "").lower()
    procedures = _load_procedures()
    if not procedures:
        return []

    weak_domains = get_weak_domains(agent_id)
    weak_domain_rates = {d["domain"]: d["success_rate"] for d in weak_domains}

    scored = []
    for proc in procedures:
        proc_domain = proc.get("domain", "general")
        confidence = proc.get("confidence", 0.5)
        steps = proc.get("steps", [])
        if not steps or confidence < 0.4:
            continue

        # D16: Skip expired agent-synthesized procedures
        if proc.get("source") == "agent_synthesized" and _is_expired(proc):
            continue

        # D16: Agent-synthesized procedures — model-matched with 1.5x boost
        if proc.get("source") == "agent_synthesized":
            if _matches_model(proc, agent_model):
                score = confidence * 1.5
            else:
                continue  # Not for this model
        elif proc_domain in weak_domain_rates:
            # High score for procedures matching weak domains
            score = confidence * (1.0 - weak_domain_rates[proc_domain])
        elif proc.get("source") == "ghost_resonance":
            # Ghost expertise always eligible at half confidence
            score = confidence * 0.5
        elif not weak_domains and confidence >= 0.8:
            # No weak domains → recommend high-confidence general procedures
            score = confidence * 0.3
        else:
            continue

        # Model family boost for ghost procedures
        if proc.get("source") == "ghost_resonance":
            ghost_model = (proc.get("ghost_model") or "").lower()
            if agent_model and ghost_model:
                if agent_model.split("-")[0] == ghost_model.split("-")[0]:
                    score *= 1.5

        entry = {
            "name": proc.get("name", "Unknown"),
            "description": proc.get("description", ""),
            "steps": steps,
            "domain": proc_domain,
            "confidence": round(confidence, 3),
            "score": round(score, 3),
        }
        if proc.get("source") == "ghost_resonance":
            entry["ghost_agent"] = proc.get("ghost_agent", "")
        if proc.get("source") == "agent_synthesized":
            entry["synthesized_for"] = proc.get("synthesized_for", "")
        scored.append(entry)

    scored.sort(key=lambda p: -p["score"])
    return scored[:max_items]


def format_agent_briefing(agent_id: str) -> str:
    """Format a markdown briefing section for this agent's procedural memory."""
    profile = get_agent_profile(agent_id)
    if not profile or profile.get("total_tasks", 0) < MIN_TASKS_FOR_PROFILE:
        return ""

    weak = get_weak_domains(agent_id)
    procs = match_procedures(agent_id, max_items=5)
    if not weak and not procs:
        return ""

    lines = ["## YOUR WEAK SPOTS (Agent-Specific)"]
    for d in weak[:3]:
        lines.append(f"  ! {d['domain']}: {d['success_rate']:.0%} success ({d['total']} tasks)")

    if procs:
        lines.append("")
        lines.append("## RECOMMENDED PROCEDURES (Tailored For You)")
        for p in procs[:5]:
            ghost_tag = f"[GHOST: {p['ghost_agent']}] " if p.get("ghost_agent") else ""
            lines.append(f"  > {ghost_tag}{p['name']} (domain: {p['domain']}, confidence: {p['confidence']:.0%})")
            for i, step in enumerate(p.get("steps", [])[:4], 1):
                lines.append(f"    {i}. {step}")
    lines.append("")
    return "\n".join(lines)


