"""Pure status-derivation and rollup helpers — the single source of truth for
turn-level and thread-level status under the v1.4.0 unified-error-reporting design.

Two functions, both pure:

* :func:`status_for_response` — turn/item-level status + optional top-level
  error summary, computed from per-evaluator results when the agent responded.
* :func:`rollup_thread_status` — thread-level rollup over per-turn statuses.

The "no response obtained" case (turn/item ``status="error"``) is handled
inline at the agent-failure sites in :mod:`evaluation_runner`, which set
``status=STATUS_ERROR`` and build the cause object directly via
:func:`error_messages.agent_request_failed` or
:func:`error_messages.turn_skipped`. This module only covers the
response-obtained side.

See research.md §R4 for the canonical pseudocode and quickstart.md §2.6 for the
exhaustive test matrix these functions must satisfy.
"""

from __future__ import annotations

from typing import Optional, Sequence, Tuple

from common import STATUS_ERROR, STATUS_FAIL, STATUS_PARTIAL, STATUS_PASS
from error_messages import ErrorObject, evaluators_failed_summary


def status_for_response(
    evaluator_results: Sequence[str],
) -> Tuple[str, Optional[ErrorObject]]:
    """Compute (status, optional summary error) for a turn/item where the agent responded.

    Args:
        evaluator_results: Per-evaluator 'result' values, each in {'pass', 'fail', 'error'}.

    Returns:
        A (status, error) tuple where status is one of 'pass', 'fail', or 'partial':

        * 'pass' — every evaluator returned 'pass', OR no evaluators ran
          (vacuous truth — items with no evaluators pass by default). error is None.
        * 'partial' — at least one evaluator returned 'error'. Error takes
          priority over pass/fail; a turn with one passing evaluator and one
          errored evaluator is 'partial' regardless of the others. error is the
          evaluatorsFailed summary
          {code, message: 'Agent response obtained. N of M evaluators failed to run.'}.
        * 'fail' — every evaluator ran successfully (no errors) AND at least
          one returned 'fail'. Covers uniform-fail and pass+fail mixes. error is None.

        Status 'error' is never returned — the caller handles the no-response
        case directly.
    """
    uniques = set(evaluator_results)
    if not uniques or uniques == {STATUS_PASS}:
        return STATUS_PASS, None
    error_count = sum(1 for r in evaluator_results if r == STATUS_ERROR)
    if error_count > 0:
        return STATUS_PARTIAL, evaluators_failed_summary(error_count, len(evaluator_results))
    return STATUS_FAIL, None


def rollup_thread_status(turn_statuses: Sequence[str]) -> str:
    """Compute a thread-level overall_status from the per-turn statuses.

    Priority rules:

    1. Any errored turn → thread 'error' (the run didn't complete).
    2. Else, any partial turn → thread 'partial'.
    3. Else, all turns 'pass' → 'pass'.
    4. Else, all turns 'fail' → 'fail'.
    5. Else (mix of pass and fail at thread level) → 'partial'.

    Note rule 5 (pass+fail mix → 'partial') does not match status_for_response
    at the per-turn level (where pass+fail among evaluators yields 'fail').
    The thread-level rule preserves existing behaviour; the mismatch is known
    and deferred for revisit.

    As a defensive fallback an empty sequence returns 'error'.
    """
    if not turn_statuses:
        return STATUS_ERROR
    if STATUS_ERROR in turn_statuses:
        return STATUS_ERROR
    if STATUS_PARTIAL in turn_statuses:
        return STATUS_PARTIAL
    uniques = set(turn_statuses)
    if uniques == {STATUS_PASS}:
        return STATUS_PASS
    if uniques == {STATUS_FAIL}:
        return STATUS_FAIL
    return STATUS_PARTIAL
