"""Evaluation runner implementation."""

from __future__ import annotations

import asyncio

from agentbyte.cancellation_token import CancellationToken
from agentbyte.messages import Usage

from agentbyte.eval.base import BaseEvalJudge, BaseEvalRunner, BaseEvalTarget
from agentbyte.eval.types import EvalScore, EvalTask, EvalTrajectory


class EvalRunner(BaseEvalRunner):
    """Evaluate one target over many tasks using one judge."""

    def __init__(self, judge: BaseEvalJudge, parallel: bool = True) -> None:
        super().__init__(judge)
        self.parallel = parallel

    async def evaluate(
        self,
        target: BaseEvalTarget,
        tasks: list[EvalTask],
        criteria: list[str] | None = None,
        cancellation_token: CancellationToken | None = None,
    ) -> list[EvalScore]:
        if self.parallel:
            return await asyncio.gather(
                *[
                    self._evaluate_single_task(
                        target,
                        task,
                        criteria=criteria,
                        cancellation_token=cancellation_token,
                    )
                    for task in tasks
                ]
            )

        scores: list[EvalScore] = []
        for task in tasks:
            if cancellation_token and cancellation_token.is_cancelled():
                break
            scores.append(
                await self._evaluate_single_task(
                    target,
                    task,
                    criteria=criteria,
                    cancellation_token=cancellation_token,
                )
            )
        return scores

    async def _evaluate_single_task(
        self,
        target: BaseEvalTarget,
        task: EvalTask,
        *,
        criteria: list[str] | None = None,
        cancellation_token: CancellationToken | None = None,
    ) -> EvalScore:
        try:
            trajectory = await target.run(task, cancellation_token)
        except Exception as exc:
            return self._build_zero_score(task, error=str(exc), criteria=criteria)

        # resolve: call-level arg > task metadata > judge default
        effective_criteria = criteria
        if effective_criteria is None:
            raw = task.metadata.get("_criteria")
            if isinstance(raw, list):
                effective_criteria = raw
            elif isinstance(raw, str):
                effective_criteria = [raw]

        try:
            return await self.judge.score(trajectory, effective_criteria, cancellation_token)
        except Exception as exc:
            return self._build_zero_score_from_trajectory(
                trajectory, error=str(exc), criteria=criteria
            )

    def _build_zero_score(
        self,
        task: EvalTask,
        *,
        error: str,
        criteria: list[str] | None,
    ) -> EvalScore:
        """Build a zero-score fallback when target execution fails."""
        dimensions = {dimension: 0.0 for dimension in (criteria or ["accuracy"])}
        failed_trajectory = EvalTrajectory(
            task=task,
            messages=[],
            success=False,
            error=error,
            usage=Usage(),
            metadata={"error": error},
        )
        return EvalScore(
            overall=0.0,
            dimensions=dimensions,
            reasoning={
                dimension: f"Execution failed: {error}" for dimension in dimensions
            },
            trajectory=failed_trajectory,
            metadata={"error": error, "judge": self.judge.name},
        )

    def _build_zero_score_from_trajectory(
        self,
        trajectory: EvalTrajectory,
        *,
        error: str,
        criteria: list[str] | None,
    ) -> EvalScore:
        """Build a zero-score fallback preserving the actual run trajectory when judge scoring fails."""
        dimensions = {dimension: 0.0 for dimension in (criteria or ["accuracy"])}
        return EvalScore(
            overall=0.0,
            dimensions=dimensions,
            reasoning={
                dimension: f"Judge scoring failed: {error}" for dimension in dimensions
            },
            trajectory=trajectory,
            metadata={"error": error, "judge": self.judge.name, "judge_failed": True},
        )


__all__ = ["EvalRunner"]
