# Copyright (c) 2022 Felix Fontein <felix@fontein.de>
# Copyright 2016 Red Hat | Ansible
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!

from __future__ import annotations

import json
import traceback
import typing as t

from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.common.text.formatters import human_to_bytes

from ansible_collections.community.docker.plugins.module_utils._api.errors import (
    APIError,
    DockerException,
    NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
    convert_port_bindings,
    normalize_links,
)
from ansible_collections.community.docker.plugins.module_utils._common_api import (
    AnsibleDockerClient,
    RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._module_container.base import (
    _DEFAULT_IP_REPLACEMENT_STRING,
    OPTION_AUTO_REMOVE,
    OPTION_BLKIO_WEIGHT,
    OPTION_CAP_DROP,
    OPTION_CAPABILITIES,
    OPTION_CGROUP_NS_MODE,
    OPTION_CGROUP_PARENT,
    OPTION_COMMAND,
    OPTION_CPU_PERIOD,
    OPTION_CPU_QUOTA,
    OPTION_CPU_SHARES,
    OPTION_CPUS,
    OPTION_CPUSET_CPUS,
    OPTION_CPUSET_MEMS,
    OPTION_DETACH_INTERACTIVE,
    OPTION_DEVICE_CGROUP_RULES,
    OPTION_DEVICE_READ_BPS,
    OPTION_DEVICE_READ_IOPS,
    OPTION_DEVICE_REQUESTS,
    OPTION_DEVICE_WRITE_BPS,
    OPTION_DEVICE_WRITE_IOPS,
    OPTION_DEVICES,
    OPTION_DNS_OPTS,
    OPTION_DNS_SEARCH_DOMAINS,
    OPTION_DNS_SERVERS,
    OPTION_DOMAINNAME,
    OPTION_ENTRYPOINT,
    OPTION_ENVIRONMENT,
    OPTION_ETC_HOSTS,
    OPTION_GROUPS,
    OPTION_HEALTHCHECK,
    OPTION_HOSTNAME,
    OPTION_IMAGE,
    OPTION_INIT,
    OPTION_IPC_MODE,
    OPTION_KERNEL_MEMORY,
    OPTION_LABELS,
    OPTION_LINKS,
    OPTION_LOG_DRIVER_OPTIONS,
    OPTION_MAC_ADDRESS,
    OPTION_MEMORY,
    OPTION_MEMORY_RESERVATION,
    OPTION_MEMORY_SWAP,
    OPTION_MEMORY_SWAPPINESS,
    OPTION_MOUNTS_VOLUMES,
    OPTION_NETWORK,
    OPTION_OOM_KILLER,
    OPTION_OOM_SCORE_ADJ,
    OPTION_PID_MODE,
    OPTION_PIDS_LIMIT,
    OPTION_PLATFORM,
    OPTION_PORTS,
    OPTION_PRIVILEGED,
    OPTION_READ_ONLY,
    OPTION_RESTART_POLICY,
    OPTION_RUNTIME,
    OPTION_SECURITY_OPTS,
    OPTION_SHM_SIZE,
    OPTION_STOP_SIGNAL,
    OPTION_STOP_TIMEOUT,
    OPTION_STORAGE_OPTS,
    OPTION_SYSCTLS,
    OPTION_TMPFS,
    OPTION_TTY,
    OPTION_ULIMITS,
    OPTION_USER,
    OPTION_USERNS_MODE,
    OPTION_UTS,
    OPTION_VOLUME_DRIVER,
    OPTION_VOLUMES_FROM,
    OPTION_WORKING_DIR,
    OPTIONS,
    Engine,
    EngineDriver,
    _is_volume_permissions,
)
from ansible_collections.community.docker.plugins.module_utils._platform import (
    compose_platform_string,
    normalize_platform_string,
)
from ansible_collections.community.docker.plugins.module_utils._util import (
    normalize_healthcheck_test,
    omit_none_from_dict,
)
from ansible_collections.community.docker.plugins.module_utils._version import (
    LooseVersion,
)

if t.TYPE_CHECKING:
    from collections.abc import Callable, Sequence

    from ansible.module_utils.basic import AnsibleModule

    from .base import Option, OptionGroup

    Sentry = object


_SENTRY: Sentry = object()


class DockerAPIEngineDriver(EngineDriver[AnsibleDockerClient]):
    name = "docker_api"

    def setup(
        self,
        argument_spec: dict[str, t.Any],
        mutually_exclusive: Sequence[Sequence[str]] | None = None,
        required_together: Sequence[Sequence[str]] | None = None,
        required_one_of: Sequence[Sequence[str]] | None = None,
        required_if: (
            Sequence[
                tuple[str, t.Any, Sequence[str]]
                | tuple[str, t.Any, Sequence[str], bool]
            ]
            | None
        ) = None,
        required_by: dict[str, Sequence[str]] | None = None,
    ) -> tuple[AnsibleModule, list[OptionGroup], AnsibleDockerClient]:
        argument_spec = dict(argument_spec or {})
        mutually_exclusive = list(mutually_exclusive or [])
        required_together = list(required_together or [])
        required_one_of = list(required_one_of or [])
        required_if = list(required_if or [])
        required_by = dict(required_by or {})

        active_options = []
        option_minimal_versions = {}
        for options in OPTIONS:
            if not options.supports_engine(self.name):
                continue

            mutually_exclusive.extend(options.ansible_mutually_exclusive)
            required_together.extend(options.ansible_required_together)
            required_one_of.extend(options.ansible_required_one_of)
            required_if.extend(options.ansible_required_if)
            required_by.update(options.ansible_required_by)
            argument_spec.update(options.argument_spec)

            engine = options.get_engine(self.name)
            if engine.min_api_version is not None:
                for option in options.options:
                    if not option.not_an_ansible_option:
                        option_minimal_versions[option.name] = {
                            "docker_api_version": engine.min_api_version
                        }
            if engine.extra_option_minimal_versions:
                option_minimal_versions.update(engine.extra_option_minimal_versions)

            active_options.append(options)

        client = AnsibleDockerClient(
            argument_spec=argument_spec,
            mutually_exclusive=mutually_exclusive,
            required_together=required_together,
            required_one_of=required_one_of,
            required_if=required_if,
            required_by=required_by,
            option_minimal_versions=option_minimal_versions,
            supports_check_mode=True,
        )

        return client.module, active_options, client

    def get_host_info(self, client: AnsibleDockerClient) -> dict[str, t.Any]:
        return client.info()

    def get_api_version(self, client: AnsibleDockerClient) -> LooseVersion:
        return client.docker_api_version

    def get_container_id(self, container: dict[str, t.Any]) -> str:
        return container["Id"]

    def get_image_from_container(self, container: dict[str, t.Any]) -> str:
        return container["Image"]

    def get_image_name_from_container(self, container: dict[str, t.Any]) -> str | None:
        return container["Config"].get("Image")

    def is_container_removing(self, container: dict[str, t.Any]) -> bool:
        if container.get("State"):
            return container["State"].get("Status") == "removing"
        return False

    def is_container_running(self, container: dict[str, t.Any]) -> bool:
        return bool(
            container.get("State")
            and container["State"].get("Running")
            and not container["State"].get("Ghost", False)
        )

    def is_container_paused(self, container: dict[str, t.Any]) -> bool:
        if container.get("State"):
            return container["State"].get("Paused", False)
        return False

    def inspect_container_by_name(
        self, client: AnsibleDockerClient, container_name: str
    ) -> dict[str, t.Any] | None:
        return client.get_container(container_name)

    def inspect_container_by_id(
        self, client: AnsibleDockerClient, container_id: str
    ) -> dict[str, t.Any] | None:
        return client.get_container_by_id(container_id)

    def inspect_image_by_id(
        self, client: AnsibleDockerClient, image_id: str
    ) -> dict[str, t.Any] | None:
        return client.find_image_by_id(image_id, accept_missing_image=True)

    def inspect_image_by_name(
        self, client: AnsibleDockerClient, repository: str, tag: str
    ) -> dict[str, t.Any] | None:
        return client.find_image(repository, tag)

    def pull_image(
        self,
        client: AnsibleDockerClient,
        repository: str,
        tag: str,
        image_platform: str | None = None,
    ) -> tuple[dict[str, t.Any] | None, bool]:
        return client.pull_image(repository, tag, image_platform=image_platform)

    def pause_container(self, client: AnsibleDockerClient, container_id: str) -> None:
        client.post_call("/containers/{0}/pause", container_id)

    def unpause_container(self, client: AnsibleDockerClient, container_id: str) -> None:
        client.post_call("/containers/{0}/unpause", container_id)

    def disconnect_container_from_network(
        self, client: AnsibleDockerClient, container_id: str, network_id: str
    ) -> None:
        client.post_json(
            "/networks/{0}/disconnect", network_id, data={"Container": container_id}
        )

    def _create_endpoint_config(self, parameters: dict[str, t.Any]) -> dict[str, t.Any]:
        parameters = parameters.copy()
        params = {}
        for para, dest_para in {
            "ipv4_address": "IPv4Address",
            "ipv6_address": "IPv6Address",
            "links": "Links",
            "aliases": "Aliases",
            "mac_address": "MacAddress",
            "driver_opts": "DriverOpts",
        }.items():
            value = parameters.pop(para, None)
            if value:
                if para == "links":
                    value = normalize_links(value)
                elif para == "driver_opts":
                    # Ensure driver_opts values are strings
                    for key, val in value.items():
                        if not isinstance(val, str):
                            raise ValueError(
                                f"driver_opts values must be strings, got {type(val).__name__} for key '{key}'"
                            )
                params[dest_para] = value
        for para, dest_para in {
            "gw_priority": "GwPriority",
        }.items():
            value = parameters.pop(para, None)
            if value is not None:
                params[dest_para] = value
        if parameters:
            ups = ", ".join([f'"{p}"' for p in sorted(parameters)])
            raise ValueError(
                f"Unknown parameter(s) for connect_container_to_network for Docker API driver: {ups}"
            )
        ipam_config = {}
        for param in ("IPv4Address", "IPv6Address"):
            if param in params:
                ipam_config[param] = params.pop(param)
        if ipam_config:
            params["IPAMConfig"] = ipam_config
        return params

    def connect_container_to_network(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        network_id: str,
        parameters: dict[str, t.Any] | None = None,
    ) -> None:
        parameters = (parameters or {}).copy()
        params = self._create_endpoint_config(parameters or {})
        data = {
            "Container": container_id,
            "EndpointConfig": params,
        }
        client.post_json("/networks/{0}/connect", network_id, data=data)

    def create_container_supports_more_than_one_network(
        self, client: AnsibleDockerClient
    ) -> bool:
        return client.docker_api_version >= LooseVersion("1.44")

    def create_container(
        self,
        client: AnsibleDockerClient,
        container_name: str,
        create_parameters: dict[str, t.Any],
        networks: dict[str, dict[str, t.Any]] | None = None,
    ) -> str:
        params = {"name": container_name}
        if "platform" in create_parameters:
            params["platform"] = create_parameters.pop("platform")
        if networks is not None:
            create_parameters = create_parameters.copy()
            create_parameters["NetworkingConfig"] = {
                "EndpointsConfig": dict(
                    (network, self._create_endpoint_config(network_params))
                    for network, network_params in networks.items()
                )
            }
        new_container = client.post_json_to_json(
            "/containers/create", data=create_parameters, params=params
        )
        client.report_warnings(new_container)
        return new_container["Id"]

    def start_container(self, client: AnsibleDockerClient, container_id: str) -> None:
        client.post_json("/containers/{0}/start", container_id)

    def wait_for_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        timeout: int | float | None = None,
    ) -> int | None:
        return client.post_json_to_json(
            "/containers/{0}/wait", container_id, timeout=timeout
        )["StatusCode"]

    def get_container_output(
        self, client: AnsibleDockerClient, container_id: str
    ) -> tuple[bytes, t.Literal[True]] | tuple[str, t.Literal[False]]:
        config = client.get_json("/containers/{0}/json", container_id)
        logging_driver = config["HostConfig"]["LogConfig"]["Type"]
        if logging_driver in ("json-file", "journald", "local"):
            params = {
                "stderr": 1,
                "stdout": 1,
                "timestamps": 0,
                "follow": 0,
                "tail": "all",
            }
            res = client._get(
                client._url("/containers/{0}/logs", container_id), params=params
            )
            output = client._get_result_tty(False, res, config["Config"]["Tty"])
            return output, True
        return f"Result logged using `{logging_driver}` driver", False

    def update_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        update_parameters: dict[str, t.Any],
    ) -> None:
        result = client.post_json_to_json(
            "/containers/{0}/update", container_id, data=update_parameters
        )
        client.report_warnings(result)

    def restart_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        timeout: int | float | None = None,
    ) -> None:
        client_timeout: int | float | None = client.timeout
        if client_timeout is not None:
            client_timeout += timeout or 10
        client.post_call(
            "/containers/{0}/restart",
            container_id,
            params={"t": timeout},
            timeout=client_timeout,
        )

    def kill_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        kill_signal: str | None = None,
    ) -> None:
        params = {}
        if kill_signal is not None:
            params["signal"] = kill_signal
        client.post_call("/containers/{0}/kill", container_id, params=params)

    def stop_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        timeout: int | float | None = None,
    ) -> None:
        if timeout:
            params = {"t": timeout}
        else:
            params = {}
            timeout = 10
        client_timeout = client.timeout
        if client_timeout is not None:
            client_timeout += timeout
        count = 0
        while True:
            try:
                client.post_call(
                    "/containers/{0}/stop",
                    container_id,
                    params=params,
                    timeout=client_timeout,
                )
            except APIError as exc:
                if (
                    "Unpause the container before stopping or killing"
                    in exc.explanation
                ):
                    # New docker daemon versions do not allow containers to be removed
                    # if they are paused. Make sure we do not end up in an infinite loop.
                    if count == 3:
                        raise RuntimeError(
                            f"{exc} [tried to unpause three times]"
                        ) from exc
                    count += 1
                    # Unpause
                    try:
                        self.unpause_container(client, container_id)
                    except Exception as exc2:
                        raise RuntimeError(f"{exc2} [while unpausing]") from exc2
                    # Now try again
                    continue
                raise
            # We only loop when explicitly requested by 'continue'
            break

    def remove_container(
        self,
        client: AnsibleDockerClient,
        container_id: str,
        remove_volumes: bool = False,
        link: bool = False,
        force: bool = False,
    ) -> None:
        params = {"v": remove_volumes, "link": link, "force": force}
        count = 0
        while True:
            try:
                client.delete_call("/containers/{0}", container_id, params=params)
            except NotFound:
                pass
            except APIError as exc:
                if (
                    "Unpause the container before stopping or killing"
                    in exc.explanation
                ):
                    # New docker daemon versions do not allow containers to be removed
                    # if they are paused. Make sure we do not end up in an infinite loop.
                    if count == 3:
                        raise RuntimeError(
                            f"{exc} [tried to unpause three times]"
                        ) from exc
                    count += 1
                    # Unpause
                    try:
                        self.unpause_container(client, container_id)
                    except Exception as exc2:
                        raise RuntimeError(f"{exc2} [while unpausing]") from exc2
                    # Now try again
                    continue
                if (
                    "removal of container " in exc.explanation
                    and " is already in progress" in exc.explanation
                ):
                    pass
                else:
                    raise
            # We only loop when explicitly requested by 'continue'
            break

    def run(self, runner: Callable[[], None], client: AnsibleDockerClient) -> None:
        try:
            runner()
        except DockerException as e:
            client.fail(
                f"An unexpected Docker error occurred: {e}",
                exception=traceback.format_exc(),
            )
        except RequestException as e:
            client.fail(
                f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
                exception=traceback.format_exc(),
            )


class DockerAPIEngine(Engine[AnsibleDockerClient]):
    def get_value(
        self,
        module: AnsibleModule,
        container: dict[str, t.Any],
        api_version: LooseVersion,
        options: list[Option],
        image: dict[str, t.Any] | None,
        host_info: dict[str, t.Any] | None,
    ) -> dict[str, t.Any]:
        return self._get_value(
            module, container, api_version, options, image, host_info
        )

    def compare_value(
        self, option: Option, param_value: t.Any, container_value: t.Any
    ) -> bool:
        if self._compare_value is not None:
            return self._compare_value(option, param_value, container_value)
        return super().compare_value(option, param_value, container_value)

    def set_value(
        self,
        module: AnsibleModule,
        data: dict[str, t.Any],
        api_version: LooseVersion,
        options: list[Option],
        values: dict[str, t.Any],
    ) -> None:
        if self._set_value is not None:
            self._set_value(module, data, api_version, options, values)

    def get_expected_values(
        self,
        module: AnsibleModule,
        client: AnsibleDockerClient,
        api_version: LooseVersion,
        options: list[Option],
        image: dict[str, t.Any] | None,
        values: dict[str, t.Any],
        host_info: dict[str, t.Any] | None,
    ) -> dict[str, t.Any]:
        if self._get_expected_values is None:
            return values
        return self._get_expected_values(
            module, client, api_version, options, image, values, host_info
        )

    def ignore_mismatching_result(
        self,
        module: AnsibleModule,
        client: AnsibleDockerClient,
        api_version: LooseVersion,
        option: Option,
        image: dict[str, t.Any] | None,
        container_value: t.Any,
        expected_value: t.Any,
        host_info: dict[str, t.Any] | None,
    ) -> bool:
        if self._ignore_mismatching_result is None:
            return False
        return self._ignore_mismatching_result(
            module,
            client,
            api_version,
            option,
            image,
            container_value,
            expected_value,
            host_info,
        )

    def preprocess_value(
        self,
        module: AnsibleModule,
        client: AnsibleDockerClient,
        api_version: LooseVersion,
        options: list[Option],
        values: dict[str, t.Any],
    ) -> dict[str, t.Any]:
        if self._preprocess_value is None:
            return values
        return self._preprocess_value(module, client, api_version, options, values)

    def update_value(
        self,
        module: AnsibleModule,
        data: dict[str, t.Any],
        api_version: LooseVersion,
        options: list[Option],
        values: dict[str, t.Any],
    ) -> None:
        if self._update_value is not None:
            self._update_value(module, data, api_version, options, values)

    def can_set_value(self, api_version: LooseVersion) -> bool:
        if self._can_set_value is None:
            return self._set_value is not None
        return self._can_set_value(api_version)

    def can_update_value(self, api_version: LooseVersion) -> bool:
        if self._can_update_value is None:
            return self._update_value is not None
        return self._can_update_value(api_version)

    def needs_container_image(self, values: dict[str, t.Any]) -> bool:
        if self._needs_container_image is None:
            return False
        return self._needs_container_image(values)

    def needs_host_info(self, values: dict[str, t.Any]) -> bool:
        if self._needs_host_info is None:
            return False
        return self._needs_host_info(values)

    def __init__(
        self,
        get_value: Callable[
            [
                AnsibleModule,
                dict[str, t.Any],
                LooseVersion,
                list[Option],
                dict[str, t.Any] | None,
                dict[str, t.Any] | None,
            ],
            dict[str, t.Any],
        ],
        preprocess_value: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any],
                ],
                dict[str, t.Any],
            ]
            | None
        ) = None,
        get_expected_values: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any] | None,
                    dict[str, t.Any],
                    dict[str, t.Any] | None,
                ],
                dict[str, t.Any],
            ]
            | None
        ) = None,
        ignore_mismatching_result: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    Option,
                    dict[str, t.Any] | None,
                    t.Any,
                    t.Any,
                    dict[str, t.Any] | None,
                ],
                bool,
            ]
            | None
        ) = None,
        set_value: (
            Callable[
                [
                    AnsibleModule,
                    dict[str, t.Any],
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any],
                ],
                None,
            ]
            | None
        ) = None,
        update_value: (
            Callable[
                [
                    AnsibleModule,
                    dict[str, t.Any],
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any],
                ],
                None,
            ]
            | None
        ) = None,
        can_set_value: Callable[[LooseVersion], bool] | None = None,
        can_update_value: Callable[[LooseVersion], bool] | None = None,
        min_api_version: str | None = None,
        compare_value: Callable[[Option, t.Any, t.Any], bool] | None = None,
        needs_container_image: Callable[[dict[str, t.Any]], bool] | None = None,
        needs_host_info: Callable[[dict[str, t.Any]], bool] | None = None,
        extra_option_minimal_versions: dict[str, dict[str, t.Any]] | None = None,
    ):
        self.min_api_version = min_api_version
        self.min_api_version_obj = (
            None if min_api_version is None else LooseVersion(min_api_version)
        )
        self.extra_option_minimal_versions = extra_option_minimal_versions
        self._get_value = get_value
        self._compare_value = compare_value
        self._set_value = set_value
        self._get_expected_values = get_expected_values
        self._ignore_mismatching_result = ignore_mismatching_result
        self._preprocess_value = preprocess_value
        self._update_value = update_value
        self._can_set_value = can_set_value
        self._can_update_value = can_update_value
        self._needs_container_image = needs_container_image
        self._needs_host_info = needs_host_info

    @classmethod
    def config_value(
        cls,
        config_name: str,
        postprocess_for_get: (
            Callable[[AnsibleModule, LooseVersion, t.Any, Sentry], t.Any | Sentry]
            | None
        ) = None,
        preprocess_for_set: (
            Callable[[AnsibleModule, LooseVersion, t.Any], t.Any] | None
        ) = None,
        get_expected_value: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    dict[str, t.Any] | None,
                    t.Any,
                    Sentry,
                ],
                t.Any | Sentry,
            ]
            | None
        ) = None,
        ignore_mismatching_result: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    Option,
                    dict[str, t.Any] | None,
                    t.Any,
                    t.Any,
                    dict[str, t.Any] | None,
                ],
                bool,
            ]
            | None
        ) = None,
        min_api_version: str | None = None,
        preprocess_value: (
            Callable[[AnsibleModule, AnsibleDockerClient, LooseVersion, t.Any], t.Any]
            | None
        ) = None,
        update_parameter: str | None = None,
        extra_option_minimal_versions: dict[str, dict[str, t.Any]] | None = None,
    ) -> DockerAPIEngine:
        def preprocess_value_(
            module: AnsibleModule,
            client: AnsibleDockerClient,
            api_version: LooseVersion,
            options: list[Option],
            values: dict[str, t.Any],
        ) -> dict[str, t.Any]:
            if len(options) != 1:
                raise AssertionError(
                    "config_value can only be used for a single option"
                )
            if preprocess_value is not None and options[0].name in values:
                value = preprocess_value(
                    module, client, api_version, values[options[0].name]
                )
                if value is None:
                    del values[options[0].name]
                else:
                    values[options[0].name] = value
            return values

        def get_value(
            module: AnsibleModule,
            container: dict[str, t.Any],
            api_version: LooseVersion,
            options: list[Option],
            image: dict[str, t.Any] | None,
            host_info: dict[str, t.Any] | None,
        ) -> dict[str, t.Any]:
            if len(options) != 1:
                raise AssertionError(
                    "config_value can only be used for a single option"
                )
            value = container["Config"].get(config_name, _SENTRY)
            if postprocess_for_get:
                value = postprocess_for_get(module, api_version, value, _SENTRY)
            if value is _SENTRY:
                return {}
            return {options[0].name: value}

        get_expected_values_: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any] | None,
                    dict[str, t.Any],
                    dict[str, t.Any] | None,
                ],
                dict[str, t.Any],
            ]
            | None
        ) = None
        if get_expected_value:

            def get_expected_values_(  # noqa: F811
                module: AnsibleModule,
                client: AnsibleDockerClient,
                api_version: LooseVersion,
                options: list[Option],
                image: dict[str, t.Any] | None,
                values: dict[str, t.Any],
                host_info: dict[str, t.Any] | None,
            ) -> dict[str, t.Any]:
                if len(options) != 1:
                    raise AssertionError(
                        "host_config_value can only be used for a single option"
                    )
                value = values.get(options[0].name, _SENTRY)
                value = get_expected_value(
                    module, client, api_version, image, value, _SENTRY
                )
                if value is _SENTRY:
                    return values
                return {options[0].name: value}

        def set_value(
            module: AnsibleModule,
            data: dict[str, t.Any],
            api_version: LooseVersion,
            options: list[Option],
            values: dict[str, t.Any],
        ) -> None:
            if len(options) != 1:
                raise AssertionError(
                    "config_value can only be used for a single option"
                )
            if options[0].name not in values:
                return
            value = values[options[0].name]
            if preprocess_for_set:
                value = preprocess_for_set(module, api_version, value)
            data[config_name] = value

        update_value: (
            Callable[
                [
                    AnsibleModule,
                    dict[str, t.Any],
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any],
                ],
                None,
            ]
            | None
        ) = None
        if update_parameter:

            def update_value(  # noqa: F811
                module: AnsibleModule,
                data: dict[str, t.Any],
                api_version: LooseVersion,
                options: list[Option],
                values: dict[str, t.Any],
            ) -> None:
                if len(options) != 1:
                    raise AssertionError(
                        "update_parameter can only be used for a single option"
                    )
                if options[0].name not in values:
                    return
                value = values[options[0].name]
                if preprocess_for_set:
                    value = preprocess_for_set(module, api_version, value)
                data[update_parameter] = value

        return cls(
            get_value=get_value,
            preprocess_value=preprocess_value_,
            get_expected_values=get_expected_values_,
            ignore_mismatching_result=ignore_mismatching_result,
            set_value=set_value,
            min_api_version=min_api_version,
            update_value=update_value,
            extra_option_minimal_versions=extra_option_minimal_versions,
        )

    @classmethod
    def host_config_value(
        cls,
        host_config_name: str,
        postprocess_for_get: (
            Callable[[AnsibleModule, LooseVersion, t.Any, Sentry], t.Any | Sentry]
            | None
        ) = None,
        preprocess_for_set: (
            Callable[[AnsibleModule, LooseVersion, t.Any], t.Any] | None
        ) = None,
        get_expected_value: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    dict[str, t.Any] | None,
                    t.Any,
                    Sentry,
                ],
                t.Any | Sentry,
            ]
            | None
        ) = None,
        ignore_mismatching_result: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    Option,
                    dict[str, t.Any] | None,
                    t.Any,
                    t.Any,
                    dict[str, t.Any] | None,
                ],
                bool,
            ]
            | None
        ) = None,
        min_api_version: str | None = None,
        preprocess_value: (
            Callable[[AnsibleModule, AnsibleDockerClient, LooseVersion, t.Any], t.Any]
            | None
        ) = None,
        update_parameter: str | None = None,
        extra_option_minimal_versions: dict[str, dict[str, t.Any]] | None = None,
    ) -> DockerAPIEngine:
        def preprocess_value_(
            module: AnsibleModule,
            client: AnsibleDockerClient,
            api_version: LooseVersion,
            options: list[Option],
            values: dict[str, t.Any],
        ) -> dict[str, t.Any]:
            if len(options) != 1:
                raise AssertionError(
                    "host_config_value can only be used for a single option"
                )
            if preprocess_value is not None and options[0].name in values:
                value = preprocess_value(
                    module, client, api_version, values[options[0].name]
                )
                if value is None:
                    del values[options[0].name]
                else:
                    values[options[0].name] = value
            return values

        def get_value(
            module: AnsibleModule,
            container: dict[str, t.Any],
            api_version: LooseVersion,
            options: list[Option],
            image: dict[str, t.Any] | None,
            host_info: dict[str, t.Any] | None,
        ) -> dict[str, t.Any]:
            if len(options) != 1:
                raise AssertionError(
                    "host_config_value can only be used for a single option"
                )
            value = container["HostConfig"].get(host_config_name, _SENTRY)
            if postprocess_for_get:
                value = postprocess_for_get(module, api_version, value, _SENTRY)
            if value is _SENTRY:
                return {}
            return {options[0].name: value}

        get_expected_values_: (
            Callable[
                [
                    AnsibleModule,
                    AnsibleDockerClient,
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any] | None,
                    dict[str, t.Any],
                    dict[str, t.Any] | None,
                ],
                dict[str, t.Any],
            ]
            | None
        ) = None
        if get_expected_value:

            def get_expected_values_(  # noqa: F811
                module: AnsibleModule,
                client: AnsibleDockerClient,
                api_version: LooseVersion,
                options: list[Option],
                image: dict[str, t.Any] | None,
                values: dict[str, t.Any],
                host_info: dict[str, t.Any] | None,
            ) -> dict[str, t.Any]:
                if len(options) != 1:
                    raise AssertionError(
                        "host_config_value can only be used for a single option"
                    )
                value = values.get(options[0].name, _SENTRY)
                value = get_expected_value(
                    module, client, api_version, image, value, _SENTRY
                )
                if value is _SENTRY:
                    return values
                return {options[0].name: value}

        def set_value(
            module: AnsibleModule,
            data: dict[str, t.Any],
            api_version: LooseVersion,
            options: list[Option],
            values: dict[str, t.Any],
        ) -> None:
            if len(options) != 1:
                raise AssertionError(
                    "host_config_value can only be used for a single option"
                )
            if options[0].name not in values:
                return
            if "HostConfig" not in data:
                data["HostConfig"] = {}
            value = values[options[0].name]
            if preprocess_for_set:
                value = preprocess_for_set(module, api_version, value)
            data["HostConfig"][host_config_name] = value

        update_value: (
            Callable[
                [
                    AnsibleModule,
                    dict[str, t.Any],
                    LooseVersion,
                    list[Option],
                    dict[str, t.Any],
                ],
                None,
            ]
            | None
        ) = None
        if update_parameter:

            def update_value(  # noqa: F811
                module: AnsibleModule,
                data: dict[str, t.Any],
                api_version: LooseVersion,
                options: list[Option],
                values: dict[str, t.Any],
            ) -> None:
                if len(options) != 1:
                    raise AssertionError(
                        "update_parameter can only be used for a single option"
                    )
                if options[0].name not in values:
                    return
                value = values[options[0].name]
                if preprocess_for_set:
                    value = preprocess_for_set(module, api_version, value)
                data[update_parameter] = value

        return cls(
            get_value=get_value,
            preprocess_value=preprocess_value_,
            get_expected_values=get_expected_values_,
            ignore_mismatching_result=ignore_mismatching_result,
            set_value=set_value,
            min_api_version=min_api_version,
            update_value=update_value,
            extra_option_minimal_versions=extra_option_minimal_versions,
        )


def _normalize_port(port: str) -> str:
    if "/" not in port:
        return port + "/tcp"
    return port


def _get_default_host_ip(module: AnsibleModule, client: AnsibleDockerClient) -> str:
    if module.params["default_host_ip"] is not None:
        return module.params["default_host_ip"]
    ip = "0.0.0.0"
    for network_data in module.params["networks"] or []:
        if network_data.get("name"):
            network = client.get_network(network_data["name"])
            if network is None:
                client.fail(
                    f"Cannot inspect the network '{network_data['name']}' to determine the default IP",
                )
            if network.get("Driver") == "bridge" and network.get("Options", {}).get(
                "com.docker.network.bridge.host_binding_ipv4"
            ):
                ip = network["Options"]["com.docker.network.bridge.host_binding_ipv4"]
                break
    return ip


def _get_value_detach_interactive(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    attach_stdin = container["Config"].get("OpenStdin")
    attach_stderr = container["Config"].get("AttachStderr")
    attach_stdout = container["Config"].get("AttachStdout")
    return {
        "interactive": bool(attach_stdin),
        "detach": not (attach_stderr and attach_stdout),
    }


def _set_value_detach_interactive(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    interactive = values.get("interactive")
    detach = values.get("detach")

    data["AttachStdout"] = False
    data["AttachStderr"] = False
    data["AttachStdin"] = False
    data["StdinOnce"] = False
    data["OpenStdin"] = interactive
    if not detach:
        data["AttachStdout"] = True
        data["AttachStderr"] = True
        if interactive:
            data["AttachStdin"] = True
            data["StdinOnce"] = True


def _get_expected_env_value(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    image: dict[str, t.Any] | None,
    value: t.Any,
    sentry: Sentry,
) -> t.Any | Sentry:
    expected_env = {}
    if image and image["Config"].get("Env"):
        for env_var in image["Config"]["Env"]:
            parts = env_var.split("=", 1)
            expected_env[parts[0]] = parts[1]
    if value and value is not sentry:
        for env_var in value:
            parts = env_var.split("=", 1)
            expected_env[parts[0]] = parts[1]
    param_env = []
    for key, env_value in expected_env.items():
        param_env.append(f"{key}={env_value}")
    return param_env


def _preprocess_cpus(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if value is not None:
        value = int(round(value * 1e9))
    return value


def _preprocess_devices(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if not value:
        return value
    expected_devices = []
    for device in value:
        parts = device.split(":")
        if len(parts) == 1:
            expected_devices.append(
                {
                    "CgroupPermissions": "rwm",
                    "PathInContainer": parts[0],
                    "PathOnHost": parts[0],
                }
            )
        elif len(parts) == 2:
            parts = device.split(":")
            expected_devices.append(
                {
                    "CgroupPermissions": "rwm",
                    "PathInContainer": parts[1],
                    "PathOnHost": parts[0],
                }
            )
        else:
            expected_devices.append(
                {
                    "CgroupPermissions": parts[2],
                    "PathInContainer": parts[1],
                    "PathOnHost": parts[0],
                }
            )
    return expected_devices


def _preprocess_rate_bps(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if not value:
        return value
    devices = []
    for device in value:
        devices.append(
            {
                "Path": device["path"],
                "Rate": human_to_bytes(device["rate"]),
            }
        )
    return devices


def _preprocess_rate_iops(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if not value:
        return value
    devices = []
    for device in value:
        devices.append(
            {
                "Path": device["path"],
                "Rate": device["rate"],
            }
        )
    return devices


def _preprocess_device_requests(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if not value:
        return value
    device_requests = []
    for dr in value:
        device_requests.append(
            {
                "Driver": dr["driver"],
                "Count": dr["count"],
                "DeviceIDs": dr["device_ids"],
                "Capabilities": dr["capabilities"],
                "Options": dr["options"],
            }
        )
    return device_requests


def _preprocess_etc_hosts(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if value is None:
        return value
    results = []
    for key, val in value.items():
        results.append(f"{key}:{val}")
    return results


def _preprocess_healthcheck(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if value is None:
        return value
    if not value or not (
        value.get("test")
        or (value.get("test_cli_compatible") and value.get("test") is None)
    ):
        value = {"test": ["NONE"]}
    elif "test" in value:
        value["test"] = normalize_healthcheck_test(value["test"])
    return omit_none_from_dict(
        {
            "Test": value.get("test"),
            "Interval": value.get("interval"),
            "Timeout": value.get("timeout"),
            "StartPeriod": value.get("start_period"),
            "StartInterval": value.get("start_interval"),
            "Retries": value.get("retries"),
        }
    )


def _postprocess_healthcheck_get_value(
    module: AnsibleModule, api_version: LooseVersion, value: t.Any, sentry: Sentry
) -> t.Any | Sentry:
    if value is None or value is sentry or value.get("Test") == ["NONE"]:
        return {"Test": ["NONE"]}
    return value


def _preprocess_convert_to_bytes(
    module: AnsibleModule,
    values: dict[str, t.Any],
    name: str,
    unlimited_value: int | None = None,
) -> dict[str, t.Any]:
    if name not in values:
        return values
    try:
        value = values[name]
        if unlimited_value is not None and value in ("unlimited", str(unlimited_value)):
            value = unlimited_value
        else:
            value = human_to_bytes(value)
        values[name] = value
        return values
    except ValueError as exc:
        module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")


def _get_image_labels(image: dict[str, t.Any] | None) -> dict[str, str]:
    if not image:
        return {}

    # Cannot use get('Labels', {}) because 'Labels' may be present and be None
    return image["Config"].get("Labels") or {}


def _get_expected_labels_value(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    image: dict[str, t.Any] | None,
    value: dict[str, t.Any],
    sentry: Sentry,
) -> dict[str, t.Any] | Sentry:
    if value is sentry:
        return sentry
    expected_labels = {}
    if module.params["image_label_mismatch"] == "ignore":
        expected_labels.update(dict(_get_image_labels(image)))
    expected_labels.update(value)
    return expected_labels


def _preprocess_links(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if value is None:
        return None

    result = []
    for link in value:
        parsed_link = link.split(":", 1)
        if len(parsed_link) == 2:
            link, alias = parsed_link
        else:
            link, alias = parsed_link[0], parsed_link[0]
        result.append(f"/{link}:/{module.params['name']}/{alias}")

    return result


def _ignore_mismatching_label_result(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    option: Option,
    image: dict[str, t.Any] | None,
    container_value: t.Any,
    expected_value: t.Any,
    host_info: dict[str, t.Any] | None,
) -> bool:
    if (
        option.comparison == "strict"
        and module.params["image_label_mismatch"] == "fail"
    ):
        # If there are labels from the base image that should be removed and
        # base_image_mismatch is fail we want raise an error.
        image_labels = _get_image_labels(image)
        would_remove_labels = []
        labels_param = module.params["labels"] or {}
        for label in image_labels:
            if label not in labels_param:
                # Format label for error message
                would_remove_labels.append(f'"{label}"')
        if would_remove_labels:
            labels = ", ".join(would_remove_labels)
            msg = (
                "Some labels should be removed but are present in the base image. You can set image_label_mismatch to 'ignore' to ignore"
                f" this error. Labels: {labels}"
            )
            client.fail(msg)
    return False


def _needs_host_info_network(values: dict[str, t.Any]) -> bool:
    return values.get("network_mode") == "default"


def _ignore_mismatching_network_result(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    option: Option,
    image: dict[str, t.Any] | None,
    container_value: t.Any,
    expected_value: t.Any,
    host_info: dict[str, t.Any] | None,
) -> bool:
    # 'networks' is handled out-of-band
    if option.name == "networks":
        return True
    # The 'default' network_mode value is translated by the Docker daemon to 'bridge' on Linux and 'nat' on Windows.
    # This happens since Docker 26.1.0 due to https://github.com/moby/moby/pull/47431; before, 'default' was returned.
    if option.name == "network_mode" and expected_value == "default":
        os_type = host_info.get("OSType") if host_info else None
        if (container_value, os_type) in (("bridge", "linux"), ("nat", "windows")):
            return True
    return False


def _preprocess_network_values(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> dict[str, t.Any]:
    if "networks" in values:
        for network in values["networks"]:
            network["id"] = _get_network_id(module, client, network["name"])
            if not network["id"]:
                client.fail(
                    f"Parameter error: network named {network['name']} could not be found. Does it exist?"
                )

    if "network_mode" in values:
        values["network_mode"] = _preprocess_container_names(
            module, client, api_version, values["network_mode"]
        )

    return values


def _get_network_id(
    module: AnsibleModule, client: AnsibleDockerClient, network_name: str
) -> str | None:
    try:
        params = {"filters": json.dumps({"name": [network_name]})}
        for network in client.get_json("/networks", params=params):
            if network["Name"] == network_name:
                return network["Id"]
        return None
    except Exception as exc:  # pylint: disable=broad-exception-caught
        client.fail(f"Error getting network id for {network_name} - {exc}")


def _get_values_network(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    value = container["HostConfig"].get("NetworkMode", _SENTRY)
    if value is _SENTRY:
        return {}
    return {"network_mode": value}


def _set_values_network(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "network_mode" not in values:
        return
    if "HostConfig" not in data:
        data["HostConfig"] = {}
    value = values["network_mode"]
    data["HostConfig"]["NetworkMode"] = value


def _get_values_mounts(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    volumes = container["Config"].get("Volumes")
    binds = container["HostConfig"].get("Binds")
    # According to https://github.com/moby/moby/, support for HostConfig.Mounts
    # has been included at least since v17.03.0-ce, which has API version 1.26.
    # The previous tag, v1.9.1, has API version 1.21 and does not have
    # HostConfig.Mounts. I have no idea what about API 1.25...
    mounts = container["HostConfig"].get("Mounts")
    if mounts is not None:
        mounts_list = []
        empty_dict: dict[str, t.Any] = {}
        for mount in mounts:
            mounts_list.append(
                {
                    "type": mount.get("Type"),
                    "source": mount.get("Source"),
                    "target": mount.get("Target"),
                    "read_only": mount.get(
                        "ReadOnly", False
                    ),  # golang's omitempty for bool returns None for False
                    "consistency": mount.get("Consistency"),
                    "propagation": mount.get("BindOptions", empty_dict).get(
                        "Propagation"
                    ),
                    "no_copy": mount.get("VolumeOptions", empty_dict).get(
                        "NoCopy", False
                    ),
                    "labels": mount.get("VolumeOptions", empty_dict).get(
                        "Labels", empty_dict
                    ),
                    "volume_driver": mount.get("VolumeOptions", empty_dict)
                    .get("DriverConfig", empty_dict)
                    .get("Name"),
                    "volume_options": mount.get("VolumeOptions", empty_dict)
                    .get("DriverConfig", empty_dict)
                    .get("Options", empty_dict),
                    "tmpfs_size": mount.get("TmpfsOptions", empty_dict).get(
                        "SizeBytes"
                    ),
                    "tmpfs_mode": mount.get("TmpfsOptions", empty_dict).get("Mode"),
                    "non_recursive": mount.get("BindOptions", empty_dict).get(
                        "NonRecursive"
                    ),
                    "create_mountpoint": mount.get("BindOptions", empty_dict).get(
                        "CreateMountpoint"
                    ),
                    "read_only_non_recursive": mount.get("BindOptions", empty_dict).get(
                        "ReadOnlyNonRecursive"
                    ),
                    "read_only_force_recursive": mount.get(
                        "BindOptions", empty_dict
                    ).get("ReadOnlyForceRecursive"),
                    "subpath": mount.get("VolumeOptions", empty_dict).get("Subpath")
                    or mount.get("ImageOptions", empty_dict).get("Subpath"),
                    "tmpfs_options": mount.get("TmpfsOptions", empty_dict).get(
                        "Options"
                    ),
                }
            )
        mounts = mounts_list
    result = {}
    if volumes is not None:
        result["volumes"] = volumes
    if binds is not None:
        result["volume_binds"] = binds
    if mounts is not None:
        result["mounts"] = mounts
    return result


def _get_bind_from_dict(volume_dict: dict[str, t.Any] | None) -> list[str]:
    results = []
    if volume_dict:
        for host_path, config in volume_dict.items():
            if isinstance(config, dict) and config.get("bind"):
                container_path = config.get("bind")
                mode = config.get("mode", "rw")
                results.append(f"{host_path}:{container_path}:{mode}")
    return results


def _get_image_binds(volumes: dict[str, t.Any] | list[dict[str, t.Any]]) -> list[str]:
    """
    Convert array of binds to array of strings with format host_path:container_path:mode

    :param volumes: array of bind dicts
    :return: array of strings
    """
    results = []
    if isinstance(volumes, dict):
        results += _get_bind_from_dict(volumes)
    elif isinstance(volumes, list):
        for vol in volumes:
            results += _get_bind_from_dict(vol)
    return results


def _get_expected_values_mounts(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    values: dict[str, t.Any],
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    expected_values = {}

    # binds
    if "mounts" in values:
        expected_values["mounts"] = values["mounts"]

    # volumes
    expected_vols = {}
    if image and image["Config"].get("Volumes"):
        expected_vols.update(image["Config"].get("Volumes"))
    if "volumes" in values:
        for vol in values["volumes"]:
            # We only expect anonymous volumes to show up in the list
            if ":" in vol:
                parts = vol.split(":")
                if len(parts) == 3:
                    continue
                if len(parts) == 2 and not _is_volume_permissions(parts[1]):
                    continue
            expected_vols[vol] = {}
    if expected_vols:
        expected_values["volumes"] = expected_vols

    # binds
    image_vols = []
    if image:
        image_vols = _get_image_binds(image["Config"].get("Volumes"))
    param_vols = []
    if "volume_binds" in values:
        param_vols = values["volume_binds"]
    expected_values["volume_binds"] = list(set(image_vols + param_vols))

    return expected_values


def _set_values_mounts(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "mounts" in values:
        if "HostConfig" not in data:
            data["HostConfig"] = {}
        mounts: list[dict[str, t.Any]] = []
        for mount in values["mounts"]:
            mount_type = mount.get("type")
            mount_res = {
                "Target": mount.get("target"),
                "Source": mount.get("source"),
                "Type": mount_type,
                "ReadOnly": mount.get("read_only"),
            }
            if "consistency" in mount:
                mount_res["Consistency"] = mount["consistency"]
            if mount_type == "bind":
                bind_opts: dict[str, t.Any] = {}
                if "propagation" in mount:
                    bind_opts["Propagation"] = mount["propagation"]
                if "non_recursive" in mount:
                    bind_opts["NonRecursive"] = mount["non_recursive"]
                if "create_mountpoint" in mount:
                    bind_opts["CreateMountpoint"] = mount["create_mountpoint"]
                if "read_only_non_recursive" in mount:
                    bind_opts["ReadOnlyNonRecursive"] = mount["read_only_non_recursive"]
                if "read_only_force_recursive" in mount:
                    bind_opts["ReadOnlyForceRecursive"] = mount[
                        "read_only_force_recursive"
                    ]
                if bind_opts:
                    mount_res["BindOptions"] = bind_opts
            if mount_type == "volume":
                volume_opts: dict[str, t.Any] = {}
                if mount.get("no_copy"):
                    volume_opts["NoCopy"] = True
                if mount.get("labels"):
                    volume_opts["Labels"] = mount.get("labels")
                if mount.get("volume_driver"):
                    driver_config: dict[str, t.Any] = {
                        "Name": mount.get("volume_driver"),
                    }
                    if mount.get("volume_options"):
                        driver_config["Options"] = mount.get("volume_options")
                    volume_opts["DriverConfig"] = driver_config
                if "subpath" in mount:
                    volume_opts["Subpath"] = mount["subpath"]
                if volume_opts:
                    mount_res["VolumeOptions"] = volume_opts
            if mount_type == "tmpfs":
                tmpfs_opts: dict[str, t.Any] = {}
                if mount.get("tmpfs_mode"):
                    tmpfs_opts["Mode"] = mount.get("tmpfs_mode")
                if mount.get("tmpfs_size"):
                    tmpfs_opts["SizeBytes"] = mount.get("tmpfs_size")
                if "tmpfs_options" in mount:
                    tmpfs_opts["Options"] = mount["tmpfs_options"]
                if tmpfs_opts:
                    mount_res["TmpfsOptions"] = tmpfs_opts
            if mount_type == "image":
                image_opts: dict[str, t.Any] = {}
                if "subpath" in mount:
                    image_opts["Subpath"] = mount["subpath"]
                if image_opts:
                    mount_res["ImageOptions"] = image_opts
            mounts.append(mount_res)
        data["HostConfig"]["Mounts"] = mounts
    if "volumes" in values:
        volumes: dict[str, t.Any] = {}
        for volume in values["volumes"]:
            # Only pass anonymous volumes to create container
            if ":" in volume:
                parts = volume.split(":")
                if len(parts) == 3:
                    continue
                if len(parts) == 2 and not _is_volume_permissions(parts[1]):
                    continue
            volumes[volume] = {}
        data["Volumes"] = volumes
    if "volume_binds" in values:
        if "HostConfig" not in data:
            data["HostConfig"] = {}
        data["HostConfig"]["Binds"] = values["volume_binds"]


def _get_values_log(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    log_config = container["HostConfig"].get("LogConfig") or {}
    return {
        "log_driver": log_config.get("Type"),
        "log_options": log_config.get("Config"),
    }


def _set_values_log(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "log_driver" not in values:
        return
    log_config = {
        "Type": values["log_driver"],
        "Config": values.get("log_options") or {},
    }
    if "HostConfig" not in data:
        data["HostConfig"] = {}
    data["HostConfig"]["LogConfig"] = log_config


def _get_values_platform(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    if image and (image.get("Os") or image.get("Architecture") or image.get("Variant")):
        return {
            "platform": compose_platform_string(
                os=image.get("Os"),
                arch=image.get("Architecture"),
                variant=image.get("Variant"),
                daemon_os=host_info.get("OSType") if host_info else None,
                daemon_arch=host_info.get("Architecture") if host_info else None,
            )
        }
    return {
        "platform": container.get("Platform"),
    }


def _get_expected_values_platform(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    values: dict[str, t.Any],
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    expected_values = {}
    if "platform" in values:
        try:
            expected_values["platform"] = normalize_platform_string(
                values["platform"],
                daemon_os=host_info.get("OSType") if host_info else None,
                daemon_arch=host_info.get("Architecture") if host_info else None,
            )
        except ValueError as exc:
            module.fail_json(msg=f"Error while parsing platform parameer: {exc}")
    return expected_values


def _set_values_platform(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "platform" in values:
        data["platform"] = values["platform"]


def _needs_container_image_platform(values: dict[str, t.Any]) -> bool:
    return "platform" in values


def _needs_host_info_platform(values: dict[str, t.Any]) -> bool:
    return "platform" in values


def _get_values_restart(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    restart_policy = container["HostConfig"].get("RestartPolicy") or {}
    return {
        "restart_policy": restart_policy.get("Name"),
        "restart_retries": restart_policy.get("MaximumRetryCount"),
    }


def _set_values_restart(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "restart_policy" not in values:
        return
    restart_policy = {
        "Name": values["restart_policy"],
        "MaximumRetryCount": values.get("restart_retries"),
    }
    if "HostConfig" not in data:
        data["HostConfig"] = {}
    data["HostConfig"]["RestartPolicy"] = restart_policy


def _update_value_restart(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "restart_policy" not in values:
        return
    data["RestartPolicy"] = {
        "Name": values["restart_policy"],
        "MaximumRetryCount": values.get("restart_retries"),
    }


def _get_values_ports(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    host_config = container["HostConfig"]
    config = container["Config"]

    # "ExposedPorts": null returns None type & causes AttributeError - PR #5517
    expected_exposed: list[str] = []
    if config.get("ExposedPorts") is not None:
        for port_and_protocol in config.get("ExposedPorts", {}):
            port, protocol = _normalize_port(port_and_protocol).rsplit("/")
            try:
                start, end = port.split("-", 1)
                start_port = int(start)
                end_port = int(end)
                for port_no in range(start_port, end_port + 1):
                    expected_exposed.append(f"{port_no}/{protocol}")
                continue
            except ValueError:
                # Either it is not a range, or a broken one - in both cases, simply add the original form
                expected_exposed.append(f"{port}/{protocol}")

    return {
        "published_ports": host_config.get("PortBindings"),
        "exposed_ports": expected_exposed,
        "publish_all_ports": host_config.get("PublishAllPorts"),
    }


def _get_expected_values_ports(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    values: dict[str, t.Any],
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    expected_values: dict[str, t.Any] = {}

    if "published_ports" in values:
        expected_bound_ports = {}
        for container_port, config in values["published_ports"].items():
            if isinstance(container_port, int):
                container_port = f"{container_port}/tcp"
            if len(config) == 1:
                if isinstance(config[0], int):
                    expected_bound_ports[container_port] = [
                        {"HostIp": "0.0.0.0", "HostPort": config[0]}
                    ]
                else:
                    expected_bound_ports[container_port] = [
                        {"HostIp": config[0], "HostPort": ""}
                    ]
            elif isinstance(config[0], tuple):
                expected_bound_ports[container_port] = []
                for host_ip, host_port in config:
                    expected_bound_ports[container_port].append(
                        {
                            "HostIp": host_ip,
                            "HostPort": to_text(
                                host_port, errors="surrogate_or_strict"
                            ),
                        }
                    )
            else:
                expected_bound_ports[container_port] = [
                    {
                        "HostIp": config[0],
                        "HostPort": to_text(config[1], errors="surrogate_or_strict"),
                    }
                ]
        expected_values["published_ports"] = expected_bound_ports

    image_ports: set[str] = set()
    if image:
        image_exposed_ports = image["Config"].get("ExposedPorts") or {}
        image_ports = {_normalize_port(p) for p in image_exposed_ports}
    param_ports: set[str] = set()
    if "ports" in values:
        param_ports = {f"{p[0]}/{p[1]}" for p in values["ports"]}
    result = sorted(image_ports | param_ports)
    expected_values["exposed_ports"] = result

    if "publish_all_ports" in values:
        expected_values["publish_all_ports"] = values["publish_all_ports"]

    return expected_values


def _set_values_ports(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "ports" in values:
        exposed_ports: dict[str, dict[str, t.Any]] = {}
        for port_definition in values["ports"]:
            port = port_definition
            proto = "tcp"
            if isinstance(port_definition, tuple):
                if len(port_definition) == 2:
                    proto = port_definition[1]
                port = port_definition[0]
            exposed_ports[f"{port}/{proto}"] = {}
        data["ExposedPorts"] = exposed_ports
    if "published_ports" in values:
        if "HostConfig" not in data:
            data["HostConfig"] = {}
        data["HostConfig"]["PortBindings"] = convert_port_bindings(
            values["published_ports"]
        )
    if "publish_all_ports" in values and values["publish_all_ports"]:
        if "HostConfig" not in data:
            data["HostConfig"] = {}
        data["HostConfig"]["PublishAllPorts"] = values["publish_all_ports"]


def _preprocess_value_ports(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> dict[str, t.Any]:
    if "published_ports" not in values:
        return values
    found = False
    for port_specs in values["published_ports"].values():
        if not isinstance(port_specs, list):
            port_specs = [port_specs]
        for port_spec in port_specs:
            if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
                found = True
                break
    if not found:
        return values
    default_ip = _get_default_host_ip(module, client)
    for port, port_specs in values["published_ports"].items():
        if isinstance(port_specs, list):
            for index, port_spec in enumerate(port_specs):
                if port_spec[0] == _DEFAULT_IP_REPLACEMENT_STRING:
                    port_specs[index] = tuple([default_ip] + list(port_spec[1:]))
        else:
            if port_specs[0] == _DEFAULT_IP_REPLACEMENT_STRING:
                values["published_ports"][port] = tuple(
                    [default_ip] + list(port_specs[1:])
                )
    return values


def _preprocess_container_names(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    value: t.Any,
) -> t.Any:
    if value is None or not value.startswith("container:"):
        return value
    container_name = value[len("container:") :]
    # Try to inspect container to see whether this is an ID or a
    # name (and in the latter case, retrieve its ID)
    container = client.get_container(container_name)
    if container is None:
        # If we cannot find the container, issue a warning and continue with
        # what the user specified.
        module.warn(f'Cannot find a container with name or ID "{container_name}"')
        return value
    return f"container:{container['Id']}"


def _get_value_command(
    module: AnsibleModule,
    container: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    value = container["Config"].get("Cmd", _SENTRY)
    if value is _SENTRY:
        return {}
    return {"command": value}


def _set_value_command(
    module: AnsibleModule,
    data: dict[str, t.Any],
    api_version: LooseVersion,
    options: list[Option],
    values: dict[str, t.Any],
) -> None:
    if "command" not in values:
        return
    value = values["command"]
    data["Cmd"] = value


def _get_expected_values_command(
    module: AnsibleModule,
    client: AnsibleDockerClient,
    api_version: LooseVersion,
    options: list[Option],
    image: dict[str, t.Any] | None,
    values: dict[str, t.Any],
    host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
    expected_values = {}
    if "command" in values:
        command = values["command"]
        if command == [] and image and image["Config"].get("Cmd"):
            command = image["Config"].get("Cmd")
        expected_values["command"] = command
    return expected_values


def _needs_container_image_command(values: dict[str, t.Any]) -> bool:
    return values.get("command") == []


OPTION_AUTO_REMOVE.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("AutoRemove")
)

OPTION_BLKIO_WEIGHT.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("BlkioWeight", update_parameter="BlkioWeight"),
)

OPTION_CAPABILITIES.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("CapAdd")
)

OPTION_CAP_DROP.add_engine("docker_api", DockerAPIEngine.host_config_value("CapDrop"))

OPTION_CGROUP_NS_MODE.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CgroupnsMode", min_api_version="1.41"),
)

OPTION_CGROUP_PARENT.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("CgroupParent")
)

OPTION_COMMAND.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_value_command,
        set_value=_set_value_command,
        get_expected_values=_get_expected_values_command,
        needs_container_image=_needs_container_image_command,
    ),
)

OPTION_CPU_PERIOD.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CpuPeriod", update_parameter="CpuPeriod"),
)

OPTION_CPU_QUOTA.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CpuQuota", update_parameter="CpuQuota"),
)

OPTION_CPUSET_CPUS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CpusetCpus", update_parameter="CpusetCpus"),
)

OPTION_CPUSET_MEMS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CpusetMems", update_parameter="CpusetMems"),
)

OPTION_CPU_SHARES.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("CpuShares", update_parameter="CpuShares"),
)

OPTION_ENTRYPOINT.add_engine("docker_api", DockerAPIEngine.config_value("Entrypoint"))

OPTION_CPUS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("NanoCpus", preprocess_value=_preprocess_cpus),
)

OPTION_DETACH_INTERACTIVE.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_value_detach_interactive, set_value=_set_value_detach_interactive
    ),
)

OPTION_DEVICES.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("Devices", preprocess_value=_preprocess_devices),
)

OPTION_DEVICE_READ_BPS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "BlkioDeviceReadBps", preprocess_value=_preprocess_rate_bps
    ),
)

OPTION_DEVICE_WRITE_BPS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "BlkioDeviceWriteBps", preprocess_value=_preprocess_rate_bps
    ),
)

OPTION_DEVICE_READ_IOPS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "BlkioDeviceReadIOps", preprocess_value=_preprocess_rate_iops
    ),
)

OPTION_DEVICE_WRITE_IOPS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "BlkioDeviceWriteIOps", preprocess_value=_preprocess_rate_iops
    ),
)

OPTION_DEVICE_REQUESTS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "DeviceRequests",
        min_api_version="1.40",
        preprocess_value=_preprocess_device_requests,
    ),
)

OPTION_DEVICE_CGROUP_RULES.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("DeviceCgroupRules", min_api_version="1.28"),
)

OPTION_DNS_SERVERS.add_engine("docker_api", DockerAPIEngine.host_config_value("Dns"))

OPTION_DNS_OPTS.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("DnsOptions")
)

OPTION_DNS_SEARCH_DOMAINS.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("DnsSearch")
)

OPTION_DOMAINNAME.add_engine("docker_api", DockerAPIEngine.config_value("Domainname"))

OPTION_ENVIRONMENT.add_engine(
    "docker_api",
    DockerAPIEngine.config_value("Env", get_expected_value=_get_expected_env_value),
)

OPTION_ETC_HOSTS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "ExtraHosts", preprocess_value=_preprocess_etc_hosts
    ),
)

OPTION_GROUPS.add_engine("docker_api", DockerAPIEngine.host_config_value("GroupAdd"))

OPTION_HEALTHCHECK.add_engine(
    "docker_api",
    DockerAPIEngine.config_value(
        "Healthcheck",
        preprocess_value=_preprocess_healthcheck,
        postprocess_for_get=_postprocess_healthcheck_get_value,
        extra_option_minimal_versions={
            "healthcheck.start_interval": {
                "docker_api_version": "1.44",
                "detect_usage": lambda c: c.module.params["healthcheck"]
                and c.module.params["healthcheck"]["start_interval"] is not None,
            },
        },
    ),
)

OPTION_HOSTNAME.add_engine("docker_api", DockerAPIEngine.config_value("Hostname"))

OPTION_IMAGE.add_engine(
    "docker_api",
    DockerAPIEngine.config_value(
        "Image",
        ignore_mismatching_result=lambda module, client, api_version, option, image, container_value, expected_value, host_info: True,
    ),
)

OPTION_INIT.add_engine("docker_api", DockerAPIEngine.host_config_value("Init"))

OPTION_IPC_MODE.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "IpcMode", preprocess_value=_preprocess_container_names
    ),
)

OPTION_KERNEL_MEMORY.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("KernelMemory", update_parameter="KernelMemory"),
)

OPTION_LABELS.add_engine(
    "docker_api",
    DockerAPIEngine.config_value(
        "Labels",
        get_expected_value=_get_expected_labels_value,
        ignore_mismatching_result=_ignore_mismatching_label_result,
    ),
)

OPTION_LINKS.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("Links", preprocess_value=_preprocess_links),
)

OPTION_LOG_DRIVER_OPTIONS.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_values_log,
        set_value=_set_values_log,
    ),
)

OPTION_MAC_ADDRESS.add_engine("docker_api", DockerAPIEngine.config_value("MacAddress"))

OPTION_MEMORY.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("Memory", update_parameter="Memory")
)

OPTION_MEMORY_RESERVATION.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "MemoryReservation", update_parameter="MemoryReservation"
    ),
)

OPTION_MEMORY_SWAP.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value("MemorySwap", update_parameter="MemorySwap"),
)

OPTION_MEMORY_SWAPPINESS.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("MemorySwappiness")
)

OPTION_STOP_TIMEOUT.add_engine(
    "docker_api", DockerAPIEngine.config_value("StopTimeout")
)

OPTION_NETWORK.add_engine(
    "docker_api",
    DockerAPIEngine(
        preprocess_value=_preprocess_network_values,
        get_value=_get_values_network,
        set_value=_set_values_network,
        ignore_mismatching_result=_ignore_mismatching_network_result,
        needs_host_info=_needs_host_info_network,
        extra_option_minimal_versions={
            "networks.mac_address": {
                "docker_api_version": "1.44",
                "detect_usage": lambda c: any(
                    net_info.get("mac_address") is not None
                    for net_info in (c.module.params["networks"] or [])
                ),
            },
            "networks.driver_opts": {
                "docker_api_version": "1.32",
                "detect_usage": lambda c: any(
                    net_info.get("driver_opts") is not None
                    for net_info in (c.module.params["networks"] or [])
                ),
            },
            "networks.gw_priority": {
                "docker_api_version": "1.48",
                "detect_usage": lambda c: any(
                    net_info.get("gw_priority") is not None
                    for net_info in (c.module.params["networks"] or [])
                ),
            },
        },
    ),
)

OPTION_OOM_KILLER.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("OomKillDisable")
)

OPTION_OOM_SCORE_ADJ.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("OomScoreAdj")
)

OPTION_PID_MODE.add_engine(
    "docker_api",
    DockerAPIEngine.host_config_value(
        "PidMode", preprocess_value=_preprocess_container_names
    ),
)

OPTION_PIDS_LIMIT.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("PidsLimit")
)

OPTION_PLATFORM.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_values_platform,
        set_value=_set_values_platform,
        get_expected_values=_get_expected_values_platform,
        needs_container_image=_needs_container_image_platform,
        needs_host_info=_needs_host_info_platform,
        min_api_version="1.41",
    ),
)

OPTION_PRIVILEGED.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("Privileged")
)

OPTION_READ_ONLY.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("ReadonlyRootfs")
)

OPTION_RESTART_POLICY.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_values_restart,
        set_value=_set_values_restart,
        update_value=_update_value_restart,
    ),
)

OPTION_RUNTIME.add_engine("docker_api", DockerAPIEngine.host_config_value("Runtime"))

OPTION_SECURITY_OPTS.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("SecurityOpt")
)

OPTION_SHM_SIZE.add_engine("docker_api", DockerAPIEngine.host_config_value("ShmSize"))

OPTION_STOP_SIGNAL.add_engine("docker_api", DockerAPIEngine.config_value("StopSignal"))

OPTION_STORAGE_OPTS.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("StorageOpt")
)

OPTION_SYSCTLS.add_engine("docker_api", DockerAPIEngine.host_config_value("Sysctls"))

OPTION_TMPFS.add_engine("docker_api", DockerAPIEngine.host_config_value("Tmpfs"))

OPTION_TTY.add_engine("docker_api", DockerAPIEngine.config_value("Tty"))

OPTION_ULIMITS.add_engine("docker_api", DockerAPIEngine.host_config_value("Ulimits"))

OPTION_USER.add_engine("docker_api", DockerAPIEngine.config_value("User"))

OPTION_USERNS_MODE.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("UsernsMode")
)

OPTION_UTS.add_engine("docker_api", DockerAPIEngine.host_config_value("UTSMode"))

OPTION_VOLUME_DRIVER.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("VolumeDriver")
)

OPTION_VOLUMES_FROM.add_engine(
    "docker_api", DockerAPIEngine.host_config_value("VolumesFrom")
)

OPTION_WORKING_DIR.add_engine("docker_api", DockerAPIEngine.config_value("WorkingDir"))

OPTION_MOUNTS_VOLUMES.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_values_mounts,
        get_expected_values=_get_expected_values_mounts,
        set_value=_set_values_mounts,
        extra_option_minimal_versions={
            "mounts.non_recursive": {
                "docker_api_version": "1.40",
                "detect_usage": lambda c: any(
                    mount.get("non_recursive") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.create_mountpoint": {
                "docker_api_version": "1.42",
                "detect_usage": lambda c: any(
                    mount.get("create_mountpoint") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.type=cluster": {
                "docker_api_version": "1.42",
                "detect_usage": lambda c: any(
                    mount.get("type") == "cluster"
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.read_only_non_recursive": {
                "docker_api_version": "1.44",
                "detect_usage": lambda c: any(
                    mount.get("read_only_non_recursive") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.read_only_force_recursive": {
                "docker_api_version": "1.44",
                "detect_usage": lambda c: any(
                    mount.get("read_only_force_recursive") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.subpath": {
                "docker_api_version": "1.45",
                "detect_usage": lambda c: any(
                    mount.get("subpath") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.tmpfs_options": {
                "docker_api_version": "1.46",
                "detect_usage": lambda c: any(
                    mount.get("tmpfs_options") is not None
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
            "mounts.type=image": {
                "docker_api_version": "1.47",
                "detect_usage": lambda c: any(
                    mount.get("type") == "image"
                    for mount in (c.module.params["mounts"] or [])
                ),
            },
        },
    ),
)

OPTION_PORTS.add_engine(
    "docker_api",
    DockerAPIEngine(
        get_value=_get_values_ports,
        get_expected_values=_get_expected_values_ports,
        set_value=_set_values_ports,
        preprocess_value=_preprocess_value_ports,
    ),
)
