"""
Base Executor class for all test executors.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
import re


@dataclass
class StepExecutionResult:
    """Result of executing a single step."""
    success: bool
    message: str
    output: Optional[str] = None  # stdout or captured output
    error: Optional[str] = None   # stderr or error message
    data: Dict[str, Any] = field(default_factory=dict)  # Additional data (exit_code, etc.)
    screenshot: Optional[str] = None  # base64 encoded screenshot (for GUI)
    duration_ms: int = 0


@dataclass
class ExecutionContext:
    """Context passed during test execution."""
    execution_id: str
    project_id: str
    case_id: str
    target_type: str  # gui, tui, cli
    variables: Dict[str, Any] = field(default_factory=dict)
    screenshots: List[str] = field(default_factory=list)  # In-memory screenshot storage
    step_results: List[StepExecutionResult] = field(default_factory=list)

    def get_var(self, name: str, default: Any = None) -> Any:
        """Get variable value with ${vars.x} or ${env.x} syntax."""
        # Handle ${vars.name}
        if name.startswith("vars."):
            return self.variables.get(name[5:], default)
        # Handle ${env.VAR}
        if name.startswith("env."):
            import os
            return os.environ.get(name[4:], default)
        # Direct variable access
        return self.variables.get(name, default)

    def set_var(self, name: str, value: Any):
        """Set variable value."""
        self.variables[name] = value


class BaseExecutor(ABC):
    """Base class for all test executors."""

    def __init__(self, context: ExecutionContext):
        self.context = context
        self._stopped = False

    @abstractmethod
    async def execute_step(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Execute a single test step."""
        pass

    def stop(self):
        """Signal the executor to stop."""
        self._stopped = True

    def is_stopped(self) -> bool:
        """Check if execution has been stopped."""
        return self._stopped

    def resolve_variables(self, text: str) -> str:
        """Resolve variables in text like ${vars.name} or ${env.VAR}."""
        import re

        def replace_var(match):
            var_expr = match.group(1)
            # Handle ${var_name} - direct variable
            if "." not in var_expr:
                return str(self.context.get_var(var_expr, f"${{{var_expr}}}"))
            # Handle ${vars.name} or ${env.VAR}
            return str(self.context.get_var(var_expr, f"${{{var_expr}}}"))

        # Pattern matches ${...} including nested braces
        result = text
        # Handle simple variables first
        result = re.sub(r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}', replace_var, result)
        # Handle vars.xxx and env.xxx
        result = re.sub(r'\$\{(vars\.[a-zA-Z_][a-zA-Z0-9_]*|env\.[a-zA-Z_][a-zA-Z0-9_]*)\}', replace_var, result)

        return result

    async def validate_assertion(self, assertion: Dict[str, Any], actual_data: Dict[str, Any]) -> tuple[bool, str]:
        """Validate an assertion against actual execution data."""
        assertion_type = assertion.get("type", "")
        expected = assertion.get("expected")

        if assertion_type == "exit_code":
            actual = actual_data.get("exit_code")
            success = actual == expected
            return success, f"Exit code: expected {expected}, got {actual}"

        elif assertion_type == "stdout_contains":
            actual = actual_data.get("stdout", "")
            pattern = str(expected)
            success = pattern in actual
            return success, f"Stdout contains '{pattern}': {success}"

        elif assertion_type == "stderr_contains":
            actual = actual_data.get("stderr", "")
            pattern = str(expected)
            success = pattern in actual
            return success, f"Stderr contains '{pattern}': {success}"

        elif assertion_type == "stdout_matches":
            import re
            actual = actual_data.get("stdout", "")
            pattern = str(expected)
            try:
                success = bool(re.search(pattern, actual))
                return success, f"Stdout matches pattern '{pattern}': {success}"
            except re.error as e:
                return False, f"Invalid regex pattern: {e}"

        elif assertion_type == "stderr_empty":
            actual = actual_data.get("stderr", "").strip()
            success = len(actual) == 0
            return success, f"Stderr is empty: {success}"

        return False, f"Unknown assertion type: {assertion_type}"

    async def execute_control_flow_step(
        self,
        step: Dict[str, Any],
        step_executor: callable,
    ) -> StepExecutionResult:
        """Execute a control flow step (if, loop, for_each).

        Args:
            step: The control flow step dictionary
            step_executor: Async function to execute individual steps

        Returns:
            StepExecutionResult with success status and execution details
        """
        action = step.get("action", "")

        if action == "if":
            return await self._execute_if_step(step, step_executor)
        elif action == "loop":
            return await self._execute_loop_step(step, step_executor)
        elif action == "for_each":
            return await self._execute_for_each_step(step, step_executor)

        return StepExecutionResult(
            success=False,
            message=f"Unknown control flow action: {action}",
        )

    async def _execute_if_step(
        self,
        step: Dict[str, Any],
        step_executor: callable,
    ) -> StepExecutionResult:
        """Execute an if conditional step.

        Evaluates the condition and executes either 'then' or 'else' branch.
        """
        condition = step.get("condition", {})
        then_steps = step.get("then", step.get("then_steps", []))
        else_steps = step.get("else", step.get("else_steps", []))

        # Evaluate condition
        condition_met = await self._evaluate_condition(condition)

        # Execute appropriate branch
        branch_results = []
        executed_branch = "then" if condition_met else "else"
        steps_to_execute = then_steps if condition_met else else_steps

        for sub_step in steps_to_execute:
            if self.is_stopped():
                break
            result = await step_executor(sub_step)
            branch_results.append({
                "action": sub_step.get("action", "unknown"),
                "success": result.success,
                "message": result.message,
            })

        return StepExecutionResult(
            success=True,
            message=f"If condition evaluated to {condition_met}, executed {executed_branch} branch with {len(branch_results)} steps",
            data={
                "condition_met": condition_met,
                "executed_branch": executed_branch,
                "branch_results": branch_results,
            },
        )

    async def _execute_loop_step(
        self,
        step: Dict[str, Any],
        step_executor: callable,
    ) -> StepExecutionResult:
        """Execute a loop step.

        Repeatedly executes body while condition is true, up to max_iterations.
        """
        condition = step.get("condition", {})
        max_iterations = step.get("max_iterations", 10)
        body = step.get("body", [])
        loop_interval = step.get("loop_interval", 0.5)

        iteration = 0
        all_iterations = []

        while iteration < max_iterations:
            if self.is_stopped():
                break

            # Check condition
            condition_met = await self._evaluate_condition(condition)
            if not condition_met:
                break

            iteration += 1
            iteration_results = []

            # Execute body
            for sub_step in body:
                if self.is_stopped():
                    break
                result = await step_executor(sub_step)
                iteration_results.append({
                    "action": sub_step.get("action", "unknown"),
                    "success": result.success,
                    "message": result.message,
                })

            all_iterations.append({
                "iteration": iteration,
                "results": iteration_results,
            })

            # Small delay between iterations
            if loop_interval > 0:
                import asyncio
                await asyncio.sleep(loop_interval)

        return StepExecutionResult(
            success=True,
            message=f"Loop executed {iteration} iteration(s)",
            data={
                "iterations": iteration,
                "max_iterations": max_iterations,
                "iterations_detail": all_iterations,
            },
        )

    async def _execute_for_each_step(
        self,
        step: Dict[str, Any],
        step_executor: callable,
    ) -> StepExecutionResult:
        """Execute a for_each step.

        Iterates over items and executes steps for each item.
        """
        items = step.get("items", [])
        var_name = step.get("var", step.get("for_each_var", "item"))
        steps = step.get("body", step.get("steps", []))

        # Resolve items if it's a variable reference
        if isinstance(items, str):
            resolved = self.resolve_variables(items)
            if resolved != items:  # Variable was resolved
                items = resolved

        # Convert items to list if needed
        if isinstance(items, str):
            # Try to parse as comma-separated or newline-separated
            items = [item.strip() for item in items.replace("\n", ",").split(",") if item.strip()]

        if not isinstance(items, list):
            return StepExecutionResult(
                success=False,
                message=f"For each items must be a list, got {type(items).__name__}",
            )

        all_results = []
        for idx, item in enumerate(items):
            if self.is_stopped():
                break

            # Set loop variable
            self.context.set_var(var_name, item)

            item_results = []
            for sub_step in steps:
                if self.is_stopped():
                    break
                result = await step_executor(sub_step)
                item_results.append({
                    "action": sub_step.get("action", "unknown"),
                    "success": result.success,
                    "message": result.message,
                })

            all_results.append({
                "index": idx,
                "item": item,
                "results": item_results,
            })

        return StepExecutionResult(
            success=True,
            message=f"For each executed over {len(items)} item(s)",
            data={
                "item_count": len(items),
                "var_name": var_name,
                "results": all_results,
            },
        )

    async def _evaluate_condition(self, condition: Dict[str, Any]) -> bool:
        """Evaluate an if condition.

        Supports various condition types:
        - output_contains: Check if last step output contains pattern
        - exit_code: Check if last step exit code matches expected
        - var_equals: Check if variable equals expected value
        - var_not_equals: Check if variable does not equal expected value
        - var_contains: Check if variable contains pattern
        - var_empty: Check if variable is empty
        - var_not_empty: Check if variable is not empty
        """
        condition_type = condition.get("type", "")

        # Get last step result for output/exit_code checks
        last_result = None
        if self.context.step_results:
            last_result = self.context.step_results[-1]

        if condition_type == "output_contains":
            pattern = condition.get("pattern", "")
            if last_result and last_result.output:
                return pattern in last_result.output
            return False

        elif condition_type == "exit_code":
            expected_code = condition.get("expected_code", condition.get("expected", 0))
            if last_result and "exit_code" in last_result.data:
                return last_result.data.get("exit_code") == expected_code
            return False

        elif condition_type == "var_equals":
            var_name = condition.get("variable", "")
            expected = condition.get("expected", condition.get("value"))
            actual = self.context.get_var(var_name)
            return str(actual) == str(expected)

        elif condition_type == "var_not_equals":
            var_name = condition.get("variable", "")
            expected = condition.get("expected", condition.get("value"))
            actual = self.context.get_var(var_name)
            return str(actual) != str(expected)

        elif condition_type == "var_contains":
            var_name = condition.get("variable", "")
            pattern = condition.get("pattern", condition.get("expected", ""))
            actual = str(self.context.get_var(var_name, ""))
            return pattern in actual

        elif condition_type == "var_empty":
            var_name = condition.get("variable", "")
            actual = self.context.get_var(var_name)
            return actual is None or str(actual) == ""

        elif condition_type == "var_not_empty":
            var_name = condition.get("variable", "")
            actual = self.context.get_var(var_name)
            return actual is not None and str(actual) != ""

        # Unknown condition type - default to False
        return False
