# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Dimension B: Source Credibility / Agent Trust.

Computes trust scores for agents based on their track record:
  accuracy_rate, prediction_accuracy, contradiction_rate, longevity.

Public: compute_trust, recompute_all, get_trust
Storage: state/agent_trust.json
"""
import json
from datetime import datetime, timezone
from pathlib import Path

from pulse.core.brain_utils import (
    STATE_DIR, LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL,
    read_jsonl_entries, now_iso,
)
from pulse.core.brain_io import _acquire

TRUST_FILE = STATE_DIR / "agent_trust.json"

# --- Tier keywords for bootstrap defaults ---
_TIER_MAP = [
    (("claude", "opus"), 0.7, "premium"),
    (("gemini", "sonnet"), 0.6, "standard"),
    (("worker", "haiku"), 0.5, "headless"),
]


# === Private helpers ===

def _load_trust() -> dict:
    """Load state/agent_trust.json."""
    if not TRUST_FILE.exists():
        return {"agents": {}, "last_recomputed": ""}
    try:
        data = json.loads(TRUST_FILE.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {"agents": {}, "last_recomputed": ""}
    except (json.JSONDecodeError, OSError):
        return {"agents": {}, "last_recomputed": ""}


def _save_trust(data: dict):
    """Save with filelock."""
    TRUST_FILE.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(TRUST_FILE):
        TRUST_FILE.write_text(
            json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
        )


def _bootstrap_trust(agent_name: str) -> tuple[float, str]:
    """Return (bootstrap_score, tier) based on agent name keywords."""
    name_lower = agent_name.lower()
    for keywords, score, tier in _TIER_MAP:
        if any(kw in name_lower for kw in keywords):
            return score, tier
    return 0.3, "unknown"


def _compute_accuracy(agent_name: str) -> tuple[int, int, float]:
    """Count JSONL entries by source. Returns (contributions, survived, accuracy_rate).

    'Survived' means consolidation_count > 1 (was merged/reinforced, not pruned).
    """
    contributions = 0
    survived = 0
    name_lower = agent_name.lower()
    for jsonl_path in (LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL):
        try:
            entries = read_jsonl_entries(jsonl_path)
        except Exception:
            continue
        for e in entries:
            src = (e.get("agent", "") or e.get("source", "")).lower()
            if name_lower in src or src in name_lower:
                contributions += 1
                if e.get("consolidation_count", 1) > 1:
                    survived += 1
    rate = survived / contributions if contributions > 0 else 0.0
    return contributions, survived, round(rate, 4)


def _compute_prediction_accuracy(agent_name: str) -> tuple[int, int, float]:
    """Read prediction metrics. Returns (correct, total, accuracy).

    Falls back to global accuracy if per-agent data unavailable.
    """
    try:
        from pulse.daemons.neural.prediction_feedback import get_prediction_accuracy
        data = get_prediction_accuracy()
        total = data.get("total", 0)
        correct = data.get("correct", 0)
        accuracy = data.get("accuracy", 0.0)
        return correct, total, accuracy
    except Exception:
        return 0, 0, 0.0


def _compute_contradiction_rate(agent_name: str) -> tuple[int, float]:
    """Read contradiction_registry.json, count agent involvement.

    Returns (contradictions_involved, inverse_rate).
    Inverse rate: 1.0 - (contradictions / max(contributions, 1)), clamped [0, 1].
    """
    try:
        reg_file = STATE_DIR / "contradiction_registry.json"
        if not reg_file.exists():
            return 0, 1.0
        data = json.loads(reg_file.read_text(encoding="utf-8"))
        name_lower = agent_name.lower()
        involved = 0
        for ctr in data.get("contradictions", []):
            for side in ("item_a", "item_b"):
                item = ctr.get(side, {})
                src = (item.get("source", "") or item.get("file", "")).lower()
                if name_lower in src:
                    involved += 1
                    break
        return involved, 1.0  # Inverse computed in compute_trust with contributions
    except Exception:
        return 0, 1.0


def _compute_longevity(agent_name: str, first_seen: str) -> float:
    """Days since first_seen / 30, capped at 1.0."""
    if not first_seen:
        return 0.0
    try:
        dt = datetime.fromisoformat(first_seen.replace("Z", "+00:00"))
        days = (datetime.now(timezone.utc) - dt).total_seconds() / 86400
        return min(1.0, round(days / 30, 4))
    except (ValueError, TypeError):
        return 0.0


def _find_first_seen(agent_name: str) -> str:
    """Scan JSONL entries to find earliest timestamp for agent."""
    earliest = ""
    name_lower = agent_name.lower()
    for jsonl_path in (LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL):
        try:
            for e in read_jsonl_entries(jsonl_path):
                src = (e.get("agent", "") or e.get("source", "")).lower()
                if name_lower in src or src in name_lower:
                    ts = e.get("timestamp", "")
                    if ts and (not earliest or ts < earliest):
                        earliest = ts
        except Exception:
            continue
    return earliest


def _collect_known_agents() -> set[str]:
    """Scan all JSONL brain files for unique agent names."""
    agents = set()
    for jsonl_path in (LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL):
        try:
            for e in read_jsonl_entries(jsonl_path):
                src = e.get("agent", "") or e.get("source", "")
                if src and src != "unknown":
                    agents.add(src)
        except Exception:
            continue
    return agents


# === Public API ===

def compute_trust(agent_name: str) -> dict:
    """Compute trust score for a single agent.

    Formula: trust = 0.4*accuracy + 0.3*prediction + 0.2*contradiction_inv + 0.1*longevity
    Returns full agent record dict.
    """
    contributions, survived, accuracy_rate = _compute_accuracy(agent_name)

    # Bootstrap: if < 5 contributions, return bootstrap trust
    if contributions < 5:
        bootstrap_score, tier = _bootstrap_trust(agent_name)
        return {
            "trust_score": bootstrap_score,
            "contributions": contributions,
            "survived_consolidation": survived,
            "contradictions_involved": 0,
            "predictions_correct": 0,
            "predictions_total": 0,
            "first_seen": "",
            "last_seen": now_iso(),
            "tier": tier,
            "accuracy_rate": accuracy_rate,
            "last_computed": now_iso(),
        }

    pred_correct, pred_total, pred_accuracy = _compute_prediction_accuracy(agent_name)
    contradictions, _ = _compute_contradiction_rate(agent_name)
    contradiction_inv = max(0.0, 1.0 - (contradictions / max(contributions, 1)))

    # Look up or compute first_seen
    existing = _load_trust()
    agent_data = existing.get("agents", {}).get(agent_name, {})
    first_seen = agent_data.get("first_seen", "") or _find_first_seen(agent_name)
    longevity = _compute_longevity(agent_name, first_seen)

    # Weighted trust formula
    trust = (0.4 * accuracy_rate
             + 0.3 * pred_accuracy
             + 0.2 * contradiction_inv
             + 0.1 * longevity)
    trust = round(min(1.0, max(0.0, trust)), 4)

    # Determine tier from bootstrap logic
    _, tier = _bootstrap_trust(agent_name)

    return {
        "trust_score": trust,
        "contributions": contributions,
        "survived_consolidation": survived,
        "contradictions_involved": contradictions,
        "predictions_correct": pred_correct,
        "predictions_total": pred_total,
        "first_seen": first_seen,
        "last_seen": now_iso(),
        "tier": tier,
        "accuracy_rate": accuracy_rate,
        "last_computed": now_iso(),
    }


def recompute_all(verbose: bool = False) -> dict:
    """Recompute trust for all known agents, save to state file."""
    agents = _collect_known_agents()
    data = _load_trust()
    agents_dict = data.get("agents", {})

    for agent_name in agents:
        record = compute_trust(agent_name)
        agents_dict[agent_name] = record
        if verbose:
            print(f"  [TRUST] {agent_name}: {record['trust_score']:.3f} "
                  f"({record['contributions']} items, tier={record['tier']})")

    data["agents"] = agents_dict
    data["last_recomputed"] = now_iso()
    _save_trust(data)
    return data


def get_trust(agent_name: str) -> float:
    """Get cached trust score. Bootstrap default for unknown agents."""
    data = _load_trust()
    agent_data = data.get("agents", {}).get(agent_name, {})
    if agent_data and agent_data.get("trust_score") is not None:
        return agent_data["trust_score"]
    # Try partial match (e.g., "claude" in "pulse_claude")
    name_lower = agent_name.lower()
    for key, val in data.get("agents", {}).items():
        if name_lower in key.lower() or key.lower() in name_lower:
            score = val.get("trust_score")
            if score is not None:
                return score
    # Bootstrap default
    bootstrap_score, _ = _bootstrap_trust(agent_name)
    return bootstrap_score
