#!/usr/bin/python3
#
# This is not a stable API; for use within autopkgtest only.
#
# Copyright 2024 Canonical Ltd.
# Copyright 2024 Simon McVittie
# SPDX-License-Identifier: GPL-2.0-or-later

import os
import shutil
import sys
import subprocess
from abc import ABC, abstractmethod
from pathlib import Path
from collections.abc import Sequence


class Dependency(ABC):
    @abstractmethod
    def check(self) -> bool:
        raise NotImplementedError


class Executable(Dependency):
    def __init__(
        self,
        exe: str,
        package: str,
        *,
        fatal: bool = True,
        if_not_root: bool = False,
        if_not_systemd: bool = False,
    ) -> None:
        self.exe = exe
        self.fatal = fatal
        self.package = package
        self.if_not_root = if_not_root
        self.if_not_systemd = if_not_systemd

    def check(self) -> bool:
        qualifiers: list[str] = []

        if self.if_not_root:
            if os.geteuid() == 0:
                return True

            qualifiers.append(" (not required if run as root)")

        if self.if_not_systemd:
            if Path("/run/systemd/system").exists():
                return True

            qualifiers.append(" (not required if init is systemd)")

        if shutil.which(self.exe):
            return True

        if self.fatal:
            level = "ERROR"
        else:
            level = "WARNING"

        sys.stderr.write(
            "%s: command %s not found, provided by package %s%s\n"
            % (level, self.exe, self.package, "".join(qualifiers))
        )
        return not self.fatal


class FileDependency(Dependency):
    def __init__(
        self,
        path: Path,
        package: str,
        *,
        fatal: bool = True,
    ) -> None:
        self.fatal = fatal
        self.path = path
        self.package = package

    def check(self) -> bool:
        if self.path.exists():
            return True

        if self.fatal:
            level = "ERROR"
        else:
            level = "WARNING"

        sys.stderr.write(
            "%s: file %s not found, provided by package %s\n"
            % (level, self.path, self.package)
        )
        return not self.fatal


class DirectoryDependency(Dependency):
    def __init__(self, path: Path) -> None:
        self.path = path

    def check(self) -> bool:
        if not os.path.exists(self.path):
            sys.stderr.write(
                "INFO: directory %s not found, creating it now\n" % self.path
            )
            try:
                os.makedirs(self.path)
            except Exception as e:
                sys.stderr.write("ERROR: could not create directory: %s\n" % e)
                return False

        if not os.access(self.path, os.W_OK):
            sys.stderr.write("ERROR: no write access to %s\n" % self.path)
            return False

        if not os.path.isdir(self.path):
            sys.stderr.write("ERROR: %s is not a directory\n" % self.path)
            return False

        return True


class KvmDependency(Dependency):
    def __init__(
        self,
        *,
        if_exists: bool,
    ) -> None:
        self.if_exists = if_exists

    def check(self) -> bool:
        if os.access("/dev/kvm", os.W_OK):
            return True

        if not os.path.exists("/dev/kvm"):
            if self.if_exists:
                return True
            else:
                sys.stderr.write("ERROR: /dev/kvm does not exist\n")
                return False

        sys.stderr.write("ERROR: no permission to write /dev/kvm\n")
        return False


class SpaceRequirement(Dependency):
    """Check if disk at path has specified amount of free space"""

    def __init__(
        self,
        path: Path,
        space: int,
        *,
        fatal: bool = False,
    ) -> None:
        self.path = path
        self.space = space
        self.fatal = fatal

    def check(self) -> bool:
        if not os.path.exists(self.path):
            return False
        free_space = shutil.disk_usage(self.path).free

        if self.fatal:
            level = "ERROR"
        else:
            level = "WARNING"

        if free_space <= self.space:
            missing = self.space - free_space
            sys.stderr.write(
                "%s: Missing %s bytes of space in directory %s \n"
                % (level, missing, os.path.realpath(self.path))
            )
            return not self.fatal
        return True


class Package(Dependency):
    def __init__(self, name: str, fatal: bool = False):
        self.name = name
        self.fatal = fatal

    def check(self) -> bool:
        level = "ERROR" if self.fatal else "WARNING"
        dpkg_ret = subprocess.run(
            ["dpkg-query", "--showformat=${Status}", "--show", self.name],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
        )
        if dpkg_ret.stdout != "install ok installed":
            sys.stderr.write(f"{level}: Missing package {self.name}\n")
            return not self.fatal

        return True


def check_dependencies(dependencies: Sequence[Dependency]):
    ok = True

    # Intentionally not short-circuiting: we want to list all missing
    # dependencies at once
    for dep in dependencies:
        if not dep.check():
            ok = False

    return ok
