#!/usr/bin/python
#
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
# 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

from __future__ import annotations

DOCUMENTATION = r"""
module: docker_compose_v2_pull

short_description: Pull a Docker compose project

version_added: 3.6.0

description:
  - Uses Docker Compose to pull images for a project.
extends_documentation_fragment:
  - community.docker._compose_v2
  - community.docker._compose_v2.minimum_version
  - community.docker._docker.cli_documentation
  - community.docker._attributes
  - community.docker._attributes.actiongroup_docker

attributes:
  check_mode:
    support: full
    details:
      - If O(policy=always), the module will always indicate a change. Docker Compose does not give any information whether
        pulling would update the image or not.
  diff_mode:
    support: none
  idempotent:
    support: full

options:
  policy:
    description:
      - Whether to pull images before running. This is used when C(docker compose up) is ran.
      - V(always) ensures that the images are always pulled, even when already present on the Docker daemon.
      - V(missing) only pulls them when they are not present on the Docker daemon. This is only supported since Docker Compose
        2.22.0.
    type: str
    choices:
      - always
      - missing
    default: always
  ignore_buildable:
    description:
      - If set to V(true), will not pull images that can be built.
    type: bool
    default: false
    version_added: 3.12.0
  ignore_pull_failures:
    description:
      - If set to V(true), will pull what it can and ignores images with pull failures.
    type: bool
    default: false
    version_added: 5.1.0
  include_deps:
    description:
      - If set to V(true), also pull services that are declared as dependencies.
      - This only makes sense if O(services) is used.
    type: bool
    default: false
    version_added: 3.12.0
  services:
    description:
      - Specifies a subset of services to be targeted.
    type: list
    elements: str
    version_added: 3.12.0

author:
  - Felix Fontein (@felixfontein)

seealso:
  - module: community.docker.docker_compose_v2
"""

EXAMPLES = r"""
---
- name: Pull images for flask project
  community.docker.docker_compose_v2_pull:
    project_src: /path/to/flask
"""

RETURN = r"""
actions:
  description:
    - A list of actions that have been applied.
  returned: success
  type: list
  elements: dict
  contains:
    what:
      description:
        - What kind of resource was changed.
      type: str
      sample: container
      choices:
        - image
        - unknown
    id:
      description:
        - The ID of the resource that was changed.
      type: str
      sample: container
    status:
      description:
        - The status change that happened.
      type: str
      sample: Pulling
      choices:
        - Pulling
"""

import traceback
import typing as t

from ansible_collections.community.docker.plugins.module_utils._common_cli import (
    AnsibleModuleDockerClient,
    DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._compose_v2 import (
    BaseComposeManager,
    common_compose_argspec_ex,
)
from ansible_collections.community.docker.plugins.module_utils._version import (
    LooseVersion,
)


class PullManager(BaseComposeManager):
    def __init__(self, client: AnsibleModuleDockerClient) -> None:
        super().__init__(client)
        parameters = self.client.module.params

        self.policy: t.Literal["always", "missing"] = parameters["policy"]
        self.ignore_buildable: bool = parameters["ignore_buildable"]
        self.ignore_pull_failures: bool = parameters["ignore_pull_failures"]
        self.include_deps: bool = parameters["include_deps"]
        self.services: list[str] = parameters["services"] or []

        if self.policy != "always" and self.compose_version < LooseVersion("2.22.0"):
            # https://github.com/docker/compose/pull/10981 - 2.22.0
            self.fail(
                f"A pull policy other than always is only supported since Docker Compose 2.22.0. {self.client.get_cli()} has version {self.compose_version}"
            )
        if self.ignore_buildable and self.compose_version < LooseVersion("2.15.0"):
            # https://github.com/docker/compose/pull/10134 - 2.15.0
            self.fail(
                f"--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}"
            )

    def get_pull_cmd(self, dry_run: bool) -> list[str]:
        args = self.get_base_args() + ["pull"]
        if self.policy != "always":
            args.extend(["--policy", self.policy])
        if self.ignore_buildable:
            args.append("--ignore-buildable")
        if self.ignore_pull_failures:
            args.append("--ignore-pull-failures")
        if self.include_deps:
            args.append("--include-deps")
        if dry_run:
            args.append("--dry-run")
        args.append("--")
        for service in self.services:
            args.append(service)
        return args

    def run(self) -> dict[str, t.Any]:
        result: dict[str, t.Any] = {}
        args = self.get_pull_cmd(self.check_mode)
        rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
        events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
        self.emit_warnings(events)
        self.update_result(
            result,
            events,
            stdout,
            stderr,
            ignore_service_pull_events=self.policy != "missing" and not self.check_mode,
        )
        self.update_failed(result, events, args, stdout, stderr, rc)
        self.cleanup_result(result)
        return result


def main() -> None:
    argument_spec = {
        "policy": {
            "type": "str",
            "choices": ["always", "missing"],
            "default": "always",
        },
        "ignore_buildable": {"type": "bool", "default": False},
        "ignore_pull_failures": {"type": "bool", "default": False},
        "include_deps": {"type": "bool", "default": False},
        "services": {"type": "list", "elements": "str"},
    }
    argspec_ex = common_compose_argspec_ex()
    argument_spec.update(argspec_ex.pop("argspec"))

    client = AnsibleModuleDockerClient(
        argument_spec=argument_spec,
        supports_check_mode=True,
        needs_api_version=False,
        **argspec_ex,
    )

    try:
        manager = PullManager(client)
        result = manager.run()
        manager.cleanup()
        client.module.exit_json(**result)
    except DockerException as e:
        client.fail(
            f"An unexpected docker error occurred: {e}",
            exception=traceback.format_exc(),
        )


if __name__ == "__main__":
    main()
