from json import loads, JSONDecodeError
from logging import getLogger, Logger, StreamHandler, INFO, basicConfig
from logging.handlers import RotatingFileHandler
from os import environ, chown
from pathlib import Path
from sys import stdout

from dbus_next import BusType, AuthError
from dbus_next.aio import MessageBus

APPLICATION_DEFAULT_PATH = "/mnt/persistent-data/evtec-ecp-data"
RESCUE_DEFAULT_PATH = "/mnt/rescue-data/evtec-ecp-data"
ECP_WORK_FILES_DIR_CANDIDATES = (
    environ.get("ECP_WORK_FILES_DIR", ""),
    APPLICATION_DEFAULT_PATH,
    RESCUE_DEFAULT_PATH,
)
"""The config is tried to be found in these folders with descending priority. The first config file found is taken.
Corresponding to the folder of the config file, the log file path is chosen.
"""

MM_SERVICE = "org.freedesktop.ModemManager1"
MM_PATH = "/org/freedesktop/ModemManager1"
MM_IFACE = "org.freedesktop.ModemManager1"
NETWORKD_SERVICE = "org.freedesktop.network1"
NETWORKD_PATH = "/org/freedesktop/network1"
NETWORKD_MANAGER_IFACE = "org.freedesktop.network1.Manager"
NETWORKD_LINK_IFACE = "org.freedesktop.network1.Link"
SIM_IFACE = "org.freedesktop.ModemManager1.Sim"
SYSTEMD_BUS_NAME = "org.freedesktop.systemd1"
SYSTEMD_PATH = "/org/freedesktop/systemd1"
SYSTEMD_MANAGER_IFACE = "org.freedesktop.systemd1.Manager"
UNIT_INTERFACE = "org.freedesktop.systemd1.Unit"

LOG_DIR = "log"
CONFIG_DIR = "config"
CONFIG_FILE = "config.json"
LOGGING_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
# defined by the YOCTO build
LOGFILE_OWNER_UID: int = 1000
LOGFILE_OWNER_GID: int = 1000


def createLogFile(logFilePath: Path):
    """Create the given path and change ownership to xfc user.

    :param logFilePath: Path to the log file
    """
    if not logFilePath.exists():
        logFilePath.touch()

    if (
        logFilePath.stat().st_uid != LOGFILE_OWNER_UID
        or logFilePath.stat().st_gid != LOGFILE_OWNER_GID
    ):
        chown(logFilePath, LOGFILE_OWNER_UID, LOGFILE_OWNER_GID)


def findPath() -> tuple[Path | None, dict]:
    """This function returns the first working path directory. It does so by iterating over the work dir candidates
    ECP_WORK_FILES_DIR_CANDIDATES and tries to find a working config file in its subpath. Returns the working file path
    when the config file is readable and can be json parsed otherwise return None.

    :return: the working path or None
    """
    workingDirectory = None
    config = {}
    for workdir in ECP_WORK_FILES_DIR_CANDIDATES:
        configFile = Path(workdir) / CONFIG_DIR / CONFIG_FILE
        if configFile.is_file():
            # Check readability
            try:
                config: dict = loads(configFile.read_text())
                isReadable = True
            except (
                FileNotFoundError,
                PermissionError,
                IsADirectoryError,
                JSONDecodeError,
            ):
                isReadable = False
            if isReadable:
                workingDirectory = workdir
                break
    return workingDirectory, config


def getConfigAndSetupLogging(
    logName: str, logLevel: int = INFO
) -> tuple[Path | None, dict, Logger]:
    """Check for a working directory and get its config file and do create a valid file rotation of the given log name.
    When there is no working directory found, only a logger is created to stdout.

    :param logName: The name of the logger to initialize
    """
    workPath, config = findPath()
    handlers = []
    streamHandler = StreamHandler(stream=stdout)
    streamHandler.setLevel(logLevel)
    handlers.append(streamHandler)
    if workPath:
        logFilePath = Path(workPath) / LOG_DIR / (logName + ".log")
        createLogFile(logFilePath)
        # (more or less 105 lines of logs) 4094 chars === 4kB / 64000 === 64kB
        # / 64kB * (1 + 2) = 192kb disk size
        fileHandler = RotatingFileHandler(logFilePath, maxBytes=64000, backupCount=2)
        fileHandler.setLevel(logLevel)
        handlers.append(fileHandler)
    basicConfig(level=logLevel, format=LOGGING_FORMAT, handlers=handlers)

    return workPath, config, getLogger(logName)


async def connectSystemBus(logger):
    """connect the system bus"""
    try:
        return await MessageBus(bus_type=BusType.SYSTEM).connect()
    except AuthError:
        logger.error("Authorization error happened while connecting the system bus")
    return False


async def getDbusIface(bus, service, path, interface, logger):
    """get the dbus interface"""
    try:
        introspect = await bus.introspect(service, path)
        obj = bus.get_proxy_object(service, path, introspect)
        return obj.get_interface(interface)
    except Exception as e:
        logger.exception(f"Failed to get interface of {interface} with traceback: {e}")
    return False
