# cspell:ignore envvars
"""Herein lies the base class for all interaction with ansible-runner.

Attributes common to all ansible-runner calls are defined within the base class.
"""

from __future__ import annotations

import logging
import os
import shutil
import sys
import tempfile

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any


if TYPE_CHECKING:
    from ansible_runner import Runner


class Base:
    """Base class for ansible-runner calls."""

    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-instance-attributes
    def __init__(
        self,
        private_data_dir: str | None = None,
        container_engine: str | None = None,
        execution_environment: bool | None = False,
        execution_environment_image: str | None = None,
        navigator_mode: str | None = None,
        container_volume_mounts: list[str] | None = None,
        container_options: list[str] | None = None,
        container_workdir: str | None = None,
        set_environment_variable: dict[str, str] | None = None,
        pass_environment_variable: list[str] | None = None,
        host_cwd: str | None = None,
        rotate_artifacts: int | None = None,
        timeout: int | None = None,
    ) -> None:
        """Handle the common argument for the ansible-runner interface class.

        Args:
            container_engine: Container engine used to isolate execution
            container_options: List of container options to pass to
                execution engine
            execution_environment: Boolean argument that enable
                execution environment support
            execution_environment_image: Container image to use when
                running an command
            navigator_mode: Valid value is either ``stdout`` or
                ``interactive``. If value is set to ``stdout`` passed
                the ``stdin`` of current running process is passed to
                ``ansible-runner`` which enables receiving command line
                prompts after the executing the command. If value is set
                to ``interactive`` the ``ansible-navigator` will run
                using text user interface (TUI).
            container_volume_mounts: List of bind mounts in the form
                ``host_dir:/container_dir:labels``
            container_workdir: The working directory within the
                container
            host_cwd: The current local working directory. If value of
                execution_environment is set to True this path will be
                volume mounted within the execution environment.
            set_environment_variable: Dict of user requested environment
                variables to set
            private_data_dir: The directory containing all runner
                metadata needed to invoke the runner module. Output
                artifacts will also be stored here for later
                consumption.
            pass_environment_variable: List of user requested
                environment variables to pass
            rotate_artifacts: Keep at most n artifact directories
            timeout: The timeout value in seconds that will be passed to
                either ``pexpect`` of ``subprocess`` invocation (based
                on ``runner_mode`` selected) while executing command. It
                the timeout is triggered it will force cancel the
                execution.
        """
        self._logger = logging.getLogger(__name__)

        self._set_private_data_directory(private_data_dir)
        self._ce = container_engine
        self._ee = execution_environment
        self._eei = execution_environment_image
        self._navigator_mode = navigator_mode
        self._set_environment_variable: dict[str, Any] = (
            set_environment_variable if isinstance(set_environment_variable, dict) else {}
        )
        self._pass_environment_variable: list[str] = (
            pass_environment_variable if isinstance(pass_environment_variable, list) else []
        )
        self._host_cwd = host_cwd
        self._rotate_artifacts = rotate_artifacts if isinstance(rotate_artifacts, int) else None
        self._timeout = timeout if isinstance(timeout, int) else None
        self.ansible_runner_instance: Runner
        self.cancelled: bool = False
        self.finished: bool = False
        self.status: str | None = None
        self._runner_args: dict[str, Any] = {}

        container_options = container_options or []

        # when the ce is podman, set the container user to root unless already specified
        if self._ce == "podman":
            has_user_flag = any(
                opt.startswith(("--user", "-u=", "-u ")) or opt == "-u" for opt in container_options
            )
            if not has_user_flag:
                container_options.append("--user=root")

        # Fix SSH agent socket when running Docker on macOS.
        if sys.platform == "darwin" and self._ce == "docker":
            ssh_agent_socket_opts = [
                # Docker exposes this proxy socket for SSH agent on macOS.
                "--volume=/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock:ro",
                "--env=SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock",
            ]

            # Prepend to existing container options to allow overriding this fix.
            container_options = ssh_agent_socket_opts + container_options

        # On macOS Tahoe (>26.3), the launchd SSH agent socket moved from
        # /private/tmp to /var/run. Since /var/run is a symlink to /private/var/run,
        # container runtimes require the resolved real path when building volume mount
        # arguments. ansible-runner normalises mount paths with os.path.abspath(), which
        # does not resolve symlinks, causing the container engine to fail with:
        #   Error: statfs /var/run/com.apple.launchd.xxxxxxxxx: no such file or directory
        # Resolving SSH_AUTH_SOCK to its real path here — before ansible-runner reads it
        # from os.environ in _handle_automounts() — ensures the correct path is passed to
        # the container engine regardless of which CE is in use.
        if sys.platform == "darwin" and "SSH_AUTH_SOCK" in os.environ:
            _real_sock = os.path.realpath(os.environ["SSH_AUTH_SOCK"])
            if _real_sock != os.environ["SSH_AUTH_SOCK"]:
                self._logger.debug(
                    "macOS SSH_AUTH_SOCK symlink resolved: %s -> %s",
                    os.environ["SSH_AUTH_SOCK"],
                    _real_sock,
                )
                os.environ["SSH_AUTH_SOCK"] = _real_sock

        if self._ee:
            self._runner_args.update(
                {
                    "container_image": self._eei,
                    "process_isolation_executable": self._ce,
                    "process_isolation": True,
                    "container_volume_mounts": container_volume_mounts,
                    "container_options": container_options,
                    "container_workdir": container_workdir,
                },
            )
        self._runner_args.update(
            {
                "private_data_dir": self._private_data_dir,
                "json_mode": True,
                "quiet": True,
                "cancel_callback": self.runner_cancelled_callback,
                "finished_callback": self.runner_finished_callback,
                "timeout": self._timeout,
            },
        )
        if self._rotate_artifacts is not None:
            self._runner_args["rotate_artifacts"] = self._rotate_artifacts

        self._add_env_vars_to_args()

        if self._host_cwd:
            # ensure the CWD ends with a trailing slash
            host_cwd = f"{Path(self._host_cwd) / ''}"
            self._runner_args["host_cwd"] = host_cwd

        if self._navigator_mode == "stdout":
            self._runner_args.update(
                {
                    "input_fd": sys.stdin,
                    "output_fd": sys.stdout,
                    "error_fd": sys.stderr,
                },
            )

    def __del__(self) -> None:
        """Drop the private_data_dir if it is temporary."""
        if (
            self._private_data_dir_is_tmp
            and self._private_data_dir
            and Path(self._private_data_dir).exists()
        ):
            self._logger.debug(
                "delete temporary ansible-runner private_data_dir at path %s",
                self._private_data_dir,
            )
            shutil.rmtree(self._private_data_dir, ignore_errors=True)

    @staticmethod
    def _generate_tmp_directory() -> str:
        """Generate a tmp directory for artifacts.

        Returns:
            Temporary directory path
        """
        return tempfile.mkdtemp(prefix="ansible-navigator_")

    def _set_private_data_directory(self, provided: str | None) -> None:
        """Set the private data directory to the provided, the username, or a uuid.

        Args:
            provided: Data directory path
        """
        if isinstance(provided, str):
            if os.access(provided, os.W_OK | os.R_OK | os.X_OK):
                private_data_directory = provided
                source = "user provided"
            else:
                self._logger.debug("Provided private data dir `%s` was not user writable", provided)
                private_data_directory = self._generate_tmp_directory()
                source = "user provided, but changed to tmp location due to permissions"
        else:
            private_data_directory = self._generate_tmp_directory()
            source = "not user provided, used tmp location"

        self._private_data_dir_is_tmp = source != "user provided"
        self._private_data_dir = private_data_directory
        self._logger.debug("private data dir %s: %s", source, self._private_data_dir)

    def runner_cancelled_callback(self) -> bool:
        """Check by runner to see if it should cancel.

        Returns:
            Boolean cancelled value
        """
        return self.cancelled

    def runner_finished_callback(self, runner: Runner) -> None:
        """Call when runner finishes.

        Args:
            runner: A runner instance
        """
        self.status = runner.status
        self.finished = True

    def _add_env_vars_to_args(self) -> None:
        """Add environment variables to runner args."""
        self._runner_args["envvars"] = {
            k: v for k, v in os.environ.items() if k.startswith("ANSIBLE_")
        }
        self._runner_args["envvars"].update(self._set_environment_variable)
        for env_var in self._pass_environment_variable:
            value = os.environ.get(env_var)
            if value is None:
                self._logger.warning(
                    "Pass through environment variable `%s` not currently set, discarded",
                    env_var,
                )
            else:
                self._runner_args["envvars"][env_var] = value
