"""Runtime hypothesis/evidence tracking and KPI helpers.

Phase 1 keeps this intentionally lightweight and non-invasive:
- Seed a small hypothesis set for each run.
- Record evidence events from tool outputs.
- Build run KPI snapshots for observability.
"""

from __future__ import annotations

import hashlib
import time
from datetime import datetime


def _clamp(value: float, low: float, high: float) -> float:
    return max(low, min(high, value))


def seed_hypotheses(goal: str, task_state: dict | None = None, max_items: int = 3) -> list[dict]:
    task = dict(task_state or {})
    task_type = str(task.get("task_type") or "generic")
    provider_scope = str(task.get("provider_scope") or "default")
    time_filter = str(task.get("time_filter") or "none")

    topic = "generic_resolution"
    if "email" in task_type:
        topic = "email_resolution"
    elif "calendar" in task_type:
        topic = "calendar_resolution"
    elif "search" in task_type:
        topic = "search_resolution"

    templates = [
        {
            "label": "primary_route",
            "confidence": 0.62,
            "assumptions": [
                "task routing is correct",
                "available tools can satisfy the request",
            ],
        },
        {
            "label": "provider_scope_adjustment",
            "confidence": 0.46,
            "assumptions": [
                "provider scope might need widening",
                "cross-source evidence may be required",
            ],
        },
        {
            "label": "time_filter_adjustment",
            "confidence": 0.42,
            "assumptions": [
                "time window may be too narrow",
                "query may need bounded widening",
            ],
        },
    ]

    seeded: list[dict] = []
    for idx, item in enumerate(templates[: max(1, max_items)]):
        seeded.append(
            {
                "hypothesis_id": f"h{idx+1}",
                "intent_cluster": topic,
                "label": item["label"],
                "confidence": float(item["confidence"]),
                "status": "active" if idx == 0 else "candidate",
                "assumptions": list(item["assumptions"]),
                "expected_signals": [
                    f"provider_scope={provider_scope}",
                    f"time_filter={time_filter}",
                ],
                "disconfirming_signals": ["tool_error", "no_relevant_records"],
                "evidence_refs": [],
                "fallback_ids": [f"h{idx+2}"] if idx + 1 < max_items else [],
            }
        )
    return seeded


def initialize_hypothesis_runtime(
    run_id: str,
    goal: str,
    task_state: dict | None = None,
    *,
    policy_mode: str = "budget",
    max_items: int = 3,
) -> dict:
    hypotheses = seed_hypotheses(goal, task_state, max_items=max_items)
    active_id = str(hypotheses[0].get("hypothesis_id") or "") if hypotheses else ""
    return {
        "version": 1,
        "run_id": str(run_id or ""),
        "created_at": datetime.now().isoformat(),
        "policy_mode": str(policy_mode or "budget"),
        "goal_preview": str(goal or "").strip()[:200],
        "active_hypothesis_id": active_id,
        "hypotheses": hypotheses,
        "evidence_events": [],
        "metrics": {
            "evidence_event_count": 0,
            "confidence_updates": 0,
        },
    }


def append_hypothesis_evidence(
    runtime: dict,
    *,
    tool_name: str,
    tool_result: str,
    step_index: int = -1,
    tool_details: dict | None = None,
) -> dict:
    active_id = str(runtime.get("active_hypothesis_id") or "")
    hypotheses = runtime.get("hypotheses") if isinstance(runtime.get("hypotheses"), list) else []
    active = None
    for item in hypotheses:
        if isinstance(item, dict) and str(item.get("hypothesis_id") or "") == active_id:
            active = item
            break
    if active is None and hypotheses:
        active = hypotheses[0]
        runtime["active_hypothesis_id"] = str(active.get("hypothesis_id") or "")

    result_text = str(tool_result or "")
    lowered = result_text.lower()
    details = dict(tool_details or {})
    error_like = lowered.startswith("error") or " error" in lowered
    empty_like = (not result_text.strip()) or ("not found" in lowered) or ("no records" in lowered)

    delta = 0.0
    outcome = "neutral"
    evidence_score = 0.5
    if error_like:
        delta = -0.12
        outcome = "error"
        evidence_score = 0.1
    elif empty_like:
        delta = -0.05
        outcome = "weak"
        evidence_score = 0.3
    else:
        delta = 0.06
        outcome = "support"
        evidence_score = 0.75

    switched_to = ""
    switched = False
    if isinstance(active, dict):
        prev = float(active.get("confidence") or 0.5)
        nxt = _clamp(prev + delta, 0.01, 0.99)
        active["confidence"] = round(nxt, 4)
        refs = active.get("evidence_refs") if isinstance(active.get("evidence_refs"), list) else []
        event_id = hashlib.sha1(f"{time.time_ns()}|{tool_name}|{step_index}".encode("utf-8")).hexdigest()[:12]
        refs.append(event_id)
        active["evidence_refs"] = refs[-50:]
    else:
        event_id = hashlib.sha1(f"{time.time_ns()}|{tool_name}|{step_index}".encode("utf-8")).hexdigest()[:12]

    if hypotheses:
        switched_to = maybe_rebalance_active_hypothesis(runtime, low_conf_threshold=0.34)
        switched = bool(switched_to)

    event = {
        "event_id": event_id,
        "ts": datetime.now().isoformat(),
        "hypothesis_id": str(runtime.get("active_hypothesis_id") or ""),
        "step_index": int(step_index),
        "tool_name": str(tool_name or ""),
        "outcome": outcome,
        "delta_confidence": round(delta, 4),
        "evidence_score": round(evidence_score, 3),
        "tool_summary": str(details.get("kind") or "")[:80],
        "switched": switched,
        "switched_to": switched_to,
    }

    events = runtime.get("evidence_events") if isinstance(runtime.get("evidence_events"), list) else []
    events.append(event)
    runtime["evidence_events"] = events[-200:]
    metrics = runtime.get("metrics") if isinstance(runtime.get("metrics"), dict) else {}
    metrics["evidence_event_count"] = int(metrics.get("evidence_event_count") or 0) + 1
    metrics["confidence_updates"] = int(metrics.get("confidence_updates") or 0) + (1 if delta != 0 else 0)
    runtime["metrics"] = metrics
    return event


def maybe_rebalance_active_hypothesis(runtime: dict, low_conf_threshold: float = 0.34) -> str:
    hypotheses = runtime.get("hypotheses") if isinstance(runtime.get("hypotheses"), list) else []
    if not hypotheses:
        return ""

    active_id = str(runtime.get("active_hypothesis_id") or "")
    active = None
    for item in hypotheses:
        if isinstance(item, dict) and str(item.get("hypothesis_id") or "") == active_id:
            active = item
            break
    if active is None:
        active = hypotheses[0] if isinstance(hypotheses[0], dict) else None
    if not isinstance(active, dict):
        return ""

    active_conf = float(active.get("confidence") or 0.0)
    ranked = [item for item in hypotheses if isinstance(item, dict)]
    ranked.sort(key=lambda x: float(x.get("confidence") or 0.0), reverse=True)
    if not ranked:
        return ""

    best = ranked[0]
    best_id = str(best.get("hypothesis_id") or "")
    best_conf = float(best.get("confidence") or 0.0)
    if best_id and best_id != str(active.get("hypothesis_id") or "") and (
        active_conf < float(low_conf_threshold) or (best_conf - active_conf) >= 0.12
    ):
        runtime["active_hypothesis_id"] = best_id
        for item in ranked:
            hid = str(item.get("hypothesis_id") or "")
            item["status"] = "active" if hid == best_id else "candidate"
        return best_id
    return ""


def summarize_hypothesis_runtime(runtime: dict) -> dict:
    hypotheses = runtime.get("hypotheses") if isinstance(runtime.get("hypotheses"), list) else []
    active_id = str(runtime.get("active_hypothesis_id") or "")
    active = None
    for item in hypotheses:
        if isinstance(item, dict) and str(item.get("hypothesis_id") or "") == active_id:
            active = item
            break
    if active is None and hypotheses and isinstance(hypotheses[0], dict):
        active = hypotheses[0]
    return {
        "active_hypothesis_id": active_id,
        "active_confidence": float((active or {}).get("confidence") or 0.0),
        "hypothesis_count": len(hypotheses),
        "evidence_event_count": int((runtime.get("metrics") or {}).get("evidence_event_count") or 0),
        "ranked_hypotheses": [
            {
                "hypothesis_id": str(item.get("hypothesis_id") or ""),
                "label": str(item.get("label") or ""),
                "confidence": float(item.get("confidence") or 0.0),
                "status": str(item.get("status") or "candidate"),
            }
            for item in sorted(
                [h for h in hypotheses if isinstance(h, dict)],
                key=lambda x: float(x.get("confidence") or 0.0),
                reverse=True,
            )[:3]
        ],
    }


def hypothesis_decision(runtime: dict) -> dict:
    summary = summarize_hypothesis_runtime(runtime)
    conf = float(summary.get("active_confidence") or 0.0)
    if conf >= 0.72:
        mode = "commit"
    elif conf >= 0.45:
        mode = "continue"
    else:
        mode = "switch_or_widen"
    return {
        "mode": mode,
        "active_hypothesis_id": str(summary.get("active_hypothesis_id") or ""),
        "active_confidence": conf,
    }


def build_run_kpis(memory: list[dict], run_budget: dict, *, completed: bool, halted: bool, halt_reason: str = "") -> dict:
    rows = [item for item in (memory or []) if isinstance(item, dict)]
    total_steps = len(rows)
    issue_steps = sum(1 for item in rows if str(item.get("verdict") or "").upper().startswith("ISSUE"))
    pass_steps = sum(1 for item in rows if str(item.get("verdict") or "").upper().startswith("PASS"))
    skip_steps = sum(1 for item in rows if str(item.get("verdict") or "").upper().startswith("SKIP"))
    tool_calls = sum(1 for item in rows if str(item.get("tool_name") or "").strip())

    started_at = float(run_budget.get("started_at") or time.monotonic())
    runtime_ms = int(max(0.0, time.monotonic() - started_at) * 1000)

    first_pass_success = bool(total_steps > 0 and issue_steps == 0 and skip_steps == 0)
    tool_call_eff = 0.0
    if tool_calls > 0:
        tool_call_eff = round(pass_steps / float(tool_calls), 4)

    correction_rate = 0.0
    if total_steps > 0:
        correction_rate = round(issue_steps / float(total_steps), 4)

    return {
        "first_pass_success": first_pass_success,
        "recovery_count": int(issue_steps),
        "time_to_solution_ms": runtime_ms,
        "tool_call_efficiency": tool_call_eff,
        "correction_rate": correction_rate,
        "completed": bool(completed),
        "halted": bool(halted),
        "halt_reason": str(halt_reason or ""),
        "totals": {
            "steps": total_steps,
            "passes": pass_steps,
            "issues": issue_steps,
            "skips": skip_steps,
            "tool_calls": tool_calls,
            "tokens": int(run_budget.get("tokens") or 0),
        },
        "budget_snapshot": {
            "max_steps": int(run_budget.get("max_steps") or 0),
            "max_tool_calls": int(run_budget.get("max_tool_calls") or 0),
            "max_total_tokens": int(run_budget.get("max_total_tokens") or 0),
            "max_runtime_seconds": int(run_budget.get("max_runtime_seconds") or 0),
            "steps_executed": int(run_budget.get("steps_executed") or 0),
            "tool_calls_runtime": int(run_budget.get("tool_calls") or 0),
        },
    }
