"""
Test Agent - Execute allowlisted test commands with receipt generation

WO-ECHO-TEST-AGENT-01: Agent for running pytest, py_compile, and tools scripts
with chain-hash verification and receipt generation.

Usage:
    from helix_cdc.echo.agents.test_agent import TestAgent
    from helix_cdc.echo.agents import register_agent

    register_agent(TestAgent())
"""

import asyncio
import hashlib
import json
import shutil
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from . import (
    AgentResult,
    DispatchableAgent,
    PROJECT_ROOT,
    generate_agent_id,
)

# Import command validation/execution from ops helpers
from helix_cdc.echo.ops.helpers import (
    ALLOWED_COMMANDS,
    BLOCKED_PATTERNS,
    validate_command,
    execute_command,
    generate_receipt_id,
)


class TestAgent(DispatchableAgent):
    """
    Agent for executing test commands with receipt generation.

    Runs allowlisted commands (pytest, python -m, make targets) and produces
    receipts with chain-hash verification for audit.

    Safety:
    - Only allows commands from ALLOWED_COMMANDS in helpers.py
    - Validates against BLOCKED_PATTERNS
    - Sandboxed to PROJECT_ROOT
    - All executions produce receipts
    """

    @property
    def agent_type(self) -> str:
        return "test"

    def __init__(
        self,
        timeout_seconds: int = 120,
        receipt_dir: str = "receipts/test_agent",
    ):
        """
        Initialize the test agent.

        Args:
            timeout_seconds: Maximum execution time for commands
            receipt_dir: Directory for receipt storage (relative to PROJECT_ROOT)
        """
        self.timeout_seconds = timeout_seconds
        self.receipt_dir = PROJECT_ROOT / receipt_dir
        self._python_available: Optional[bool] = None

    def available(self) -> bool:
        """Check if python3 and PROJECT_ROOT are writable."""
        if self._python_available is None:
            # Check python3 exists
            python_path = shutil.which("python3")
            if not python_path:
                self._python_available = False
            else:
                # Check PROJECT_ROOT is writable
                try:
                    test_file = PROJECT_ROOT / ".test_agent_check"
                    test_file.touch()
                    test_file.unlink()
                    self._python_available = True
                except Exception:
                    self._python_available = False
        return self._python_available

    def _parse_test_intent(self, query: str) -> Optional[Dict[str, Any]]:
        """
        Parse test intent from natural language query.

        Returns dict with command and args, or None if not parseable.
        """
        # Use the helper function from ops
        from helix_cdc.echo.ops.helpers import _parse_test_command
        return _parse_test_command(query)

    def _compute_chain_hash(
        self,
        prompt_sha: str,
        argv_json_sha: str,
        stdout_sha: str,
        stderr_sha: str,
        exit_code: int,
    ) -> str:
        """
        Compute chain hash for receipt verification.

        chain_hash = sha256(prompt_sha || argv_json_sha || stdout_sha || stderr_sha || exit_code)
        """
        data = f"{prompt_sha}|{argv_json_sha}|{stdout_sha}|{stderr_sha}|{exit_code}"
        return hashlib.sha256(data.encode()).hexdigest()

    def _build_receipt(
        self,
        prompt: str,
        command_argv: List[str],
        result: Dict[str, Any],
        status: str,
    ) -> Dict[str, Any]:
        """
        Build a receipt for the test execution.

        Returns receipt dict with all verification fields.
        """
        receipt_id = generate_receipt_id()
        timestamp = datetime.now(timezone.utc)

        # Compute hashes
        prompt_sha = hashlib.sha256(prompt.encode()).hexdigest()
        argv_json = json.dumps(command_argv, sort_keys=True)
        argv_json_sha = hashlib.sha256(argv_json.encode()).hexdigest()

        stdout = result.get("stdout", "")
        stderr = result.get("stderr", "")
        stdout_sha = hashlib.sha256(stdout.encode()).hexdigest()
        stderr_sha = hashlib.sha256(stderr.encode()).hexdigest()

        # Derived output hash for compatibility (stdout + "\n" + stderr)
        combined_output = stdout + "\n" + stderr
        output_sha = hashlib.sha256(combined_output.encode()).hexdigest()

        exit_code = result.get("exit_code", -1)

        chain_hash = self._compute_chain_hash(
            prompt_sha, argv_json_sha, stdout_sha, stderr_sha, exit_code
        )

        # Truncate output for preview (last 40 lines)
        def get_preview(text: str, lines: int = 40) -> str:
            all_lines = text.split("\n")
            if len(all_lines) <= lines:
                return text
            return "\n".join(all_lines[-lines:])

        receipt = {
            # Schema version for forward compatibility
            "schema_version": "1.0.0",

            "receipt_id": receipt_id,
            "receipt_type": "test_execution",
            "timestamp_utc": timestamp.isoformat(),
            "work_order": "WO-ECHO-TEST-AGENT-01",
            "prompt": prompt,
            "prompt_sha256": prompt_sha,
            "command_argv": command_argv,
            "argv_sha256": argv_json_sha,  # Explicit storage for easy verification
            "cwd": str(PROJECT_ROOT),
            "timeout_s": self.timeout_seconds,
            "exit_code": exit_code,
            "stdout_sha256": stdout_sha,
            "stderr_sha256": stderr_sha,
            "output_sha256": output_sha,  # Derived: sha256(stdout + "\n" + stderr)
            "stdout_preview": get_preview(stdout),
            "stderr_preview": get_preview(stderr),
            "elapsed_ms": int(result.get("elapsed_seconds", 0) * 1000),
            "status": status,
            "chain_hash": chain_hash,

            # Self-documenting verification formulas
            "verification": {
                "chain_hash_formula": "sha256(prompt_sha256 | argv_sha256 | stdout_sha256 | stderr_sha256 | exit_code)",
                "argv_sha256_formula": "sha256(json.dumps(command_argv, sort_keys=True))",
                "output_sha256_formula": "sha256(stdout + '\\n' + stderr)",
                "separator": "|",
            },
        }

        return receipt

    def _save_receipt(self, receipt: Dict[str, Any]) -> str:
        """
        Save receipt to disk.

        Returns relative path to saved receipt.
        """
        # Ensure receipt directory exists
        self.receipt_dir.mkdir(parents=True, exist_ok=True)

        # Generate filename
        timestamp = datetime.now(timezone.utc)
        date_str = timestamp.strftime("%Y%m%d")
        receipt_id = receipt.get("receipt_id", generate_receipt_id())[:8]
        status = receipt.get("status", "UNKNOWN").lower()

        filename = f"{status}_{date_str}_{receipt_id}.json"
        receipt_path = self.receipt_dir / filename

        with open(receipt_path, "w") as f:
            json.dump(receipt, f, indent=2, default=str)

        return str(receipt_path.relative_to(PROJECT_ROOT))

    def _determine_status(self, result: Dict[str, Any], was_rejected: bool = False) -> str:
        """
        Determine test status from execution result.

        Returns: PASS, FAIL, TIMEOUT, REJECTED, or ERROR
        """
        if was_rejected:
            return "REJECTED"

        exit_code = result.get("exit_code", -1)
        stderr = result.get("stderr", "")

        if "timed out" in stderr.lower():
            return "TIMEOUT"
        elif exit_code == 0:
            return "PASS"
        elif exit_code == -1:
            return "ERROR"
        else:
            return "FAIL"

    async def execute(self, query: str, context: Dict[str, Any]) -> AgentResult:
        """
        Execute a test command for the given query.

        Args:
            query: The user's query (e.g., "run pytest tests/ -v")
            context: Additional context (parsed_command, etc.)

        Returns:
            AgentResult with execution results and receipt source
        """
        start_time = time.perf_counter()
        agent_id = generate_agent_id()

        if not self.available():
            return AgentResult(
                agent_id=agent_id,
                agent_type=self.agent_type,
                query=query,
                result={"status": "ERROR", "error": "TestAgent not available"},
                sources=[],
                confidence=0.0,
                elapsed_ms=0.0,
                error="python3 not available or PROJECT_ROOT not writable",
            )

        # Get parsed command from context or parse it ourselves
        parsed_command = context.get("parsed_command")
        if not parsed_command:
            parsed_command = self._parse_test_intent(query)

        if not parsed_command:
            elapsed_ms = (time.perf_counter() - start_time) * 1000
            return AgentResult(
                agent_id=agent_id,
                agent_type=self.agent_type,
                query=query,
                result={
                    "status": "REJECTED",
                    "error": "Could not parse test command from query",
                },
                sources=[],
                confidence=0.0,
                elapsed_ms=elapsed_ms,
                error="Could not parse test command from query",
            )

        command = parsed_command["command"]
        args = parsed_command["args"]

        # Validate command against allowlist
        is_valid, reason = validate_command(command, args)

        if not is_valid:
            # Build rejection receipt
            rejection_result = {
                "exit_code": -1,
                "stdout": "",
                "stderr": f"Command rejected: {reason}",
                "elapsed_seconds": 0,
            }
            receipt = self._build_receipt(query, [command] + args, rejection_result, "REJECTED")
            receipt_path = self._save_receipt(receipt)

            elapsed_ms = (time.perf_counter() - start_time) * 1000
            return AgentResult(
                agent_id=agent_id,
                agent_type=self.agent_type,
                query=query,
                result={
                    "status": "REJECTED",
                    "command_argv": [command] + args,
                    "reason": reason,
                    "receipt_path": receipt_path,
                },
                sources=[f"receipt:{receipt_path}"],
                confidence=0.0,
                elapsed_ms=elapsed_ms,
                error=f"Command rejected: {reason}",
            )

        # Execute the command (in thread pool to not block)
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(
            None,
            lambda: execute_command(
                command,
                args,
                timeout_seconds=self.timeout_seconds,
                cwd=str(PROJECT_ROOT),
            ),
        )

        # Determine status
        status = self._determine_status(result)

        # Build and save receipt
        command_argv = [command] + args
        receipt = self._build_receipt(query, command_argv, result, status)
        receipt_path = self._save_receipt(receipt)

        # Compute confidence based on status
        if status == "PASS":
            confidence = 1.0
        elif status == "FAIL":
            confidence = 0.8  # We still executed successfully, just tests failed
        elif status == "TIMEOUT":
            confidence = 0.5
        else:
            confidence = 0.0

        elapsed_ms = (time.perf_counter() - start_time) * 1000

        # Build result data
        result_data = {
            "status": status,
            "command_argv": command_argv,
            "exit_code": result.get("exit_code"),
            "elapsed_ms": int(result.get("elapsed_seconds", 0) * 1000),
            "stdout_preview": receipt.get("stdout_preview", ""),
            "stderr_preview": receipt.get("stderr_preview", ""),
            "receipt_path": receipt_path,
            "chain_hash": receipt.get("chain_hash"),
        }

        return AgentResult(
            agent_id=agent_id,
            agent_type=self.agent_type,
            query=query,
            result=result_data,
            sources=[f"receipt:{receipt_path}"],
            confidence=confidence,
            elapsed_ms=elapsed_ms,
            error=None if status in ("PASS", "FAIL") else f"Test execution {status}",
        )


# Auto-register the agent when this module is imported
from . import register_agent as _register

try:
    _register(TestAgent())
except ValueError:
    pass  # Already registered
