from ci.util.global_interrupt_handler import handle_keyboard_interrupt


#!/usr/bin/env python3
"""
Docker-based QEMU ESP32 Runner

Runs ESP32 firmware in QEMU using Docker containers for better isolation
and consistency across different environments.
"""

import argparse
import os
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Optional, cast

from running_process import RunningProcess

from ci.docker_utils.DockerManager import DockerManager


# Docker image constants
DEFAULT_DOCKER_IMAGE = "espressif/idf:latest"
ALTERNATIVE_DOCKER_IMAGE = "espressif/idf:latest"
FALLBACK_DOCKER_IMAGE = "espressif/idf:release-v5.2"

# QEMU binary paths inside espressif/idf Docker container
QEMU_RISCV32_PATH = "/opt/esp/tools/qemu-riscv32/esp_develop_9.2.2_20250817/qemu/bin/qemu-system-riscv32"
QEMU_XTENSA_PATH = (
    "/opt/esp/tools/qemu-xtensa/esp_develop_9.2.2_20250817/qemu/bin/qemu-system-xtensa"
)

# QEMU wrapper script template (formatted at runtime)
QEMU_WRAPPER_SCRIPT_TEMPLATE = """#!/bin/bash
set -e
echo "Starting {echo_target} QEMU emulation..."
echo "Firmware: {firmware_path}"
echo "Machine: {qemu_machine}"
echo "QEMU system: {qemu_system}"
echo "Container: $(cat /etc/os-release | head -1)"

# Check if firmware file exists
if [ ! -f "{firmware_path}" ]; then
    echo "ERROR: Firmware file not found: {firmware_path}"
    exit 1
fi

# Check firmware size
FIRMWARE_SIZE=$(stat -c%s "{firmware_path}")
echo "Firmware size: $FIRMWARE_SIZE bytes"

# Copy firmware to writable location since QEMU needs write access
cp "{firmware_path}" /tmp/flash.bin
echo "Copied firmware to writable location: /tmp/flash.bin"

# Try different QEMU configurations depending on machine type
if [ "{qemu_machine}" = "esp32c3" ]; then
    # ESP32C3 uses RISC-V architecture
    echo "Running {qemu_system} for {qemu_machine}"
    {qemu_system} \\
        -nographic \\
        -machine {qemu_machine} \\
        -drive file="/tmp/flash.bin",if=mtd,format=raw \\
        -global driver=timer.{qemu_machine}.timg,property=wdt_disable,value=true \\
        -monitor none \\
        -serial mon:stdio
else
    # ESP32/ESP32-S2/ESP32-S3 use Xtensa architecture
    # Note: The watchdog timer driver name must match the machine type
    # (timer.esp32.timg for esp32, timer.esp32s3.timg for esp32s3, etc.)
    echo "Running {qemu_system} for {qemu_machine}"
    {qemu_system} \\
        -nographic \\
        -machine {qemu_machine} \\
        -drive file="/tmp/flash.bin",if=mtd,format=raw \\
        -global driver=timer.{qemu_machine}.timg,property=wdt_disable,value=true \\
        -monitor none \\
        -serial mon:stdio
fi

echo "QEMU execution completed"
exit 0
"""


def get_docker_env() -> dict[str, str]:
    """Get environment for Docker commands, handling Git Bash/MSYS2 path conversion."""
    env = os.environ.copy()
    # Set UTF-8 encoding environment variables for Windows
    env["PYTHONIOENCODING"] = "utf-8"
    env["PYTHONUTF8"] = "1"
    # Only set MSYS_NO_PATHCONV if we're in a Git Bash/MSYS2 environment
    if (
        "MSYSTEM" in os.environ
        or os.environ.get("TERM") == "xterm"
        or "bash.exe" in os.environ.get("SHELL", "")
    ):
        env["MSYS_NO_PATHCONV"] = "1"
    return env


def run_subprocess_safe(
    cmd: list[str], **kwargs: Any
) -> subprocess.CompletedProcess[str]:
    """Run subprocess with safe UTF-8 handling and error replacement."""
    kwargs.setdefault("encoding", "utf-8")
    kwargs.setdefault("errors", "replace")
    kwargs.setdefault("env", get_docker_env())
    return subprocess.run(cmd, **kwargs)  # type: ignore[return-value]


def run_docker_command_streaming(cmd: list[str]) -> int:
    """Run docker command with streaming output using RunningProcess."""
    print(f"Executing: {' '.join(cmd)}")
    proc = RunningProcess(cmd, env=get_docker_env(), auto_run=True)

    # Stream all output to stdout
    with proc.line_iter(timeout=None) as it:
        for line in it:
            print(line)

    returncode = cast(int, proc.wait())
    if returncode != 0:
        raise subprocess.CalledProcessError(returncode, cmd)
    return returncode


def run_docker_command_no_fail(cmd: list[str]) -> int:
    """Run docker command with streaming output, but don't raise exceptions on failure."""
    print(f"Executing: {' '.join(cmd)}")
    proc = RunningProcess(cmd, env=get_docker_env(), auto_run=True)

    # Stream all output to stdout
    with proc.line_iter(timeout=None) as it:
        for line in it:
            print(line)

    return cast(int, proc.wait())


class DockerQEMURunner:
    """Runner for ESP32 QEMU emulation using Docker containers.

    Container Naming Convention:
        Simulator containers use platform-level naming with timestamps for uniqueness:
        - Pattern: fastled-simulator-{platform}-{timestamp}
        - Examples:
            - fastled-simulator-esp-xtensa-1234567890 (ESP32, ESP32-S2, ESP32-S3, ESP8266)
            - fastled-simulator-esp-riscv-1234567890 (ESP32-C3, ESP32-C6, etc.)
            - fastled-simulator-avr-1234567890 (Uno, ATtiny, etc.)

        Simulator containers are temporary (created/destroyed per test run) unlike
        compiler containers which are persistent (paused/unpaused).

    Image Naming Convention:
        - Registry images: niteris/fastled-simulator-{platform}:latest
        - Local images: fastled-platformio-{board}-{hash} (for compatibility)
    """

    def __init__(self, docker_image: Optional[str] = None):
        """Initialize Docker QEMU runner.

        Args:
            docker_image: Docker image to use, defaults to espressif/idf
        """
        self.docker_manager = DockerManager()
        self.docker_image = docker_image or DEFAULT_DOCKER_IMAGE
        self.container_name = None
        # Use Linux-style paths for all containers since we're using Ubuntu/Alpine
        self.firmware_mount_path = "/workspace/firmware"

    def check_docker_available(self) -> bool:
        """Check if Docker is available and running."""
        try:
            proc = RunningProcess(
                ["docker", "version"], env=get_docker_env(), auto_run=True
            )
            returncode = proc.wait(timeout=5)
            return returncode == 0
        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except (Exception, FileNotFoundError):
            return False

    def check_image_exists(self, image_name: str) -> bool:
        """Check if Docker image exists locally.

        Args:
            image_name: Name of the Docker image to check

        Returns:
            True if image exists, False otherwise
        """
        try:
            proc = RunningProcess(
                ["docker", "images", "-q", image_name],
                env=get_docker_env(),
                auto_run=True,
            )
            stdout_lines: list[str] = []
            with proc.line_iter(timeout=None) as it:
                for line in it:
                    stdout_lines.append(line)
            result_stdout = "\n".join(stdout_lines)
            return bool(result_stdout.strip())
        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except Exception:
            return False

    def get_board_image_name(self, platform: str) -> str:
        """Get Docker image name for board platform.

        Args:
            platform: Board platform name (e.g., 'esp32dev', 'esp32s3')

        Returns:
            Docker image name following FastLED convention
        """
        from ci.docker_utils.build_image import generate_config_hash

        try:
            config_hash = generate_config_hash(platform)
            return f"fastled-platformio-{platform}-{config_hash}"
        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except Exception:
            # Fallback if hash generation fails
            return f"fastled-platformio-{platform}"

    def get_registry_image_name(self, platform: str) -> str:
        """Get Docker Hub registry image name for board platform.

        Args:
            platform: Board platform name (e.g., 'esp32dev', 'esp32s3')

        Returns:
            Docker Hub registry image name (e.g., 'niteris/fastled-simulator-esp-xtensa')
        """
        from ci.docker_utils.build_platforms import get_platform_for_board

        # Get platform family (e.g., 'esp32s3' -> 'esp-xtensa')
        platform_family = get_platform_for_board(platform)
        if not platform_family:
            # Fallback: try direct platform name
            fallback_image = f"niteris/fastled-simulator-{platform}"
            print(
                f"⚠️  Warning: Platform family not found for '{platform}', using fallback: {fallback_image}"
            )
            return fallback_image

        # Get simulator image name (e.g., 'niteris/fastled-simulator-esp-xtensa')
        # Note: Simulator images follow same platform-level naming as compiler images
        registry_image = f"niteris/fastled-simulator-{platform_family}"
        return registry_image

    def pull_and_tag_board_image(self, platform: str) -> bool:
        """Try to pull board image from Docker Hub registry and tag it locally.

        Args:
            platform: Board platform name (e.g., 'esp32dev', 'esp32s3')

        Returns:
            True if pull and tag succeeded, False otherwise
        """
        from ci.docker_utils.build_platforms import get_platform_for_board

        # Show platform family resolution for debugging
        platform_family = get_platform_for_board(platform)
        if platform_family:
            print(f"📦 Platform '{platform}' belongs to family '{platform_family}'")

        registry_image = self.get_registry_image_name(platform)
        local_image = self.get_board_image_name(platform)

        print(f"🔍 Attempting to pull from Docker Hub: {registry_image}")
        print(f"   Will be tagged locally as: {local_image}")
        print("   This may take a few minutes on first run...")
        print()

        try:
            # Pull from Docker Hub
            pull_cmd = ["docker", "pull", registry_image]
            proc = RunningProcess(pull_cmd, env=get_docker_env(), auto_run=True)

            # Stream pull output
            with proc.line_iter(timeout=None) as it:
                for line in it:
                    print(line)

            returncode = proc.wait()

            if returncode != 0:
                print("❌ Failed to pull image from Docker Hub")
                return False

            print()
            print(f"✅ Successfully pulled {registry_image}")
            print(f"   Tagging as local image: {local_image}")

            # Tag the pulled image with local name
            tag_cmd = ["docker", "tag", registry_image, local_image]
            tag_proc = RunningProcess(tag_cmd, env=get_docker_env(), auto_run=True)
            tag_returncode = tag_proc.wait()

            if tag_returncode != 0:
                print("❌ Failed to tag pulled image")
                return False

            print("✅ Image ready for use")
            print()
            return True

        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except Exception as e:
            print(f"❌ Error pulling from registry: {e}")
            return False

    def build_board_image(self, platform: str, progress: bool = True) -> int:
        """Build Docker image for platform using ci-compile.py logic.

        Args:
            platform: Board platform name (e.g., 'esp32dev', 'esp32s3')
            progress: Whether to show progress indicators

        Returns:
            Exit code (0 for success, non-zero for failure)
        """
        if progress:
            print(f"🔨 Building Docker image for {platform}...")
            print("   This will take approximately 30 minutes")
            print("   Progress will be shown below:")
            print()

        # Build command: uv run ci/ci-compile.py --docker --build {platform} Blink
        build_cmd = [
            "uv",
            "run",
            "ci/ci-compile.py",
            platform,
            "--examples",
            "Blink",
            "--docker",
            "--build",
        ]

        proc = RunningProcess(
            build_cmd,
            env=get_docker_env(),
            timeout=1800000,  # 30 minutes in milliseconds
            auto_run=True,
        )

        # Stream build output
        with proc.line_iter(timeout=None) as it:
            for line in it:
                print(line)

        returncode = cast(int, proc.wait())

        if returncode == 0 and progress:
            print()
            print(f"✅ Docker image for {platform} built successfully")
            print()
        elif returncode != 0:
            print()
            print(f"❌ Failed to build Docker image for {platform}")
            print()

        return returncode

    def pull_image(self):
        """Pull the Docker image if not already available."""
        print(f"Ensuring Docker image {self.docker_image} is available...")
        try:
            # Check if image exists locally
            proc = RunningProcess(
                ["docker", "images", "-q", self.docker_image],
                env=get_docker_env(),
                auto_run=True,
            )
            stdout_lines: list[str] = []
            with proc.line_iter(timeout=None) as it:
                for line in it:
                    stdout_lines.append(line)
            result_stdout = "\n".join(stdout_lines)
            if not result_stdout.strip():
                # Image doesn't exist, pull it directly using docker command
                print(f"Pulling Docker image: {self.docker_image}")
                run_docker_command_streaming(["docker", "pull", self.docker_image])
                print(f"Successfully pulled {self.docker_image}")
            else:
                print(f"Image {self.docker_image} already available locally")
        except subprocess.CalledProcessError as e:
            print(f"Warning: Failed to pull image {self.docker_image}: {e}")
            if self.docker_image == DEFAULT_DOCKER_IMAGE:
                print(f"Trying alternative image: {ALTERNATIVE_DOCKER_IMAGE}")
                self.docker_image = ALTERNATIVE_DOCKER_IMAGE
                try:
                    print(f"Pulling alternative Docker image: {self.docker_image}")
                    run_docker_command_streaming(["docker", "pull", self.docker_image])
                    print(f"Successfully pulled {self.docker_image}")
                except subprocess.CalledProcessError as e2:
                    print(f"Failed to pull alternative image: {e2}")
                    print(f"Trying fallback image: {FALLBACK_DOCKER_IMAGE}")
                    self.docker_image = FALLBACK_DOCKER_IMAGE
                    try:
                        print(f"Pulling fallback Docker image: {self.docker_image}")
                        run_docker_command_streaming(
                            ["docker", "pull", self.docker_image]
                        )
                        print(f"Successfully pulled {self.docker_image}")
                    except subprocess.CalledProcessError as e3:
                        print(f"Failed to pull fallback image: {e3}")
                        raise e3
            else:
                raise e

    def prepare_firmware(self, firmware_path: Path) -> Path:
        """Prepare firmware files for mounting into Docker container.

        Args:
            firmware_path: Path to firmware.bin or build directory

        Returns:
            Path to the prepared firmware directory
        """
        # Create temporary directory for firmware files
        temp_dir = Path(tempfile.mkdtemp(prefix="qemu_firmware_"))

        try:
            if firmware_path.is_file() and firmware_path.suffix == ".bin":
                # Copy single firmware file
                shutil.copy2(firmware_path, temp_dir / "firmware.bin")
                # Create proper 4MB flash image for QEMU
                self._create_flash_image(firmware_path, temp_dir / "flash.bin")
            else:
                # Copy entire build directory
                if firmware_path.is_dir():
                    # Copy relevant files from build directory
                    patterns = ["*.bin", "*.elf", "partitions.csv", "flash_args"]
                    for pattern in patterns:
                        for file in firmware_path.glob(pattern):
                            shutil.copy2(file, temp_dir)

                    # Also check for PlatformIO build structure
                    pio_build = firmware_path / ".pio" / "build" / "esp32dev"
                    if pio_build.exists():
                        for pattern in patterns:
                            for file in pio_build.glob(pattern):
                                shutil.copy2(file, temp_dir)

                    # Create proper flash.bin file from firmware.bin
                    firmware_bin = temp_dir / "firmware.bin"
                    if firmware_bin.exists():
                        # Create proper 4MB flash image for QEMU
                        self._create_flash_image(firmware_bin, temp_dir / "flash.bin")
                    else:
                        # Create a minimal 4MB flash.bin for testing
                        flash_bin = temp_dir / "flash.bin"
                        flash_bin.write_bytes(
                            b"\xff" * (4 * 1024 * 1024)
                        )  # 4MB of 0xFF
                else:
                    raise ValueError(f"Invalid firmware path: {firmware_path}")

            return temp_dir
        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except Exception as e:
            # Clean up on error
            shutil.rmtree(temp_dir, ignore_errors=True)
            raise e

    def _create_flash_image(
        self, firmware_path: Path, output_path: Path, flash_size_mb: int = 4
    ):
        """Create a proper flash image for QEMU ESP32.

        Args:
            firmware_path: Path to the firmware.bin file
            output_path: Path where to write the flash.bin
            flash_size_mb: Flash size in MB (must be 2, 4, 8, or 16)
        """
        if flash_size_mb not in [2, 4, 8, 16]:
            raise ValueError(
                f"Flash size must be 2, 4, 8, or 16 MB, got {flash_size_mb}"
            )

        flash_size = flash_size_mb * 1024 * 1024

        # Read firmware content
        firmware_data = firmware_path.read_bytes()

        # Create flash image: firmware at beginning, rest filled with 0xFF
        flash_data = firmware_data + b"\xff" * (flash_size - len(firmware_data))

        # Ensure we have exactly the right size
        if len(flash_data) > flash_size:
            raise ValueError(
                f"Firmware size ({len(firmware_data)} bytes) exceeds flash size ({flash_size} bytes)"
            )

        flash_data = flash_data[:flash_size]  # Truncate to exact size

        output_path.write_bytes(flash_data)

    def build_qemu_command(
        self,
        machine: str = "esp32",
    ) -> list[str]:
        """Build QEMU command to run inside Docker container.

        Args:
            machine: QEMU machine type (esp32, esp32c3, esp32s3, etc.)

        Returns:
            List of command arguments for QEMU
        """
        firmware_path = f"{self.firmware_mount_path}/flash.bin"

        # Determine QEMU system and machine based on target
        if machine == "esp32c3":
            qemu_system = QEMU_RISCV32_PATH
            qemu_machine = "esp32c3"
            echo_target = "ESP32C3"
        elif machine == "esp32s3":
            qemu_system = QEMU_XTENSA_PATH
            qemu_machine = "esp32s3"
            echo_target = "ESP32S3"
        else:
            # Default to ESP32 (Xtensa)
            qemu_system = QEMU_XTENSA_PATH
            qemu_machine = "esp32"
            echo_target = "ESP32"

        # Format the wrapper script template with runtime values
        wrapper_script = QEMU_WRAPPER_SCRIPT_TEMPLATE.format(
            echo_target=echo_target,
            firmware_path=firmware_path,
            qemu_machine=qemu_machine,
            qemu_system=qemu_system,
        )

        cmd = ["bash", "-c", wrapper_script]

        return cmd

    def run(
        self,
        firmware_path: Path,
        timeout: int = 30,
        interrupt_regex: Optional[str] = None,
        interactive: bool = False,
        output_file: Optional[str] = None,
        machine: str = "esp32",
        skip_pull: bool = False,
    ) -> int:
        """Run ESP32/ESP32C3/ESP32S3 firmware in QEMU using Docker.

        Args:
            firmware_path: Path to firmware.bin or build directory
            timeout: Timeout in seconds (timeout is treated as success)
            interrupt_regex: Regex pattern to detect in output (informational only)
            interactive: Run in interactive mode (attach to container)
            output_file: Optional file path to write QEMU output to
            machine: QEMU machine type (esp32, esp32c3, esp32s3, etc.)
            skip_pull: Skip pulling Docker image (assumes image already exists)

        Returns:
            Exit code: actual QEMU/container exit code, except timeout returns 0
        """
        if not self.check_docker_available():
            print("ERROR: Docker is not available or not running", file=sys.stderr)
            print("Please install Docker and ensure it's running", file=sys.stderr)
            return 1

        # Pull image if needed (unless skip_pull is True)
        if not skip_pull:
            self.pull_image()
        else:
            print(f"Skipping image pull (using existing {self.docker_image})")

        # Prepare firmware files
        print(f"Preparing firmware from: {firmware_path}")
        temp_firmware_dir = None

        try:
            temp_firmware_dir = self.prepare_firmware(firmware_path)

            # Generate unique container name based on platform family
            # Determine platform family from machine type
            from ci.docker_utils.build_platforms import get_platform_for_board

            platform = get_platform_for_board(machine)
            if platform:
                # Use platform-level naming with timestamp for uniqueness
                # Simulator containers are temporary (created/destroyed per test)
                self.container_name = f"fastled-simulator-{platform}-{int(time.time())}"
            else:
                # Fallback for unmapped boards
                self.container_name = f"fastled-simulator-{machine}-{int(time.time())}"

            # Convert Windows paths to Docker-compatible paths
            def windows_to_docker_path(path_str: str | Path) -> str:
                """Convert Windows path to Docker volume mount format."""
                import os

                # Check if we're in Git Bash/MSYS2 environment
                is_git_bash = (
                    "MSYSTEM" in os.environ
                    or os.environ.get("TERM") == "xterm"
                    or "bash.exe" in os.environ.get("SHELL", "")
                )

                if os.name == "nt" and is_git_bash:
                    # Convert C:\path\to\dir to /c/path/to/dir for Git Bash
                    path_str = str(path_str).replace("\\", "/")
                    if len(path_str) > 2 and path_str[1:3] == ":/":  # Drive letter
                        path_str = "/" + path_str[0].lower() + path_str[2:]
                else:
                    # For cmd.exe on Windows, keep native Windows paths
                    path_str = str(path_str)

                return path_str

            docker_firmware_path = windows_to_docker_path(temp_firmware_dir)

            # Only mount the firmware directory - QEMU is built into the container
            volumes = {
                docker_firmware_path: {
                    "bind": self.firmware_mount_path,
                    "mode": "ro",  # Read-only mount
                },
            }

            # Build QEMU command
            qemu_cmd = self.build_qemu_command(machine=machine)

            print(f"Running QEMU in Docker container: {self.container_name}")
            print(f"QEMU command: {' '.join(qemu_cmd)}")

            if interactive:
                # Run interactively with streaming
                return self.docker_manager.run_container_streaming(
                    image_name=self.docker_image,
                    command=qemu_cmd,
                    volumes=volumes,
                    name=self.container_name,
                    timeout=timeout,
                    interrupt_pattern=interrupt_regex,
                    output_file=output_file,
                )
            else:
                # Run with streaming output and return actual exit code
                return self.docker_manager.run_container_streaming(
                    image_name=self.docker_image,
                    command=qemu_cmd,
                    volumes=volumes,
                    name=self.container_name,
                    timeout=timeout,
                    interrupt_pattern=interrupt_regex,
                    output_file=output_file,
                )

        except KeyboardInterrupt as ki:
            handle_keyboard_interrupt(ki)
            raise
        except Exception as e:
            print(f"ERROR: {e}", file=sys.stderr)
            return 1
        finally:
            # Cleanup
            if temp_firmware_dir and temp_firmware_dir.exists():
                shutil.rmtree(temp_firmware_dir, ignore_errors=True)

            # Note: Container cleanup is handled automatically by the --rm flag
            # in run_container_streaming(). The container is automatically removed
            # when it exits, so we don't need to manually stop/rm it here.
            # Attempting cleanup here creates race conditions when DockerManager
            # already stops the container on timeout.


def main():
    parser = argparse.ArgumentParser(
        description="Run ESP32 firmware in QEMU using Docker"
    )
    parser.add_argument(
        "firmware_path", type=Path, help="Path to firmware.bin file or build directory"
    )
    parser.add_argument(
        "--docker-image",
        type=str,
        help=f"Docker image to use (default: {DEFAULT_DOCKER_IMAGE})",
    )
    parser.add_argument(
        "--flash-size", type=int, default=4, help="Flash size in MB (default: 4)"
    )
    parser.add_argument(
        "--timeout", type=int, default=30, help="Timeout in seconds (default: 30)"
    )
    parser.add_argument(
        "--interrupt-regex", type=str, help="Regex pattern to interrupt execution"
    )
    parser.add_argument(
        "--interactive", action="store_true", help="Run in interactive mode"
    )
    parser.add_argument("--output-file", type=str, help="File to write QEMU output to")
    parser.add_argument(
        "--machine",
        type=str,
        default="esp32",
        help="QEMU machine type: esp32, esp32c3, esp32s3 (default: esp32)",
    )
    parser.add_argument(
        "--skip-pull",
        action="store_true",
        help="Skip pulling Docker image (assumes image already exists)",
    )

    args = parser.parse_args()

    if not args.firmware_path.exists():
        print(
            f"ERROR: Firmware path does not exist: {args.firmware_path}",
            file=sys.stderr,
        )
        return 1

    runner = DockerQEMURunner(docker_image=args.docker_image)
    return runner.run(
        firmware_path=args.firmware_path,
        timeout=args.timeout,
        interrupt_regex=args.interrupt_regex,
        interactive=args.interactive,
        output_file=args.output_file,
        machine=args.machine,
        skip_pull=args.skip_pull,
    )


if __name__ == "__main__":
    sys.exit(main())
