import asyncio

from dbus_next import BusType, DBusError, InvalidObjectPathError
from dbus_next.aio import MessageBus
from enum import Enum
from typing import Dict, Any, List, Union


class Operation(Enum):
    """Possible RAUC operational states"""

    IDLE = "idle"
    INSTALLING = "installing"
    UNKNOWN = "unknown"


class Rauc:
    """Interfaces with the system RAUC daemon via d-bus"""

    def __init__(self):
        self._bus = None
        self._installer = None

    async def connect(self) -> None:
        """Connect to d-bus"""
        self._bus = await MessageBus(bus_type=BusType.SYSTEM).connect()

        introspection = await self._bus.introspect("de.pengutronix.rauc", "/")
        proxy = self._bus.get_proxy_object("de.pengutronix.rauc", "/", introspection)
        self._installer = proxy.get_interface("de.pengutronix.rauc.Installer")

    async def getSlotStatus(self) -> Dict[str, Dict[str, Any]]:
        """Get the status of all slots

        Example:
        {
        "rootfs.0": {
            "installed.timestamp": "2023-06-20T14:10:03Z",
            "size": 357381084,
            "sha256": "c69f8b6cca06fa39614904d7ea93d240cd5ca7145a7c3e2d09c29ec6afec41aa",
            "bundle.build": "20230620062026",
            "bundle.compatible": "Evtec Rauc Update",
            "installed.count": 3,
            "bundle.version": "1.0",
            "type": "ext4",
            "bootname": "A",
            "class": "rootfs",
            "activated.count": 3,
            "bundle.description": "evtec-image-update-bundle version 1.0-r0",
            "boot-status": "good",
            "activated.timestamp": "2023-06-20T14:10:03Z",
            "mountpoint": "/",
            "state": "booted",
            "status": "ok",
            "device": "/dev/mmcblk0p2"
        }

        """
        result: List = await self._installer.call_get_slot_status()
        # transform [["slotName", {"paramName": Variant, ...}] ...] to plain dict
        parsed = dict(result)
        for slotName, slotData in parsed.items():
            parsed[slotName] = {paramName: param.value for paramName, param in slotData.items()}

        return parsed

    async def getPrimarySlot(self) -> str:
        """Get the currently selected boot slot

        :return: e.g. 'rootfs.1'
        """
        result: str = await self._installer.call_get_primary()
        return result

    async def getOperation(self) -> Operation:
        """
        Retrieves the rauc operation status via the d-bus API.

        :return: one of the known operation states
        """
        result: str = await self._installer.get_operation()
        try:
            return Operation(result)
        except ValueError:
            return Operation.UNKNOWN

    async def getLastError(self) -> str:
        """
        Retrieves the last error during a rauc installation.

        :return: The error or exception as string
        """
        result: str = await self._installer.get_last_error()
        return result

    async def installBundle(self, bundleFile: str) -> None:
        """Install the specified bundle in not booted slot

        RAUC daemon will start the process in the background

        :param bundleFile: path to bundle file
        """
        await self._installer.call_install_bundle(bundleFile, {})

    async def getProgress(self) -> str:
        """
        During an installation of the rauc bundle the progress of the installation progress is retrieved via the d-bus API

        :return: A string with the response of the rauc progress
        """
        percentage, message, nestingDepth = await self._installer.get_progress()
        return f"{percentage:3d}% ({message})"

    async def disconnect(self):
        if self._bus:
            self._bus.disconnect()
            await self._bus.wait_for_disconnect()


async def getRaucOperation() -> Union[Operation, None]:
    """
    Retrieves the rauc operation status via the d-bus API.

    :return: rauc operation status
    """
    try:
        rauc = Rauc()
        await rauc.connect()

        # check if RAUC is idle, otherwise status is not available
        operation = await rauc.getOperation()

        await rauc.disconnect()
        return operation
    except (AttributeError, DBusError, InvalidObjectPathError, TypeError, IndexError, EOFError) as e:
        print(e)


async def getRaucProgress() -> Union[str, None]:
    """
    During an installation of the rauc bundle the progress of the installation progress is retrieved via the d-bus API

    :return: A string with the response of the rauc progress
    """
    try:
        rauc = Rauc()
        await rauc.connect()

        progress = await rauc.getProgress()

        await rauc.disconnect()
        return progress
    except (AttributeError, DBusError, InvalidObjectPathError, TypeError, IndexError, EOFError) as e:
        print(e)


async def getRaucInformation() -> Union[Dict, None]:
    """
    Retrieves via d-bus API the pure raucslot information of the available slots.
    Additionally, extends the information about 'operation, primary, last-error, progress'

    :return: A dict with the response of the rauc information and extension
    """
    try:
        rauc = Rauc()
        await rauc.connect()

        # check if RAUC is idle, otherwise status is not available
        operation = await rauc.getOperation()
        status: Dict = await rauc.getSlotStatus()
        status["operation"] = operation.value
        pSlot = await rauc.getPrimarySlot()
        status["primary"] = pSlot
        lastError = await rauc.getLastError()
        status["last-error"] = lastError
        status["progress"] = ""

        await rauc.disconnect()
        return status
    except (AttributeError, DBusError, InvalidObjectPathError, TypeError, IndexError, EOFError) as e:
        print(e)


async def getRaucSlotInformation() -> Union[Dict, None]:
    """
    Retrieves via d-bus API the pure raucslot information of the available slots.
    Hint: When rauc operation is not IDLE e.g. during an installation, the response of the slots isn't available.

    :return: rauc slot information or None when an installation is ongoing.
    """
    try:
        rauc = Rauc()
        await rauc.connect()
        status = None

        operation = await rauc.getOperation()

        if operation.value == Operation.IDLE.value:
            status: Dict = await rauc.getSlotStatus()

        await rauc.disconnect()
        return status
    except (AttributeError, DBusError, InvalidObjectPathError, TypeError, IndexError, EOFError) as e:
        print(e)


async def installRaucBundle(bundleFile: str) -> None:
    """
    Coroutine for the installation of Rauc Bundle via d-bus API.

    :param bundleFile: path or URL to bundle file
    """
    try:
        rauc = Rauc()
        await rauc.connect()

        await rauc.installBundle(bundleFile)

        await rauc.disconnect()
    except (AttributeError, DBusError, InvalidObjectPathError, TypeError, IndexError, EOFError) as e:
        print(e)
