"""Docker Compose executor for container management."""

import asyncio
import subprocess
import time
from pathlib import Path
from typing import Any

from app.config import get_settings
from app.executors.base import BaseExecutor, ExecutionResult
from app.utils.validation import ValidationError, validate_file_path


class DockerExecutor(BaseExecutor):
    """Execute Docker Compose operations with security controls.

    Security measures:
    - Directory validation against allowed stacks root
    - Compose file existence verification
    - Path traversal prevention
    - Timeout enforcement on each subprocess
    - No shell=True, command list only

    Payload:
        {
            "compose_dir": "/opt/letsbe/stacks/myapp",
            "pull": true  # Optional, defaults to false
        }

    Result:
        {
            "compose_dir": "/opt/letsbe/stacks/myapp",
            "compose_file": "/opt/letsbe/stacks/myapp/docker-compose.yml",
            "pull_ran": true,
            "logs": {
                "pull": "<stdout+stderr>",
                "up": "<stdout+stderr>"
            }
        }
    """

    # Compose file search order
    COMPOSE_FILE_NAMES = ["docker-compose.yml", "compose.yml"]

    # Default timeout for each docker command (seconds)
    DEFAULT_COMMAND_TIMEOUT = 300

    @property
    def task_type(self) -> str:
        return "DOCKER_RELOAD"

    async def execute(self, payload: dict[str, Any]) -> ExecutionResult:
        """Execute Docker Compose pull (optional) and up -d --remove-orphans.

        Args:
            payload: Must contain "compose_dir", optionally "pull" (bool) and "timeout"

        Returns:
            ExecutionResult with reload confirmation and logs
        """
        self.validate_payload(payload, ["compose_dir"])
        settings = get_settings()

        compose_dir = payload["compose_dir"]
        pull = payload.get("pull", False)
        timeout = payload.get("timeout", self.DEFAULT_COMMAND_TIMEOUT)

        # Validate compose directory is under allowed stacks root
        try:
            validated_dir = validate_file_path(
                compose_dir,
                settings.allowed_stacks_root,
                must_exist=True,
            )
        except ValidationError as e:
            self.logger.warning("docker_dir_validation_failed", path=compose_dir, error=str(e))
            return ExecutionResult(
                success=False,
                data={},
                error=f"Directory validation failed: {e}",
            )

        # Verify it's actually a directory
        if not validated_dir.is_dir():
            self.logger.warning("docker_not_directory", path=compose_dir)
            return ExecutionResult(
                success=False,
                data={},
                error=f"Path is not a directory: {compose_dir}",
            )

        # Find compose file in order of preference
        compose_file = self._find_compose_file(validated_dir)
        if compose_file is None:
            self.logger.warning("docker_compose_not_found", dir=compose_dir)
            return ExecutionResult(
                success=False,
                data={},
                error=f"No compose file found in {compose_dir}. "
                      f"Looked for: {', '.join(self.COMPOSE_FILE_NAMES)}",
            )

        self.logger.info(
            "docker_reloading",
            compose_dir=str(validated_dir),
            compose_file=str(compose_file),
            pull=pull,
        )

        start_time = time.time()
        logs: dict[str, str] = {}
        pull_ran = False

        try:
            # Run pull if requested
            if pull:
                pull_ran = True
                exit_code, stdout, stderr = await self._run_compose_command(
                    compose_file,
                    validated_dir,
                    ["pull"],
                    timeout,
                )
                logs["pull"] = self._combine_output(stdout, stderr)

                if exit_code != 0:
                    duration_ms = (time.time() - start_time) * 1000
                    self.logger.warning(
                        "docker_pull_failed",
                        compose_dir=str(validated_dir),
                        exit_code=exit_code,
                        stderr=stderr[:500] if stderr else None,
                    )
                    return ExecutionResult(
                        success=False,
                        data={
                            "compose_dir": str(validated_dir),
                            "compose_file": str(compose_file),
                            "pull_ran": pull_ran,
                            "logs": logs,
                        },
                        error=f"Docker pull failed with exit code {exit_code}",
                        duration_ms=duration_ms,
                    )

            # Run up -d --remove-orphans
            exit_code, stdout, stderr = await self._run_compose_command(
                compose_file,
                validated_dir,
                ["up", "-d", "--remove-orphans"],
                timeout,
            )
            logs["up"] = self._combine_output(stdout, stderr)

            duration_ms = (time.time() - start_time) * 1000
            success = exit_code == 0

            if success:
                self.logger.info(
                    "docker_reloaded",
                    compose_dir=str(validated_dir),
                    exit_code=exit_code,
                    duration_ms=duration_ms,
                )
            else:
                self.logger.warning(
                    "docker_reload_failed",
                    compose_dir=str(validated_dir),
                    exit_code=exit_code,
                    stderr=stderr[:500] if stderr else None,
                )

            return ExecutionResult(
                success=success,
                data={
                    "compose_dir": str(validated_dir),
                    "compose_file": str(compose_file),
                    "pull_ran": pull_ran,
                    "logs": logs,
                },
                error=f"Docker up failed with exit code {exit_code}" if not success else None,
                duration_ms=duration_ms,
            )

        except asyncio.TimeoutError:
            duration_ms = (time.time() - start_time) * 1000
            self.logger.error("docker_timeout", compose_dir=str(validated_dir), timeout=timeout)
            return ExecutionResult(
                success=False,
                data={
                    "compose_dir": str(validated_dir),
                    "compose_file": str(compose_file),
                    "pull_ran": pull_ran,
                    "logs": logs,
                },
                error=f"Docker operation timed out after {timeout} seconds",
                duration_ms=duration_ms,
            )

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            self.logger.error("docker_error", compose_dir=str(validated_dir), error=str(e))
            return ExecutionResult(
                success=False,
                data={
                    "compose_dir": str(validated_dir),
                    "compose_file": str(compose_file),
                    "pull_ran": pull_ran,
                    "logs": logs,
                },
                error=str(e),
                duration_ms=duration_ms,
            )

    def _find_compose_file(self, compose_dir: Path) -> Path | None:
        """Find compose file in the directory.

        Searches in order: docker-compose.yml, compose.yml

        Args:
            compose_dir: Directory to search in

        Returns:
            Path to compose file, or None if not found
        """
        for filename in self.COMPOSE_FILE_NAMES:
            compose_file = compose_dir / filename
            if compose_file.exists():
                return compose_file
        return None

    def _combine_output(self, stdout: str, stderr: str) -> str:
        """Combine stdout and stderr into a single string.

        Args:
            stdout: Standard output
            stderr: Standard error

        Returns:
            Combined output string
        """
        parts = []
        if stdout:
            parts.append(stdout)
        if stderr:
            parts.append(stderr)
        return "\n".join(parts)

    async def _run_compose_command(
        self,
        compose_file: Path,
        compose_dir: Path,
        args: list[str],
        timeout: int,
    ) -> tuple[int, str, str]:
        """Run a docker compose command.

        Args:
            compose_file: Path to compose file
            compose_dir: Working directory
            args: Additional arguments after 'docker compose -f <file>'
            timeout: Operation timeout in seconds

        Returns:
            Tuple of (exit_code, stdout, stderr)
        """
        def _run() -> tuple[int, str, str]:
            # Build command: docker compose -f <file> <args>
            cmd = [
                "docker",
                "compose",
                "-f",
                str(compose_file),
            ] + args

            # Run command from compose directory, no shell=True
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
                cwd=str(compose_dir),
            )

            return result.returncode, result.stdout, result.stderr

        return await asyncio.wait_for(
            asyncio.to_thread(_run),
            timeout=timeout + 30,  # Watchdog with buffer
        )
