# VERIFICATION_STAMP
# Story: 3.01 (Track B) — MVFLTrigger — 3-Condition Detection
# Verified By: parallel-builder (claude-sonnet-4-6)
# Verified At: 2026-02-25
# Tests: 9/9
# Coverage: 100% (all branches in evaluate, _check_external_rejection,
#                 _check_semantic_inconsistency, _check_syntax_errors)

"""
Genesis Persistent Context Architecture — MVFL Trigger
Story 3.01 — Track B

Evaluates swarm output against 3 trigger conditions:
1. Syntax/Format — missing required fields, invalid JSON, type mismatches
2. Semantic Inconsistency — internal contradictions in output
3. External Rejection — HTTP 4xx/5xx or api_error present

Priority: External (3) > Semantic (2) > Syntax (1)
"""
from dataclasses import dataclass
from typing import Optional


@dataclass
class MVFLTriggerResult:
    """Result of MVFL trigger evaluation."""
    triggered: bool
    trigger_type: Optional[str]  # "syntax" | "semantic" | "external_rejection" | None
    severity: int  # 0=clean, 1=warning, 2=error, 3=critical
    details: str


class MVFLTrigger:
    """Evaluates swarm output for 3 error trigger conditions."""

    def evaluate(self, output: dict, task_payload: dict) -> MVFLTriggerResult:
        """
        Evaluate output against all 3 trigger conditions.
        Returns first triggered condition (highest priority first).

        Priority order:
          External rejection (severity 3) > Semantic (severity 2) > Syntax (severity 1)
        """
        # Check external rejection first (highest priority)
        ext = self._check_external_rejection(output)
        if ext:
            return ext

        # Then semantic inconsistency
        sem = self._check_semantic_inconsistency(output, task_payload)
        if sem:
            return sem

        # Then syntax/format errors
        syn = self._check_syntax_errors(output, task_payload)
        if syn:
            return syn

        return MVFLTriggerResult(False, None, 0, "Clean output")

    # ------------------------------------------------------------------
    # Condition 3: External Rejection
    # ------------------------------------------------------------------

    def _check_external_rejection(self, output: dict) -> Optional[MVFLTriggerResult]:
        """Condition 3: HTTP 4xx/5xx codes or api_error present."""
        # Check for HTTP status codes >= 400
        status_code = output.get("status_code")
        if status_code is not None and isinstance(status_code, int) and status_code >= 400:
            return MVFLTriggerResult(
                triggered=True,
                trigger_type="external_rejection",
                severity=3,
                details=f"External rejection: HTTP {status_code}",
            )

        # Check for api_error field (truthy value)
        api_error = output.get("api_error")
        if api_error:
            return MVFLTriggerResult(
                triggered=True,
                trigger_type="external_rejection",
                severity=3,
                details=f"External rejection: API error — {api_error}",
            )

        # Check for explicit error status with error message
        if output.get("status") == "error" and output.get("error"):
            return MVFLTriggerResult(
                triggered=True,
                trigger_type="external_rejection",
                severity=3,
                details=f"External rejection: {output['error']}",
            )

        return None

    # ------------------------------------------------------------------
    # Condition 2: Semantic Inconsistency
    # ------------------------------------------------------------------

    def _check_semantic_inconsistency(
        self, output: dict, task_payload: dict
    ) -> Optional[MVFLTriggerResult]:
        """Condition 2: Internal contradictions in multi-field output."""
        inconsistencies = []

        # status='completed' but result is None
        if output.get("status") == "completed" and output.get("result") is None:
            inconsistencies.append("status='completed' but result=None")

        # status='completed' but result is empty string
        if output.get("status") == "completed" and output.get("result") == "":
            inconsistencies.append("status='completed' but result is empty string")

        # status='completed' but error field is also present
        if output.get("status") == "completed" and output.get("error"):
            inconsistencies.append("status='completed' but error field present")

        # Negative duration
        duration = output.get("duration_ms")
        if (
            duration is not None
            and isinstance(duration, (int, float))
            and duration < 0
        ):
            inconsistencies.append(f"negative duration: {duration}ms")

        if inconsistencies:
            return MVFLTriggerResult(
                triggered=True,
                trigger_type="semantic",
                severity=2,
                details=f"Semantic inconsistency: {'; '.join(inconsistencies)}",
            )

        return None

    # ------------------------------------------------------------------
    # Condition 1: Syntax / Format Errors
    # ------------------------------------------------------------------

    def _check_syntax_errors(
        self, output: dict, task_payload: dict
    ) -> Optional[MVFLTriggerResult]:
        """Condition 1: Missing required fields, type mismatches."""
        errors = []

        schema = task_payload.get("expected_schema", {})

        if schema:
            # Validate against explicit schema provided by task_payload
            type_map: dict = {
                "str": str,
                "int": int,
                "float": float,
                "list": list,
                "dict": dict,
                "bool": bool,
            }
            for field, field_spec in schema.items():
                if not isinstance(field_spec, dict):
                    continue

                if field_spec.get("required"):
                    if field not in output or output[field] is None:
                        errors.append(f"Missing required field: {field}")
                        continue  # Skip type check — field is absent

                    expected_type = field_spec.get("type")
                    if expected_type and expected_type in type_map:
                        if not isinstance(output[field], type_map[expected_type]):
                            actual_type = type(output[field]).__name__
                            errors.append(
                                f"Type mismatch for {field}: expected {expected_type},"
                                f" got {actual_type}"
                            )
        else:
            # Default minimum schema: every swarm output must carry task_id + status
            for field in ("task_id", "status"):
                if field not in output:
                    errors.append(f"Missing expected field: {field}")

        if errors:
            return MVFLTriggerResult(
                triggered=True,
                trigger_type="syntax",
                severity=1,
                details=f"Syntax/format errors: {'; '.join(errors)}",
            )

        return None
