"""
Test Execution Engine
Manages test case execution with real-time progress updates
"""

import asyncio
import uuid
from datetime import datetime
from enum import Enum
from typing import Optional, List, Dict, Any, Callable
from dataclasses import dataclass, field

from opentester.models.project import TargetType
from opentester.core.executors import CLIExecutor, ExecutionContext
from opentester.core.storage import get_execution_storage


class ExecutionStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    STOPPED = "stopped"


class StepStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    PASSED = "passed"
    FAILED = "failed"
    SKIPPED = "skipped"


@dataclass
class StepResult:
    step_index: int
    action: str
    status: StepStatus
    message: str = ""
    screenshot: Optional[str] = None  # base64 encoded
    duration_ms: int = 0
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    data: Dict[str, Any] = field(default_factory=dict)  # Detailed execution data (stdout, stderr, etc.)


@dataclass
class ExecutionState:
    execution_id: str
    project_id: str
    case_id: str
    status: ExecutionStatus
    current_step: int = 0
    total_steps: int = 0
    results: List[StepResult] = field(default_factory=list)
    start_time: Optional[str] = None
    end_time: Optional[str] = None
    error_message: Optional[str] = None


class ExecutionManager:
    """Manages active executions and WebSocket connections with persistence."""

    def __init__(self):
        self._executions: Dict[str, ExecutionState] = {}
        self._connections: Dict[str, List[Callable]] = {}
        self._active_tasks: Dict[str, asyncio.Task] = {}
        self._storage = get_execution_storage()
        self._load_executions_from_disk()

    def _load_executions_from_disk(self):
        """Load historical executions from disk on startup."""
        try:
            executions = self._storage.list_executions(limit=1000)
            for data in executions:
                # Convert dict back to ExecutionState
                execution = ExecutionState(
                    execution_id=data["execution_id"],
                    project_id=data["project_id"],
                    case_id=data["case_id"],
                    status=ExecutionStatus(data.get("status", "pending")),
                    current_step=data.get("current_step", 0),
                    total_steps=data.get("total_steps", 0),
                    start_time=data.get("start_time"),
                    end_time=data.get("end_time"),
                    error_message=data.get("error_message"),
                    results=[
                        StepResult(
                            step_index=r["step_index"],
                            action=r["action"],
                            status=StepStatus(r["status"]),
                            message=r.get("message", ""),
                            screenshot=r.get("screenshot"),
                            duration_ms=r.get("duration_ms", 0),
                            timestamp=r.get("timestamp", ""),
                            data=r.get("data", {}),
                        )
                        for r in data.get("results", [])
                    ]
                )
                self._executions[data["execution_id"]] = execution
                self._connections[data["execution_id"]] = []
        except Exception as e:
            print(f"Warning: Failed to load executions from disk: {e}")

    def _execution_to_dict(self, execution: ExecutionState) -> dict:
        """Convert ExecutionState to dict for storage."""
        return {
            "execution_id": execution.execution_id,
            "project_id": execution.project_id,
            "case_id": execution.case_id,
            "status": execution.status.value,
            "current_step": execution.current_step,
            "total_steps": execution.total_steps,
            "start_time": execution.start_time,
            "end_time": execution.end_time,
            "error_message": execution.error_message,
            "results": [
                {
                    "step_index": r.step_index,
                    "action": r.action,
                    "status": r.status.value,
                    "message": r.message,
                    "screenshot": r.screenshot,
                    "duration_ms": r.duration_ms,
                    "timestamp": r.timestamp,
                    "data": r.data,
                }
                for r in execution.results
            ],
        }

    def _persist_execution(self, execution_id: str):
        """Persist execution to disk."""
        execution = self._executions.get(execution_id)
        if execution:
            try:
                self._storage.save_execution(self._execution_to_dict(execution))
            except Exception as e:
                print(f"Warning: Failed to persist execution {execution_id}: {e}")

    def create_execution(self, project_id: str, case_id: str, total_steps: int) -> str:
        """Create a new execution and return its ID."""
        execution_id = str(uuid.uuid4())
        self._executions[execution_id] = ExecutionState(
            execution_id=execution_id,
            project_id=project_id,
            case_id=case_id,
            status=ExecutionStatus.PENDING,
            total_steps=total_steps,
            start_time=datetime.now().isoformat(),
        )
        self._connections[execution_id] = []
        # Persist to disk
        self._persist_execution(execution_id)
        return execution_id

    def get_execution(self, execution_id: str) -> Optional[ExecutionState]:
        """Get execution state by ID."""
        return self._executions.get(execution_id)

    def subscribe(self, execution_id: str, callback: Callable):
        """Subscribe to execution updates."""
        if execution_id in self._connections:
            self._connections[execution_id].append(callback)

    def unsubscribe(self, execution_id: str, callback: Callable):
        """Unsubscribe from execution updates."""
        if execution_id in self._connections:
            if callback in self._connections[execution_id]:
                self._connections[execution_id].remove(callback)

    async def _notify(self, execution_id: str, data: Dict[str, Any]):
        """Notify all subscribers of an update."""
        if execution_id in self._connections:
            for callback in self._connections[execution_id]:
                try:
                    if asyncio.iscoroutinefunction(callback):
                        await callback(data)
                    else:
                        callback(data)
                except Exception:
                    pass  # Ignore callback errors

    async def update_step(
        self,
        execution_id: str,
        step_result: StepResult,
    ):
        """Update execution with step result and notify subscribers."""
        execution = self._executions.get(execution_id)
        if not execution:
            return

        execution.results.append(step_result)
        execution.current_step = step_result.step_index + 1

        # Persist to disk after each step
        self._persist_execution(execution_id)

        await self._notify(execution_id, {
            "type": "step_update",
            "execution_id": execution_id,
            "step": {
                "index": step_result.step_index,
                "action": step_result.action,
                "status": step_result.status.value,
                "message": step_result.message,
                "duration_ms": step_result.duration_ms,
                "timestamp": step_result.timestamp,
                "data": step_result.data,
            },
            "progress": {
                "current": execution.current_step,
                "total": execution.total_steps,
                "percentage": int((execution.current_step / execution.total_steps) * 100),
            },
        })

    async def set_status(
        self,
        execution_id: str,
        status: ExecutionStatus,
        error_message: Optional[str] = None,
    ):
        """Set execution status and notify."""
        execution = self._executions.get(execution_id)
        if not execution:
            return

        execution.status = status
        if error_message:
            execution.error_message = error_message
        if status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED, ExecutionStatus.STOPPED]:
            execution.end_time = datetime.now().isoformat()

        # Persist to disk on status change
        self._persist_execution(execution_id)

        await self._notify(execution_id, {
            "type": "status_update",
            "execution_id": execution_id,
            "status": status.value,
            "error_message": error_message,
            "end_time": execution.end_time,
        })

    async def start_execution(
        self,
        execution_id: str,
        steps: List[Dict[str, Any]],
        target_type: TargetType = TargetType.CLI,
        variables: Optional[Dict[str, Any]] = None,
    ):
        """Start executing test steps."""
        execution = self._executions.get(execution_id)
        if not execution:
            return

        await self.set_status(execution_id, ExecutionStatus.RUNNING)

        # Create execution task
        task = asyncio.create_task(self._run_steps(
            execution_id, steps, target_type, variables
        ))
        self._active_tasks[execution_id] = task

        try:
            await task
        except asyncio.CancelledError:
            await self.set_status(execution_id, ExecutionStatus.STOPPED)
        except Exception as e:
            await self.set_status(execution_id, ExecutionStatus.FAILED, str(e))

    async def _run_steps(
        self,
        execution_id: str,
        steps: List[Dict[str, Any]],
        target_type: TargetType = TargetType.CLI,
        variables: Optional[Dict[str, Any]] = None,
    ):
        """Run all test steps using appropriate executor."""
        execution = self._executions.get(execution_id)
        if not execution:
            return

        # Create execution context
        context = ExecutionContext(
            execution_id=execution_id,
            project_id=execution.project_id,
            case_id=execution.case_id,
            target_type=target_type.value,
            variables=variables or {},
        )

        # Select executor based on target type
        if target_type == TargetType.CLI:
            executor = CLIExecutor(context)
        else:
            # For now, default to CLI executor for TUI and GUI (until implemented)
            executor = CLIExecutor(context)

        try:
            for i, step in enumerate(steps):
                # Check if stopped
                if execution_id not in self._active_tasks or executor.is_stopped():
                    break

                start_time = datetime.now()
                step_start_iso = start_time.isoformat()

                # Execute step using executor
                result = await executor.execute_step(step)
                duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)

                # Convert executor result to StepResult
                step_status = StepStatus.PASSED if result.success else StepStatus.FAILED

                # Create step result
                step_result = StepResult(
                    step_index=i,
                    action=step.get("action", "unknown"),
                    status=step_status,
                    message=result.message,
                    screenshot=result.screenshot,
                    duration_ms=duration_ms,
                    timestamp=step_start_iso,
                    data=result.data,
                )

                await self.update_step(execution_id, step_result)

                # Handle failure based on step's on_failure setting
                if not result.success:
                    on_failure = step.get("on_failure", "fail")
                    if on_failure == "fail":
                        await self.set_status(
                            execution_id,
                            ExecutionStatus.FAILED,
                            f"Step {i + 1} failed: {result.message}"
                        )
                        return
                    elif on_failure == "ignore":
                        step_result.status = StepStatus.SKIPPED
                    # TODO: Implement retry and fallback handling

            # Mark as completed if not stopped and not already failed
            execution = self._executions.get(execution_id)
            if execution and execution.status == ExecutionStatus.RUNNING:
                await self.set_status(execution_id, ExecutionStatus.COMPLETED)

        except Exception as e:
            await self.set_status(execution_id, ExecutionStatus.FAILED, str(e))
        finally:
            executor.stop()

    def stop_execution(self, execution_id: str) -> bool:
        """Stop a running execution."""
        task = self._active_tasks.get(execution_id)
        if task and not task.done():
            task.cancel()
            return True
        return False

    def cleanup(self, execution_id: str):
        """Clean up execution resources."""
        self._executions.pop(execution_id, None)
        self._connections.pop(execution_id, None)
        self._active_tasks.pop(execution_id, None)


# Global execution manager instance
execution_manager = ExecutionManager()
