import contextlib
import os
import re
import subprocess

from typing import Optional, Tuple, Union
from server.data.definitions import ECP_APP_FILES_DIR
from server.data.definitions import Paths
from server.utils.raucInterface import getRaucSlotInformation

BOOTED = "booted"
BUNDLE_VERSION = "bundle.version"
ECP_PACKAGE_REGEX = r"^Package: ecp$"
ECP_DEVELOPMENT_PACKAGE_REGEX = r"^Package: ecp-development$"
INSTALL_IN_PROGRESS = "Installation in progress..."
MSII_PACKAGE_REGEX = r"^Package: msi-interconnection-firmware"
OS_RELEASE_PACKAGE_REGEX = r"^VERSION_ID=(.*)$"
NOT_INSTALLED = "Not installed..."
RECOVERY_SLOT = "recovery.0"
STATE = "state"
VERSION_PREFIX = "Version:"
VERSION_SUFFIX = "-r0"
UNKNOWN_VERSION = "Unknown"


class VersionControl:
    """
    # COPIED from ECP
    This class is in charge of retrieving the ECP version during runtime

    By default, the version is read from the opkg status file.
    If there is a .git folder in ECP root, we assume it's a non packaged version (aka ECP-Dev)
    and we parse the git version.
    """

    _cachedVersionString = None
    """In order to read the version only once from filesystem, it is cached in a class variable"""

    @classmethod
    def getVersion(cls) -> str:
        """Get the ECP version from opkg or git

        :return: the version
        """
        if cls._cachedVersionString is not None:
            # do not parse version info if cache is available
            return cls._cachedVersionString

        # get version from git or opkg
        version = (
            VersionControl._readGitVersion()
            or VersionControl._readOpkgVersion(ECP_PACKAGE_REGEX)
            or VersionControl._readOpkgVersion(ECP_DEVELOPMENT_PACKAGE_REGEX)
            or UNKNOWN_VERSION
        )

        cls._cachedVersionString = version
        return version

    @staticmethod
    def _readOpkgVersion(packageRegex: str) -> Optional[str]:
        """Read version from opkg status file.

        The file has the following structure:

            Package: <PKG-NAME>
            Version: <VERSION>
            Depends: bash,
            Status: install ok installed
            Architecture: armv7at2hf-neon
            Installed-Size: 12760419
            Installed-Time: 1667215309

            Package: <PKG-NAME>
            ...

        :return: version or `None` on parsing error
        """
        with contextlib.suppress(FileNotFoundError, IsADirectoryError):
            with open(Paths.STATUS_FILE.value) as f:
                packageFound = False
                for line in f:
                    if re.search(packageRegex, line, re.MULTILINE):
                        # found correct package, version must be in the next few lines
                        packageFound = True
                    elif packageFound and line.strip() == "":
                        # package-blocks are separated by an empty line,
                        # this condition prohibits reading the version from another package.
                        return
                    elif packageFound and VERSION_PREFIX in line:
                        line = line.replace(VERSION_SUFFIX, "")
                        return line.replace(VERSION_PREFIX, "").strip()

    @staticmethod
    def _readOsRelease(versionRegex: str) -> Optional[str]:
        """Read the os-release version

        :param versionRegex:asd
        :return: os-release version or 'UNKNOWN' if not found
        """
        with contextlib.suppress(FileNotFoundError, IsADirectoryError):
            with open(Paths.OS_RELEASE.value) as f:
                for line in f:
                    if re.match(versionRegex, line):
                        return line.split("=")[1]
            return UNKNOWN_VERSION

    @staticmethod
    async def getRaucVersions() -> Union[Tuple[str, str], Tuple[None, None]]:
        """
        It retrieves via d-bus API for the rauc service the os version and rescue os version
        The interaction is equivalent to the commandline execution 'rauc status --detailed'
        """
        # Define the fallback version
        osRelease = UNKNOWN_VERSION
        rescueOsVersion = NOT_INSTALLED

        # Parse information of the rauc over d-bus api
        raucSlotInfo = await getRaucSlotInformation()

        if raucSlotInfo:
            # Parse the booted slot version and the recovery os version
            for key, value in raucSlotInfo.items():
                if value.get(STATE) == BOOTED:  # Check if state exists and is "booted"
                    osRelease = value.get(BUNDLE_VERSION, UNKNOWN_VERSION)  # Get os bundle.version

                if key == RECOVERY_SLOT:
                    rescueOsVersion = value.get(BUNDLE_VERSION, NOT_INSTALLED)  # Get recovery bundle.version

        else:
            osRelease = None
            rescueOsVersion = None

        return osRelease, rescueOsVersion

    @staticmethod
    def readMsiVersion() -> Optional[str]:
        return VersionControl._readOpkgVersion(MSII_PACKAGE_REGEX)

    @staticmethod
    def readOsRelease() -> Optional[str]:
        return VersionControl._readOsRelease(OS_RELEASE_PACKAGE_REGEX)

    @staticmethod
    def _readGitVersion() -> Optional[str]:
        if not (os.path.isfile(os.path.join(ECP_APP_FILES_DIR, ".git"))):
            return None
        with contextlib.suppress(Exception):
            # if run from dev, we have an additional `src` folder
            p = subprocess.Popen(
                args=[ECP_APP_FILES_DIR / "scripts/getversionstring"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=ECP_APP_FILES_DIR,
            )
            stdOut, _ = p.communicate()
            devVersion = stdOut.decode().strip()
            if len(devVersion) > 3:  # keep "Unknown" on error
                return devVersion
