"""
Docker sandbox for safe code validation and testing
"""

import docker
import time
import tempfile
import shutil
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, Any, List, Optional
import pandas as pd


@dataclass
class TestFailure:
    """
    Test failure information

    Attributes:
        test_name: Name of failed test
        error_message: Error message
        traceback: Full traceback
        file_path: File where test failed
        line_number: Line number of failure
    """

    test_name: str
    error_message: str
    traceback: str
    file_path: str
    line_number: int


@dataclass
class TestResult:
    """
    Test execution result

    Attributes:
        passed: Whether all tests passed
        total_tests: Total number of tests
        passed_tests: Number of passed tests
        failed_tests: Number of failed tests
        skipped_tests: Number of skipped tests
        failures: List of test failures
        execution_time: Execution time in seconds
        coverage: Code coverage percentage (optional)
    """

    passed: bool
    total_tests: int
    passed_tests: int
    failed_tests: int
    skipped_tests: int
    failures: List[TestFailure] = field(default_factory=list)
    execution_time: float = 0.0
    coverage: Optional[float] = None


@dataclass
class CodeValidationResult:
    """
    Code validation result

    Attributes:
        syntax_valid: Whether syntax is valid
        pep8_compliant: Whether code is PEP8 compliant
        black_formatted: Whether code is black formatted
        errors: List of error messages
        warnings: List of warning messages
        suggestions: List of suggestions
    """

    syntax_valid: bool
    pep8_compliant: bool
    black_formatted: bool
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    suggestions: List[str] = field(default_factory=list)


@dataclass
class BacktestResult:
    """
    Backtest execution result

    Attributes:
        success: Whether backtest completed successfully
        metrics: Performance metrics from backtest
        equity_curve: Equity curve values
        trades: List of trade metrics
        execution_time: Execution time in seconds
        errors: List of error messages
    """

    success: bool
    metrics: Optional[Any] = None  # PeriodMetrics
    equity_curve: List[float] = field(default_factory=list)
    trades: List[Any] = field(default_factory=list)  # List[TradeMetrics]
    execution_time: float = 0.0
    errors: List[str] = field(default_factory=list)


class DockerSandbox:
    """
    Docker sandbox environment for safe code testing

    This class provides an isolated Docker environment for running tests,
    validating code, and executing backtests without affecting the host system.
    """

    def __init__(self, image_name: str = "openfund-agent-sandbox:latest", timeout: int = 300):
        """
        Initialize Docker sandbox

        Args:
            image_name: Docker image name to use
            timeout: Timeout in seconds for operations

        Raises:
            docker.errors.DockerException: If Docker is not available
        """
        self.image_name = image_name
        self.timeout = timeout
        try:
            self.client = docker.from_env()
            # Verify Docker is accessible
            self.client.ping()
        except Exception as e:
            raise RuntimeError(f"Docker is not available: {str(e)}")

    def create_container(
        self, workspace_path: str, env_vars: Optional[Dict[str, str]] = None
    ) -> docker.models.containers.Container:
        """
        Create a sandbox container

        Args:
            workspace_path: Path to workspace directory to mount
            env_vars: Environment variables to set in container

        Returns:
            Docker container object

        Raises:
            docker.errors.ImageNotFound: If image doesn't exist
            docker.errors.APIError: If container creation fails
        """
        if env_vars is None:
            env_vars = {}

        # Ensure workspace path exists
        workspace = Path(workspace_path).resolve()
        if not workspace.exists():
            raise ValueError(f"Workspace path does not exist: {workspace_path}")

        # Create container with workspace mounted
        container = self.client.containers.create(
            image=self.image_name,
            command="sleep infinity",  # Keep container running
            detach=True,
            volumes={str(workspace): {"bind": "/workspace", "mode": "rw"}},
            working_dir="/workspace",
            environment=env_vars,
            network_mode="bridge",
            mem_limit="4g",
            cpu_count=2,
            auto_remove=False,
        )

        # Start the container
        container.start()

        # Wait for container to be ready
        time.sleep(2)

        return container

    def run_tests(
        self, container: docker.models.containers.Container, test_command: str = "pytest"
    ) -> TestResult:
        """
        Run tests in the sandbox

        Args:
            container: Docker container to run tests in
            test_command: Test command to execute (default: pytest)

        Returns:
            Test execution result
        """
        start_time = time.time()

        try:
            # Run pytest with JSON output for parsing
            cmd = (
                f"{test_command} --tb=short --json-report --json-report-file=/tmp/test_report.json"
            )
            exit_code, output = container.exec_run(cmd, workdir="/workspace", demux=True)

            execution_time = time.time() - start_time

            # Parse output
            stdout = output[0].decode("utf-8") if output[0] else ""
            stderr = output[1].decode("utf-8") if output[1] else ""

            # Try to read JSON report
            try:
                exit_code_cat, report_output = container.exec_run("cat /tmp/test_report.json")
                if exit_code_cat == 0:
                    import json

                    report_data = json.loads(report_output.decode("utf-8"))
                    return self._parse_pytest_json_report(report_data, execution_time)
            except Exception:
                pass

            # Fallback: parse text output
            return self._parse_pytest_output(stdout + stderr, execution_time, exit_code)

        except Exception as e:
            return TestResult(
                passed=False,
                total_tests=0,
                passed_tests=0,
                failed_tests=0,
                skipped_tests=0,
                failures=[
                    TestFailure(
                        test_name="test_execution",
                        error_message=str(e),
                        traceback="",
                        file_path="",
                        line_number=0,
                    )
                ],
                execution_time=time.time() - start_time,
            )

    def run_code_validation(
        self, container: docker.models.containers.Container, files: List[str]
    ) -> CodeValidationResult:
        """
        Run code validation checks

        Args:
            container: Docker container to run validation in
            files: List of file paths to validate

        Returns:
            Code validation result
        """
        result = CodeValidationResult(syntax_valid=True, pep8_compliant=True, black_formatted=True)

        for file_path in files:
            # Check syntax
            exit_code, output = container.exec_run(
                f"python -m py_compile {file_path}", workdir="/workspace"
            )
            if exit_code != 0:
                result.syntax_valid = False
                result.errors.append(f"Syntax error in {file_path}: {output.decode('utf-8')}")

            # Check PEP8 with pycodestyle
            exit_code, output = container.exec_run(f"pycodestyle {file_path}", workdir="/workspace")
            if exit_code != 0:
                result.pep8_compliant = False
                result.warnings.append(f"PEP8 violations in {file_path}: {output.decode('utf-8')}")

            # Check black formatting
            exit_code, output = container.exec_run(
                f"black --check {file_path}", workdir="/workspace"
            )
            if exit_code != 0:
                result.black_formatted = False
                result.suggestions.append(f"File {file_path} would be reformatted by black")

        return result

    def run_backtest(
        self,
        container: docker.models.containers.Container,
        strategy_config: Dict[str, Any],
        historical_data: Optional[pd.DataFrame] = None,
    ) -> BacktestResult:
        """
        Run backtest in the sandbox

        Args:
            container: Docker container to run backtest in
            strategy_config: Strategy configuration
            historical_data: Historical market data (optional)

        Returns:
            Backtest execution result
        """
        start_time = time.time()

        try:
            # Save strategy config to container
            import json

            config_json = json.dumps(strategy_config)
            container.exec_run(
                f"echo '{config_json}' > /tmp/strategy_config.json",
                workdir="/workspace",
            )

            # Save historical data if provided
            if historical_data is not None:
                # Convert DataFrame to CSV and copy to container
                with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
                    historical_data.to_csv(f.name, index=False)
                    temp_csv = f.name

                # Copy file to container
                with open(temp_csv, "rb") as f:
                    container.put_archive("/tmp", f.read())

                Path(temp_csv).unlink()

            # Run backtest script
            exit_code, output = container.exec_run(
                "python -m openfund_agent.backtest --config /tmp/strategy_config.json",
                workdir="/workspace",
            )

            execution_time = time.time() - start_time

            if exit_code == 0:
                # Parse backtest results
                stdout = output.decode("utf-8")
                # Try to read results file
                try:
                    exit_code_cat, results_output = container.exec_run(
                        "cat /tmp/backtest_results.json"
                    )
                    if exit_code_cat == 0:
                        results_data = json.loads(results_output.decode("utf-8"))
                        return BacktestResult(
                            success=True,
                            metrics=results_data.get("metrics"),
                            equity_curve=results_data.get("equity_curve", []),
                            trades=results_data.get("trades", []),
                            execution_time=execution_time,
                        )
                except Exception:
                    pass

                return BacktestResult(success=True, execution_time=execution_time, errors=[])
            else:
                stderr = output.decode("utf-8")
                return BacktestResult(
                    success=False,
                    execution_time=execution_time,
                    errors=[f"Backtest failed: {stderr}"],
                )

        except Exception as e:
            return BacktestResult(
                success=False,
                execution_time=time.time() - start_time,
                errors=[f"Backtest execution error: {str(e)}"],
            )

    def cleanup(self, container: docker.models.containers.Container) -> None:
        """
        Clean up and remove container

        Args:
            container: Docker container to clean up
        """
        try:
            container.stop(timeout=10)
            container.remove(force=True)
        except Exception as e:
            # Log error but don't raise
            print(f"Warning: Failed to cleanup container: {str(e)}")

    def _parse_pytest_json_report(
        self, report_data: Dict[str, Any], execution_time: float
    ) -> TestResult:
        """
        Parse pytest JSON report

        Args:
            report_data: JSON report data from pytest
            execution_time: Test execution time

        Returns:
            Parsed test result
        """
        summary = report_data.get("summary", {})
        tests = report_data.get("tests", [])

        failures = []
        for test in tests:
            if test.get("outcome") == "failed":
                call = test.get("call", {})
                failures.append(
                    TestFailure(
                        test_name=test.get("nodeid", "unknown"),
                        error_message=call.get("longrepr", ""),
                        traceback=call.get("traceback", ""),
                        file_path=test.get("file", ""),
                        line_number=test.get("lineno", 0),
                    )
                )

        return TestResult(
            passed=summary.get("failed", 0) == 0,
            total_tests=summary.get("total", 0),
            passed_tests=summary.get("passed", 0),
            failed_tests=summary.get("failed", 0),
            skipped_tests=summary.get("skipped", 0),
            failures=failures,
            execution_time=execution_time,
            coverage=report_data.get("coverage"),
        )

    def _parse_pytest_output(
        self, output: str, execution_time: float, exit_code: int
    ) -> TestResult:
        """
        Parse pytest text output

        Args:
            output: Pytest output text
            execution_time: Test execution time
            exit_code: Process exit code

        Returns:
            Parsed test result
        """
        import re

        # Parse summary line (e.g., "5 passed, 2 failed in 1.23s")
        summary_match = re.search(
            r"(\d+)\s+passed(?:,\s+(\d+)\s+failed)?(?:,\s+(\d+)\s+skipped)?", output
        )

        if summary_match:
            passed = int(summary_match.group(1))
            failed = int(summary_match.group(2) or 0)
            skipped = int(summary_match.group(3) or 0)
            total = passed + failed + skipped
        else:
            # Fallback
            passed = 0 if exit_code != 0 else 1
            failed = 1 if exit_code != 0 else 0
            skipped = 0
            total = 1

        # Parse failures
        failures = []
        failure_pattern = re.compile(r"FAILED\s+(.+?)\s+-\s+(.+?)(?:\n|$)", re.MULTILINE)
        for match in failure_pattern.finditer(output):
            failures.append(
                TestFailure(
                    test_name=match.group(1),
                    error_message=match.group(2),
                    traceback="",
                    file_path="",
                    line_number=0,
                )
            )

        return TestResult(
            passed=exit_code == 0,
            total_tests=total,
            passed_tests=passed,
            failed_tests=failed,
            skipped_tests=skipped,
            failures=failures,
            execution_time=execution_time,
        )
