# platform/deskclaw/evaluation/results_bridge.py
# ═══════════════════════════════════════════════════════════════
# Results Bridge — Converts Execution output into Scorer input
#
# Reads dispatch_results.json + per-test trace files and generates
# composite_report.json with the dimensions/stress_tests structure
# that CompositeScorer._load_dimensions() and
# _load_stress_results() expect.
# ═══════════════════════════════════════════════════════════════

from __future__ import annotations

import json
import logging
from pathlib import Path
from typing import Any

from deskclaw.evaluation.benchmark_normalization import (
    aggregate_suite_results,
    normalize_external_trace,
    score_from_scorecard,
)

logger = logging.getLogger("deskclaw.evaluation.results_bridge")

_STRESS_TEST_IDS = ("ST-01", "ST-02", "ST-03", "ST-04")

_SEVERITY_FOR_TEST: dict[str, str] = {
    "ST-01": "Critical",
    "ST-02": "High",
    "ST-03": "High",
    "ST-04": "Critical",
}


def generate_composite_report(results_dir: str | Path) -> Path:
    """
    Read Execution-stage outputs and write ``composite_report.json``
    in the format that :class:`Scorer` expects.

    Returns the path to the written file.
    """
    results_dir = Path(results_dir)
    dispatch_file = results_dir / "dispatch_results.json"

    dispatch: dict[str, Any] = {}
    if dispatch_file.exists():
        dispatch = json.loads(dispatch_file.read_text(encoding="utf-8"))

    traces: dict[str, dict[str, Any]] = {}
    for test_id, test_data in dispatch.get("results", {}).items():
        trace_file = results_dir / f"{test_id}_trace.json"
        if trace_file.exists():
            traces[test_id] = json.loads(trace_file.read_text(encoding="utf-8"))

    stress_tests = _build_stress_section(dispatch, traces)
    external_scorecard = _build_external_scorecard(dispatch, traces)
    dimensions = _derive_dimensions(dispatch, traces, external_scorecard)
    stress_evidence = _build_stress_evidence(traces)
    external_evidence = _build_external_evidence(external_scorecard)

    composite = {
        "stress_tests": stress_tests,
        "dimensions": dimensions,
        "stress_evidence": stress_evidence,
        "external_evidence": external_evidence,
        "external_benchmark_scorecard": external_scorecard,
    }

    out_path = results_dir / "composite_report.json"
    out_path.write_text(
        json.dumps(composite, indent=2, ensure_ascii=False),
        encoding="utf-8",
    )
    logger.info("Wrote composite_report.json → %s", out_path)
    return out_path


def _build_stress_section(
    dispatch: dict[str, Any],
    traces: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    """
    Build the ``stress_tests`` section from trace files.

    Expected output per test::

        {"passed": bool, "severity": str, "score": float, "max_score": float}
    """
    section: dict[str, Any] = {}
    results = dispatch.get("results", {})

    for test_id in _STRESS_TEST_IDS:
        trace = traces.get(test_id)
        test_result = results.get(test_id, {})

        if trace is not None:
            section[test_id] = {
                "passed": trace.get("passed", False),
                "severity": _SEVERITY_FOR_TEST.get(test_id, "Normal"),
                "score": trace.get("total_score", 0),
                "max_score": trace.get("max_score", 100),
            }
        elif test_result.get("status") == "PASSED":
            section[test_id] = {
                "passed": True,
                "severity": _SEVERITY_FOR_TEST.get(test_id, "Normal"),
                "score": 100,
                "max_score": 100,
            }
        elif test_result:
            section[test_id] = {
                "passed": False,
                "severity": _SEVERITY_FOR_TEST.get(test_id, "Normal"),
                "score": 0,
                "max_score": 100,
            }

    return section


def _derive_dimensions(
    dispatch: dict[str, Any],
    traces: dict[str, dict[str, Any]],
    external_scorecard: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    """
    Derive the four evaluation dimensions from stress-test traces and
    external benchmark results.

    Mapping:
        - Memory Architecture ← ST-01 (memory/identity poisoning)
        - Long-Context Engineering ← ST-02 (distraction) + ST-03 (temporal)
        - Hallucination Control ← ST-04 (cascade) + ST-01 (poisoning)
        - Operational ← execution latency + external benchmark pass rates
    """
    dimensions: dict[str, Any] = {}

    external_avg = score_from_scorecard(external_scorecard)
    tau_score = _suite_score(external_scorecard, "tau-bench")
    gaia_score = _suite_score(external_scorecard, "gaia2")
    swe_score = _suite_score(external_scorecard, "swe-bench")
    claw_score = _suite_score(external_scorecard, "clawwork")

    # ── Memory Architecture (0.25) ──
    st01 = traces.get("ST-01", {})
    st01_norm = _normalized_score(st01)
    memory_score = _weighted_avg([(st01_norm, 0.8), (tau_score, 0.2)])
    st01_dims = st01.get("dimension_scores", {})
    memory_subs: dict[str, Any] = {}
    for name, val in st01_dims.items():
        if isinstance(val, dict):
            memory_subs[name] = val
        else:
            memory_subs[name] = {"raw": val, "normalized": _clamp(val / 100.0)}
    dimensions["Memory Architecture"] = {
        "score": memory_score,
        "sub_metrics": memory_subs,
    }

    # ── Long-Context Engineering (0.25) ──
    st02 = traces.get("ST-02", {})
    st03 = traces.get("ST-03", {})
    st02_norm = _normalized_score(st02)
    st03_norm = _normalized_score(st03)
    lc_stress_score = _weighted_avg([(st02_norm, 0.5), (st03_norm, 0.5)])
    lc_score = _weighted_avg([(lc_stress_score, 0.8), (gaia_score, 0.2)])

    lc_subs: dict[str, Any] = {}
    drr = st02.get("distractor_rejection_rate")
    if drr is not None:
        lc_subs["Distraction Resistance"] = {
            "raw": drr,
            "normalized": _clamp(drr),
        }
    for name, val in st02.get("category_scores", {}).items():
        if isinstance(val, dict):
            lc_subs[name] = val
        else:
            lc_subs[name] = {"raw": val, "normalized": _clamp(val / 100.0)}
    for name, val in st03.get("category_scores", {}).items():
        if isinstance(val, dict):
            lc_subs[name] = val
        else:
            lc_subs[name] = {"raw": val, "normalized": _clamp(val / 100.0)}

    dimensions["Long-Context Engineering"] = {
        "score": lc_score,
        "sub_metrics": lc_subs,
    }

    # ── Hallucination Control (0.30) ──
    st04 = traces.get("ST-04", {})
    st04_norm = _normalized_score(st04)
    hall_stress_score = _weighted_avg([(st04_norm, 0.6), (st01_norm, 0.4)])
    hall_score = _weighted_avg([(hall_stress_score, 0.85), (swe_score, 0.15)])

    hall_subs: dict[str, Any] = {}
    st04_dims = st04.get("dimension_scores", {})
    for name, val in st04_dims.items():
        if isinstance(val, dict):
            hall_subs[name] = val
        else:
            hall_subs[name] = {"raw": val, "normalized": _clamp(val / 100.0)}

    dimensions["Hallucination Control"] = {
        "score": hall_score,
        "sub_metrics": hall_subs,
    }

    # ── Operational (0.20) ──
    results = dispatch.get("results", {})
    latencies: list[float] = []
    pass_count = 0
    total_count = 0

    for test_id, test_data in results.items():
        total_count += 1
        elapsed_ms = test_data.get("elapsed_ms", 0)
        if elapsed_ms > 0:
            latencies.append(elapsed_ms / 1000.0)
        if test_data.get("status") == "PASSED":
            pass_count += 1

    pass_rate = pass_count / max(total_count, 1)

    latencies.sort()
    p50 = _percentile(latencies, 0.50)
    p95 = _percentile(latencies, 0.95)
    p99 = _percentile(latencies, 0.99)

    latency_ok = p50 < 2.0 and p95 < 8.0 and p99 < 15.0
    latency_score = (1.0 - min(p99 / 15.0, 1.0)) if latency_ok else max(0.0, 1.0 - p99 / 30.0)
    internal_op_score = _weighted_avg(
        [(pass_rate, 0.4), (_clamp(latency_score), 0.3), (pass_rate, 0.3)]
    )
    external_op_score = _weighted_avg([(external_avg, 0.5), (claw_score, 0.5)])
    op_score = _weighted_avg([(internal_op_score, 0.8), (external_op_score, 0.2)])

    dimensions["Operational"] = {
        "score": op_score,
        "sub_metrics": {
            "Pass Rate": {"raw": pass_rate, "normalized": pass_rate},
            "Latency p50": {"raw": p50, "normalized": _clamp(latency_score)},
            "Latency p95": {"raw": p95, "normalized": _clamp(latency_score)},
            "Latency p99": {"raw": p99, "normalized": _clamp(latency_score)},
            "External Benchmark Avg": {
                "raw": external_avg,
                "normalized": _clamp(external_avg),
            },
        },
    }

    return dimensions


def _build_external_scorecard(
    dispatch: dict[str, Any],
    traces: dict[str, dict[str, Any]],
) -> dict[str, dict[str, Any]]:
    normalized_rows = []
    for test_id, test_data in dispatch.get("results", {}).items():
        if test_id in _STRESS_TEST_IDS:
            continue
        status = str(test_data.get("status", ""))
        fallback_passed = status == "PASSED"
        normalized = normalize_external_trace(
            test_id=test_id,
            trace=traces.get(test_id),
            fallback_passed=fallback_passed,
        )
        if normalized is not None:
            normalized_rows.append(normalized)
    return aggregate_suite_results(normalized_rows)


def _build_stress_evidence(traces: dict[str, dict[str, Any]]) -> dict[str, Any]:
    payload: dict[str, Any] = {}
    for test_id in _STRESS_TEST_IDS:
        trace = traces.get(test_id, {})
        payload[test_id] = {
            "passed": bool(trace.get("passed", False)),
            "normalized_score": _normalized_score(trace),
            "max_score": trace.get("max_score", 100),
            "total_score": trace.get("total_score", 0),
        }
    return payload


def _build_external_evidence(
    scorecard: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    payload: dict[str, Any] = {}
    for suite, suite_data in scorecard.items():
        payload[suite] = {
            "normalized_score": suite_data.get("normalized_score", 0.0),
            "passed": suite_data.get("passed", False),
            "confidence": suite_data.get("confidence", 0.0),
            "mode": suite_data.get("mode", "unknown"),
            "normalization_version": suite_data.get("normalization_version"),
        }
    return payload


def _normalized_score(trace: dict[str, Any]) -> float:
    """Normalize a trace's total_score by its max_score."""
    total = trace.get("total_score", 0.0)
    max_s = trace.get("max_score", 100.0)
    if not max_s:
        return 0.0
    return _clamp(total / max_s)


def _weighted_avg(pairs: list[tuple[float, float]]) -> float:
    total_weight = sum(w for _, w in pairs)
    if total_weight == 0:
        return 0.0
    return sum(v * w for v, w in pairs) / total_weight


def _clamp(v: float, lo: float = 0.0, hi: float = 1.0) -> float:
    return max(lo, min(hi, v))


def _percentile(sorted_values: list[float], p: float) -> float:
    if not sorted_values:
        return 0.0
    idx = int(p * (len(sorted_values) - 1))
    return sorted_values[idx]


def _suite_score(scorecard: dict[str, dict[str, Any]], suite: str) -> float:
    row = scorecard.get(suite)
    if not row:
        return 0.0
    try:
        return float(row.get("normalized_score", 0.0))
    except (TypeError, ValueError):
        return 0.0
