"""
CLI Executor - Executes CLI commands and assertions.
Uses subprocess for process execution.
"""

import asyncio
import shlex
from typing import Any, Dict, List, Optional

from .base import BaseExecutor, ExecutionContext, StepExecutionResult


class CLIExecutor(BaseExecutor):
    """Executor for CLI (command-line interface) applications."""

    def __init__(self, context: ExecutionContext):
        super().__init__(context)
        self._process: Optional[asyncio.subprocess.Process] = None
        self._last_stdout = ""
        self._last_stderr = ""
        self._last_exit_code = 0

    async def execute_step(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Execute a single CLI test step."""
        action = step.get("action", "")
        step_name = step.get("name", f"Step {action}")

        if self.is_stopped():
            return StepExecutionResult(
                success=False,
                message=f"Step '{step_name}' skipped - execution stopped",
            )

        # Route to specific action handler
        handlers = {
            "exec": self._handle_exec,
            "launch": self._handle_launch,
            "wait": self._handle_wait,
            "assert": self._handle_assert,
            "screenshot": self._handle_screenshot,
        }

        # Handle control flow actions
        if action in ("if", "loop", "for_each"):
            return await self.execute_control_flow_step(step, self.execute_step)

        handler = handlers.get(action)
        if handler:
            result = await handler(step)
            # Store result in context for condition evaluation
            self.context.step_results.append(result)
            return result
        else:
            return StepExecutionResult(
                success=False,
                message=f"Unknown action: {action}",
            )

    async def _handle_exec(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Execute a CLI command using subprocess."""
        command = step.get("command", "")
        if not command:
            return StepExecutionResult(
                success=False,
                message="No command specified for exec action",
            )

        # Resolve variables in command
        command = self.resolve_variables(command)

        # Get options
        working_dir = step.get("working_dir")
        if working_dir:
            working_dir = self.resolve_variables(working_dir)

        env_vars = step.get("env", {})
        resolved_env = {}
        for k, v in env_vars.items():
            resolved_env[k] = self.resolve_variables(v)

        capture = step.get("capture", ["stdout", "stderr", "exit_code"])
        timeout = step.get("timeout", 30)

        # Merge with current environment
        import os
        exec_env = {**os.environ, **resolved_env} if resolved_env else None

        try:
            # Execute command - on Windows, use subprocess in a thread to avoid asyncio issues
            import sys
            import subprocess
            import os

            if sys.platform == "win32":
                # Use sync subprocess in executor to avoid Windows asyncio issues
                loop = asyncio.get_event_loop()

                def run_subprocess():
                    return subprocess.run(
                        command,
                        shell=True,
                        capture_output=True,
                        cwd=working_dir,
                        env=exec_env or os.environ,
                        timeout=timeout,
                    )

                result = await loop.run_in_executor(None, run_subprocess)

                stdout_data = result.stdout
                stderr_data = result.stderr
                returncode = result.returncode
            else:
                # On Unix, use asyncio subprocess
                self._process = await asyncio.create_subprocess_shell(
                    command,
                    stdout=asyncio.subprocess.PIPE if "stdout" in capture else asyncio.subprocess.DEVNULL,
                    stderr=asyncio.subprocess.PIPE if "stderr" in capture else asyncio.subprocess.DEVNULL,
                    cwd=working_dir,
                    env=exec_env,
                )

                # Wait for completion with timeout
                stdout_data = b""
                stderr_data = b""

                if "stdout" in capture or "stderr" in capture:
                    stdout_data, stderr_data = await asyncio.wait_for(
                        self._process.communicate(),
                        timeout=timeout
                    )
                else:
                    await asyncio.wait_for(self._process.wait(), timeout=timeout)

                returncode = self._process.returncode or 0

            # Store results
            self._last_stdout = stdout_data.decode("utf-8", errors="replace") if stdout_data else ""
            self._last_stderr = stderr_data.decode("utf-8", errors="replace") if stderr_data else ""
            self._last_exit_code = returncode

            # Build result
            message_parts = [f"Executed: {command}"]
            data = {
                "exit_code": self._last_exit_code,
                "command": command,
            }

            if "stdout" in capture:
                data["stdout"] = self._last_stdout
                # Truncate for message
                stdout_preview = self._last_stdout[:200] + "..." if len(self._last_stdout) > 200 else self._last_stdout
                if stdout_preview:
                    message_parts.append(f"stdout: {stdout_preview}")

            if "stderr" in capture:
                data["stderr"] = self._last_stderr
                if self._last_stderr:
                    stderr_preview = self._last_stderr[:200] + "..." if len(self._last_stderr) > 200 else self._last_stderr
                    message_parts.append(f"stderr: {stderr_preview}")

            if "exit_code" in capture:
                message_parts.append(f"exit_code: {self._last_exit_code}")

            # Determine success (non-zero exit code is not necessarily a failure for exec)
            success = True

            return StepExecutionResult(
                success=success,
                message=" | ".join(message_parts),
                output=self._last_stdout if "stdout" in capture else None,
                error=self._last_stderr if self._last_stderr else None,
                data=data,
            )

        except asyncio.TimeoutError:
            if self._process:
                try:
                    self._process.kill()
                    await self._process.wait()
                except:
                    pass
            return StepExecutionResult(
                success=False,
                message=f"Command timed out after {timeout}s: {command}",
                error="Timeout",
                data={"exit_code": -1, "command": command},
            )

        except Exception as e:
            return StepExecutionResult(
                success=False,
                message=f"Failed to execute command: {command}",
                error=str(e),
                data={"exit_code": -1, "command": command},
            )

        finally:
            self._process = None

    async def _handle_launch(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Launch a CLI application (similar to exec but expects long-running)."""
        command = step.get("command", "")
        if not command:
            return StepExecutionResult(
                success=False,
                message="No command specified for launch action",
            )

        command = self.resolve_variables(command)
        timeout = step.get("timeout", 5)  # Shorter timeout for launch verification

        try:
            self._process = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )

            # Give it a moment to start
            await asyncio.sleep(0.5)

            # Check if process is still running
            if self._process.returncode is None:
                return StepExecutionResult(
                    success=True,
                    message=f"Launched: {command} (PID: {self._process.pid})",
                    data={"pid": self._process.pid, "command": command},
                )
            else:
                # Process exited quickly
                stdout, stderr = await self._process.communicate()
                return StepExecutionResult(
                    success=False,
                    message=f"Process exited immediately: {command}",
                    output=stdout.decode("utf-8", errors="replace") if stdout else None,
                    error=stderr.decode("utf-8", errors="replace") if stderr else None,
                    data={"exit_code": self._process.returncode, "command": command},
                )

        except Exception as e:
            return StepExecutionResult(
                success=False,
                message=f"Failed to launch: {command}",
                error=str(e),
            )

    async def _handle_wait(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Wait for a condition or duration."""
        # Support both formats:
        # 1. condition: {type: duration, value: 3}
        # 2. duration: 3 (direct)
        condition = step.get("condition", {})

        # Check for direct duration field
        if "duration" in step:
            duration = step["duration"]
            await asyncio.sleep(duration)
            return StepExecutionResult(
                success=True,
                message=f"Waited for {duration}s",
                data={"waited": duration},
            )

        condition_type = condition.get("type", "duration")

        if condition_type == "duration":
            duration = condition.get("value", 1)
            await asyncio.sleep(duration)
            return StepExecutionResult(
                success=True,
                message=f"Waited for {duration}s",
                data={"waited": duration},
            )

        elif condition_type == "output_contains":
            pattern = condition.get("pattern", "")
            timeout = condition.get("timeout", 10)

            # Wait for pattern in output
            start_time = asyncio.get_event_loop().time()
            while asyncio.get_event_loop().time() - start_time < timeout:
                if pattern in self._last_stdout or pattern in self._last_stderr:
                    return StepExecutionResult(
                        success=True,
                        message=f"Found pattern '{pattern}' in output",
                    )
                await asyncio.sleep(0.1)

            return StepExecutionResult(
                success=False,
                message=f"Timeout waiting for pattern '{pattern}' in output",
                error="Pattern not found",
            )

        else:
            return StepExecutionResult(
                success=False,
                message=f"Unknown wait condition type: {condition_type}",
            )

    async def _handle_assert(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Handle assertion step."""
        assertion = step.get("assertion", {})
        assertion_type = assertion.get("type", "")
        expected = assertion.get("expected")

        actual_data = {
            "exit_code": self._last_exit_code,
            "stdout": self._last_stdout,
            "stderr": self._last_stderr,
        }

        success, message = await self.validate_assertion(assertion, actual_data)

        return StepExecutionResult(
            success=success,
            message=f"Assertion ({assertion_type}): {message}",
            data={
                "assertion_type": assertion_type,
                "expected": expected,
                "actual": actual_data.get(assertion_type.replace("_", "") if assertion_type != "exit_code" else "exit_code"),
            },
        )

    async def _handle_screenshot(self, step: Dict[str, Any]) -> StepExecutionResult:
        """CLI doesn't support screenshots - return placeholder."""
        name = step.get("name", step.get("screenshot_name", "screenshot"))
        return StepExecutionResult(
            success=True,
            message=f"Screenshot not applicable for CLI tests (requested: {name})",
        )

    def stop(self):
        """Stop the running process if any."""
        super().stop()
        if self._process and self._process.returncode is None:
            try:
                self._process.kill()
            except:
                pass
