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

from aiohttp import web

PORT = 8090
RESPONSE_CODE = 200
REMOTE_DISPLAY_1 = "RemoteDisplay1"
REMOTE_HMI_1 = "RemoteHMI1"
REMOTE_HMI_2 = "RemoteHMI2"
DISPLAY_ROLE_LOCAL_HMI = "ControllerLocalHMI"  # for crema&charge with controller board, we can directly show picture
REMOTE_HMI_ROLES = [REMOTE_HMI_1, REMOTE_HMI_2]
DISPLAY_ROLES = [REMOTE_DISPLAY_1, REMOTE_HMI_1, REMOTE_HMI_2, DISPLAY_ROLE_LOCAL_HMI]
CONFIG_KEY_DEVICE_SECTION = "Device"
CONFIG_KEY_ROLE = "Role"
CONFIG_KEY_TYPE = "Type"
CONFIG_KEY_SERIAL_SECTION = "Serial"
CONFIG_KEY_SERIAL_NUMBER = "Serial"
CONFIG_KEY_USER_INTERFACE_SECTION = "UserInterface"
CONFIG_KEY_DEVICE_VIEW = "DeviceView"
DEFAULT_DEVICE_SERIAL_NUMBER = 0
DEFAULT_DEVICE_ROLE = "ControllerRemoteDisplay"
DEFAULT_DEVICE_TYPE = "espresso&charge"
DEFAULT_DEVICE_VIEW = "ec"

ICON_NAME = "icon-coffeecup-shattered-"
DEVICE_VIEW_UFC_300 = "ufc300"
PRE_DEVICES = ["CM", "PRE300", "PRE360AIO"]
RESCUE_INDEX = "rescue.html"
LOG_NAME = "RescueDisplay"
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.
"""


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)


def getConfigKeys(config: dict) -> tuple[str, str, int, str]:
    """Read the device role from the config file given environment variable

    :param config: The config file as dict
    """
    deviceSection = config.get(CONFIG_KEY_DEVICE_SECTION, {})
    serialSection = deviceSection.get(CONFIG_KEY_SERIAL_SECTION, {})
    userInterfaceSection = config.get(CONFIG_KEY_USER_INTERFACE_SECTION, {})
    return (
        deviceSection.get(CONFIG_KEY_ROLE, DEFAULT_DEVICE_ROLE),
        deviceSection.get(CONFIG_KEY_TYPE, DEFAULT_DEVICE_TYPE),
        serialSection.get(
            CONFIG_KEY_SERIAL_NUMBER,
            DEFAULT_DEVICE_SERIAL_NUMBER,
        ),
        userInterfaceSection.get(CONFIG_KEY_DEVICE_VIEW, DEFAULT_DEVICE_VIEW),
    )


def showStaticPicture(picturePath: Path) -> None:
    """Use the framebuffer directly to show the static picture.

    :param picturePath: Path to the static picture with correct resolution
    """
    showPictureProcess = Popen(
        args=["dd", f"if={picturePath.absolute()}", "of=/dev/fb0"],
        stdout=PIPE,
        stderr=PIPE,
    )
    stdout, stderr = showPictureProcess.communicate()
    if stderr:
        logger: Logger = getLogger(LOG_NAME)
        logger.info(stderr.decode())


async def indexHandler(request: web.Request) -> web.Response:
    """Handles GET requests and serves the HTML index page with CORS headers.

    :param request: The incoming HTTP GET request.
    :type request: web.Request
    :return: HTTP response containing HTML content and CORS headers.
    :rtype: web.Response
    """

    htmlFile = "<html><body>500</body></html>"
    sourcePath = Path(__file__).parent / RESCUE_INDEX
    if sourcePath.is_file():
        with open(sourcePath, "r") as file:
            htmlFile = file.read()
    return web.Response(
        text=htmlFile,
        content_type="text/html",
        headers={
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, OPTIONS",
            "Access-Control-Allow-Headers": "*",
        },
    )


async def optionsHandler(request: web.Request) -> web.Response:
    """Handles CORS preflight (OPTIONS) requests and returns with necessary headers.

    :param request: The incoming HTTP OPTIONS request.
    :type request: web.Request
    :return: HTTP response with only CORS headers.
    :rtype: web.Response
    """

    return web.Response(
        headers={
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, OPTIONS",
            "Access-Control-Allow-Headers": "*",
        }
    )


def setGPIO(logger: Logger, path: str, value: str):
    try:
        with open(path, "w") as gpio:
            gpio.write(value)
    except (OSError, IOError) as e:
        logger.info(f"Error setting GPIO: {e}")


def main():
    # Find which config file to use
    _, config, logger = getConfigAndSetupLogging(LOG_NAME, INFO)

    # setup logging takes care of creating a log file
    logger.info(f"Config found {config}")

    deviceRole, deviceType, deviceSerial, deviceView = getConfigKeys(config)
    logger.info(
        f"Config Keys: Role:{deviceRole}, Type:{deviceType}, Serial:{deviceSerial}, DeviceView:{deviceView}"
    )
    if deviceRole in DISPLAY_ROLES:
        parentDir = Path(__file__).parent
        resolution800x400 = parentDir / (ICON_NAME + "800x400.raw")
        resolution1024x768 = parentDir / (ICON_NAME + "1024x768.raw")
        resolution1280x800 = parentDir / (ICON_NAME + "1280x800.raw")
        if deviceView == DEVICE_VIEW_UFC_300:
            showStaticPicture(resolution1024x768)
        elif deviceRole == DISPLAY_ROLE_LOCAL_HMI:
            showStaticPicture(resolution1280x800)
        elif deviceType in PRE_DEVICES:
            showStaticPicture(resolution1280x800)
        else:
            showStaticPicture(resolution800x400)
    else:
        setGPIO(logger, "/sys/class/gpio/gpio161/value", "1")
        setGPIO(logger, "/sys/class/gpio/gpio159/value", "1")
        setGPIO(logger, "/sys/class/gpio/gpio158/value", "1")
        app = web.Application()
        routes = ["", "/", "/index.html", "/index.htm"]

        for route in routes:
            app.router.add_route("GET", route, indexHandler)
            app.router.add_route("OPTIONS", route, optionsHandler)

        web.run_app(app, host="0.0.0.0", port=PORT)


if __name__ == "__main__":
    """Main entrypoint, any unhandled exceptions will lead to non zero exit code"""
    main()
