# SPDX-License-Identifier: AGPL-3.0-or-later
"""Component to manage a container using podman."""

import contextlib

from django.utils.translation import gettext_noop

from plinth import app, log, privileged
from plinth.daemon import diagnose_port_listening
from plinth.diagnostic_check import (DiagnosticCheck,
                                     DiagnosticCheckParameters, Result)


class Container(app.LeaderComponent, log.LogEmitter):
    """Component to manage a podman container."""

    def __init__(self, component_id: str, name: str, image_name: str,
                 volume_name: str, volume_path: str,
                 volumes: dict[str, str] | None = None,
                 env: dict[str, str] | None = None,
                 binds_to: list[str] | None = None,
                 devices: dict[str, str] | None = None,
                 listen_ports: list[tuple[int, str]] | None = None):
        """Initialize a container component.

        `name` is a string which is the name of the container to create and
        manage. A systemd service unit with the same name is also created.

        `image_name` is a string that represents the repository location from
        which the container images must be pull from.

        `volume_name` is a string with name of the storage volume to create for
        the container to use.

        `volume_path` is a string path on the host machine where the volume
        files for the container is stored.

        `volumes` is a dictionary mapping each string path on the host to a
        string path inside the container. These are bind mounts made available
        inside the container.

        `env` is a dictionary of string key to string values that set the
        environment variables for the processes inside the container to run in.

        `binds_to` is a list of systemd service units that the container's own
        systemd service unit will add BindsTo= and After= dependencies on.

        `devices` is a list of strings with device paths that will be made
        available inside the container. If any of the devices don't exist on
        the host, they will not be added.

        `listen_ports` is a list of tuples containing port number and 'tcp4' or
        'tcp6' network types on which this container is expected to listen on
        after starting the container. This information is used to run
        diagnostic checks on the container.
        """
        super().__init__(component_id)
        self.name = name
        self.image_name = image_name
        self.volume_name = volume_name
        self.volume_path = volume_path
        self.volumes = volumes
        self.env = env
        self.binds_to = binds_to
        self.devices = devices
        self.listen_ports = listen_ports or []

        # For logs
        self.unit = self.name

    def is_enabled(self):
        """Return if the container is enabled."""
        return privileged.container_is_enabled(self.name)

    def enable(self):
        """Run operations to enable and run the container."""
        super().enable()
        privileged.container_enable(self.name)

    def disable(self):
        """Run operations to disable and stop the container."""
        super().disable()
        privileged.container_disable(self.name)

    def is_running(self):
        """Return whether the container service is running."""
        return privileged.is_running(self.name)

    @contextlib.contextmanager
    def ensure_running(self):
        """Ensure a service is running and return to previous state."""
        from plinth.privileged import service as service_privileged
        starting_state = self.is_running()
        if not starting_state:
            service_privileged.enable(self.name)

        try:
            yield starting_state
        finally:
            if not starting_state:
                service_privileged.disable(self.name)

    def setup(self, old_version: int):
        """Bring up and run the container."""
        # Determine whether app should be disabled after setup
        should_disable = old_version and not self.is_enabled()

        privileged.container_setup(self.name, self.image_name,
                                   self.volume_name, self.volume_path,
                                   self.volumes, self.env, self.binds_to,
                                   self.devices)

        if should_disable:
            self.disable()

    def uninstall(self):
        """Remove the container."""
        privileged.container_uninstall(self.name, self.image_name,
                                       self.volume_name, self.volume_path)

    def diagnose(self) -> list[DiagnosticCheck]:
        """Check if the container is running..

        See :py:meth:`plinth.app.Component.diagnose`.
        """
        results = []
        results.append(self._diagnose_unit_is_running())
        for port in self.listen_ports:
            results.append(
                diagnose_port_listening(port[0], port[1], None,
                                        self.component_id))

        return results

    def _diagnose_unit_is_running(self) -> DiagnosticCheck:
        """Check if a daemon is running."""
        check_id = f'container-running-{self.name}'
        result = Result.PASSED if self.is_running() else Result.FAILED

        description = gettext_noop('Container {container_name} is running')
        parameters: DiagnosticCheckParameters = {
            'container_name': str(self.name)
        }

        return DiagnosticCheck(check_id, description, result, parameters,
                               self.component_id)
