#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""Prediction Feedback: Outcome recording, metrics, confidence decay."""

import sys
from datetime import datetime, timezone
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent

from pulse.core.brain_utils import load_json_dict, atomic_update_json, now_iso as _now_iso
from pulse.core.reward.reward_signal import REWARD_SIGNALS_FILE

STATE_DIR = ROOT_DIR / "state"
ALERTS_FILE = STATE_DIR / "prediction_alerts.json"
METRICS_FILE = STATE_DIR / "prediction_metrics.json"

DECAY_AMOUNT = 0.05
EXPIRE_THRESHOLD = 0.15


def _update_metrics(outcome_type: str, confidence: float):
    """Update prediction_metrics.json with outcome classification."""
    def _updater(data):
        if not isinstance(data, dict):
            data = {"total_predictions": 0, "outcomes": {}, "accuracy_by_confidence": {}, "last_updated": ""}
        data["total_predictions"] = data.get("total_predictions", 0) + 1
        outcomes = data.setdefault("outcomes", {})
        outcomes[outcome_type] = outcomes.get(outcome_type, 0) + 1
        if confidence < 0.5:
            bucket = "0.3-0.5"
        elif confidence < 0.7:
            bucket = "0.5-0.7"
        else:
            bucket = "0.7-1.0"
        by_conf = data.setdefault("accuracy_by_confidence", {})
        bucket_data = by_conf.setdefault(bucket, {"correct": 0, "total": 0})
        bucket_data["total"] = bucket_data.get("total", 0) + 1
        if outcome_type in ("true_positive", "true_negative"):
            bucket_data["correct"] = bucket_data.get("correct", 0) + 1
        data["last_updated"] = _now_iso()
        return data
    METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
    atomic_update_json(METRICS_FILE, _updater, default={})


def _alert_matches(alert: dict, task_id: str, task_title: str = "") -> bool:
    """Check if a prediction alert matches a task (exact → prefix → title)."""
    # 1. Exact ID match
    if alert.get("task_id") == task_id:
        return True
    # 2. ID prefix match (e.g. oracle IDs with different suffixes)
    alert_id = alert.get("task_id", "")
    if alert_id and task_id and (alert_id.startswith(task_id) or task_id.startswith(alert_id)):
        return True
    # 3. Title-based matching (need a title to compare)
    if not task_title:
        return False
    title_lower = task_title.lower()
    if len(title_lower) < 5:
        return False
    # 3a. Check evidence_chain — oracle stores analyzed task titles here
    for evidence in alert.get("evidence_chain", []):
        if isinstance(evidence, str) and evidence.lower() in title_lower:
            return True
        if isinstance(evidence, str) and title_lower in evidence.lower():
            return True
    # 3b. Check prediction text or alert task_title
    pred_text = alert.get("prediction", "").lower()
    alert_title = alert.get("task_title", "").lower()
    if pred_text and title_lower in pred_text:
        return True
    if alert_title and title_lower in alert_title:
        return True
    return False


def _resolve_task_title(task_id: str) -> str:
    """Look up a task's title from the task board by its ID."""
    try:
        board = load_json_dict(STATE_DIR / "task_board.json")
        for d in board.get("dispatches", []):
            for t in d.get("tasks", []):
                tid = t.get("task_id") or t.get("id", "")
                if tid == task_id:
                    return t.get("title", "")
    except Exception:
        pass
    return ""


def record_prediction_outcome(task_id: str, success: bool, agent_name: str = "unknown",
                              task_title: str = ""):
    """Record whether a prediction was accurate (feedback loop).

    Called by pulse_save.py after task completion:
    - prediction existed AND task failed -> true_positive (prediction correct)
    - prediction existed AND task succeeded -> false_positive (wrong, bump down)
    - no prediction AND task failed -> missed (opportunity to learn)
    - no prediction AND task succeeded -> true_negative (correctly no warning)
    """
    # Resolve title for fuzzy matching if not provided
    if not task_title:
        task_title = _resolve_task_title(task_id)

    updated = [False]
    alert_confidence = [0.5]

    def _updater(data):
        if not isinstance(data, dict):
            return data
        for alert in data.get("alerts", []):
            if _alert_matches(alert, task_id, task_title) and alert.get("status") == "active":
                alert_confidence[0] = alert.get("confidence", 0.5)
                if success:
                    alert["status"] = "resolved_success"
                    alert["outcome"] = "false_positive"
                    alert["resolved_by"] = agent_name
                    alert["resolved_at"] = _now_iso()
                    alert["confidence"] = max(0.1, alert.get("confidence", 0.5) - 0.15)
                else:
                    alert["status"] = "confirmed_failure"
                    alert["outcome"] = "true_positive"
                    alert["resolved_by"] = agent_name
                    alert["resolved_at"] = _now_iso()
                    alert["confidence"] = min(1.0, alert.get("confidence", 0.5) + 0.15)
                try:  # P10-32: Counterfactual regret — boost if correct but under-confident
                    if not success and float(alert.get("confidence", 0)) < 0.65:
                        regret = 1.0 - float(alert.get("confidence", 0))
                        alert["confidence"] = min(0.9, alert.get("confidence", 0.5) + regret * 0.3)
                        alert["_counterfactual_regret"] = True
                except Exception:
                    pass
                updated[0] = True
                break
        return data

    ALERTS_FILE.parent.mkdir(parents=True, exist_ok=True)
    atomic_update_json(ALERTS_FILE, _updater, default={"alerts": [], "total_generated": 0})

    # Item 169: Per-domain prediction calibration
    try:
        from pulse.brain.metacognition.prediction_calibration import record_domain_prediction
        _alerts_snap = load_json_dict(ALERTS_FILE)
        for _alert in _alerts_snap.get("alerts", []):
            if _alert_matches(_alert, task_id, task_title) and _alert.get("outcome") in ("true_positive", "false_positive"):
                _prediction_correct = not success  # true_positive = predicted failure = correct when task failed
                record_domain_prediction(_alert.get("domain", "general"), float(_alert.get("confidence", 0.5)), _prediction_correct)
                break
    except Exception: pass

    # Item 106: Prediction accuracy feedback to oracle (Metacognition)
    try:
        _alerts_data = load_json_dict(ALERTS_FILE)
        predictions = _alerts_data.get("alerts", [])
        total = len(predictions)
        correct = sum(1 for p in predictions if p.get("outcome") in ("true_positive", "true_negative"))
        accuracy = correct / max(total, 1)
        from pulse.brain.metacognition.metamemory import _record_source_outcome
        _record_source_outcome("oracle", accuracy > 0.5)
    except Exception:
        pass

    if updated[0]:
        outcome = "false_positive" if success else "true_positive"
        _update_metrics(outcome, alert_confidence[0])
        # U4: Reward signal — unified through reward_signal.py (Phase 2 AGI refinement)
        if success:
            try:
                from pulse.core.reward.reward_signal import record_task_outcome
                record_task_outcome(
                    task_id=task_id, agent=agent_name, success=True,
                    domain="prediction", retrieved_item_ids=[],
                )
            except ImportError:
                _record_reward(task_id, alert_confidence[0], agent_name)
    else:
        outcome = "true_negative" if success else "missed"
        _update_metrics(outcome, 0.0)

    return updated[0]



def decay_stale_predictions():
    """Decay confidence of predictions older than 24h, at most once per 24h window.

    Uses per-alert `last_decayed_at` to ensure each alert loses exactly DECAY_AMOUNT
    per 24h regardless of how often this function is called.
    """
    now = datetime.now(timezone.utc)
    decayed = [0]
    archived = [0]

    def _updater(data):
        if not isinstance(data, dict):
            return data
        for alert in data.get("alerts", []):
            if alert.get("status") != "active":
                continue
            try:
                created = datetime.fromisoformat(alert["timestamp"].replace("Z", "+00:00"))
                age_hours = (now - created).total_seconds() / 3600
            except (KeyError, ValueError):
                continue
            if age_hours < 24:
                continue
            # Only decay once per 24h window — check last_decayed_at
            last_decayed_at = alert.get("last_decayed_at")
            if last_decayed_at:
                try:
                    last_decay_dt = datetime.fromisoformat(last_decayed_at.replace("Z", "+00:00"))
                    if (now - last_decay_dt).total_seconds() < 86400:
                        continue
                except (ValueError, TypeError):
                    pass
            alert["confidence"] = round(max(0.0, alert.get("confidence", 0.5) - DECAY_AMOUNT), 2)
            alert["last_decayed_at"] = now.isoformat()
            decayed[0] += 1
            if alert["confidence"] < EXPIRE_THRESHOLD:
                alert["status"] = "expired"
                alert["outcome"] = "auto_expired_stale"
                alert["resolved_at"] = _now_iso()
                archived[0] += 1
        return data

    ALERTS_FILE.parent.mkdir(parents=True, exist_ok=True)
    atomic_update_json(ALERTS_FILE, _updater, default={"alerts": [], "total_generated": 0})
    return decayed[0], archived[0]


def get_prediction_accuracy() -> dict:
    """Get prediction accuracy summary for boot briefing."""
    data = load_json_dict(METRICS_FILE)
    if not data or not data.get("total_predictions"):
        return {"accuracy": 0.0, "total": 0, "correct": 0}
    outcomes = data.get("outcomes", {})
    correct = outcomes.get("true_positive", 0) + outcomes.get("true_negative", 0)
    total = data.get("total_predictions", 0)
    return {
        "accuracy": round(correct / total, 2) if total > 0 else 0.0,
        "total": total,
        "correct": correct,
    }


def get_domain_accuracy() -> dict:
    """Get prediction accuracy broken down by domain.

    Returns {"domains": {"brain": {"accuracy": 0.8, "total": 5, "correct": 4}, ...},
             "by_confidence": {"0.3-0.5": {...}, ...}}
    """
    data = load_json_dict(METRICS_FILE)
    if not data:
        return {"domains": {}, "by_confidence": {}}
    # Per-domain stats from prediction alerts
    alerts = load_json_dict(ALERTS_FILE)
    domain_stats: dict[str, dict] = {}
    for alert in alerts.get("alerts", []):
        outcome = alert.get("outcome", "")
        if not outcome:
            continue
        # Extract domain from risk_components or default to general
        components = alert.get("risk_components", [])
        domain = "general"
        for comp in components:
            if "/" in comp:
                parts = comp.split("/")
                for p in parts:
                    if p in ("brain", "api", "cli", "web", "daemons", "core", "admin", "graph", "workers"):
                        domain = p
                        break
            if domain != "general":
                break
        d = domain_stats.setdefault(domain, {"correct": 0, "total": 0})
        d["total"] += 1
        if outcome in ("true_positive", "true_negative"):
            d["correct"] += 1
    for d in domain_stats.values():
        d["accuracy"] = round(d["correct"] / d["total"], 2) if d["total"] > 0 else 0.0
    return {
        "domains": domain_stats,
        "by_confidence": data.get("accuracy_by_confidence", {}),
    }


def get_active_predictions(task_id: str = "", task_title: str = "") -> list:
    """Get active predictions, optionally filtered by task_id (with fuzzy matching)."""
    alerts_data = load_json_dict(ALERTS_FILE)
    active = [a for a in alerts_data.get("alerts", []) if a.get("status") == "active"]
    if task_id:
        # Resolve title for fuzzy matching if not provided
        resolved_title = task_title or _resolve_task_title(task_id)
        active = [a for a in active if _alert_matches(a, task_id, resolved_title)]
    return active
