#!/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 typing import List, 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("%s: Missing package %s\n" % (level, self.name))
            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
