import asyncio
import logging
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
import time
from string import Template

import asyncgelf

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
LOG_NAME = "RescueSyslog"

KEY_SYSTEM_SECTION = "System"
KEY_SYSLOG_SECTION = "Syslog"
KEY_SYSLOG_ENABLE = "Enable"
KEY_SYSLOG_HEARTBEAT_INTERVAL = "HeartbeatInterval"
KEY_SYSLOG_HOST = "Host"
KEY_SYSLOG_NAME = "Name"
KEY_SYSLOG_PORT = "Port"
KEY_SYSLOG_PROTOCOL = "Protocol"
SYSLOG_KEYS = [
    KEY_SYSLOG_ENABLE,
    KEY_SYSLOG_HEARTBEAT_INTERVAL,
    KEY_SYSLOG_HOST,
    KEY_SYSLOG_NAME,
    KEY_SYSLOG_PORT,
    KEY_SYSLOG_PROTOCOL,
]
KEY_DEVICE_SECTION = "Device"
KEY_DEVICE_SERIAL = "Serial"
KEY_DEVICE_YEAR = "Year"
KEY_DEVICE_BATCH = "Batch"
KEY_DEVICE_ART_NR = "Art"
KEY_DEVICE_MAJ_REF = "MajRev"
KEY_DEVICE_MIN_REF = "MinRev"
SERIAL_KEYS = [
    KEY_DEVICE_SERIAL,
    KEY_DEVICE_YEAR,
    KEY_DEVICE_BATCH,
    KEY_DEVICE_ART_NR,
    KEY_DEVICE_MAJ_REF,
    KEY_DEVICE_MIN_REF,
]

GRAYLOG_HOST = "RESCUE"
MESSAGE = "Message"
FACILITY = "Facility"
SEND_INTERVAL = 60
SYSLOG_TCP_CONFIG = "TCP"
SYSLOG_UDP_CONFIG = "UDP"
GRAYLOG_LEVEL = "7"
MESSAGE_TEMPLATE = f"""{GRAYLOG_HOST}:${FACILITY} ${MESSAGE}"""


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 getSyslogConfigKeys(config: dict) -> tuple[bool, int, str, str, int, str]:
    """Read the syslog config section from the config path.

    :param config: Config file as dictionary
    """
    configKeys = {
        KEY_SYSLOG_ENABLE: False,
        KEY_SYSLOG_HEARTBEAT_INTERVAL: 0,
        KEY_SYSLOG_HOST: "127.0.0.1",
        KEY_SYSLOG_NAME: "",
        KEY_SYSLOG_PORT: 12201,
        KEY_SYSLOG_PROTOCOL: "GELFTCP",
    }
    syslogSection = config.get(KEY_SYSTEM_SECTION, {}).get(KEY_SYSLOG_SECTION, {})
    if syslogSection:
        configKeys.update(syslogSection)
    return (
        configKeys[KEY_SYSLOG_ENABLE],
        configKeys[KEY_SYSLOG_HEARTBEAT_INTERVAL],
        configKeys[KEY_SYSLOG_HOST],
        configKeys[KEY_SYSLOG_NAME],
        configKeys[KEY_SYSLOG_PORT],
        configKeys[KEY_SYSLOG_PROTOCOL],
    )


def getDeviceSerial(config: dict) -> str:
    """Read the device serial from the config path.

    :param configFile: path to the config file
    """
    configKeys = {
        KEY_DEVICE_SERIAL: 0,
        KEY_DEVICE_YEAR: 0,
        KEY_DEVICE_BATCH: 0,
        KEY_DEVICE_ART_NR: 10000,
        KEY_DEVICE_MIN_REF: "A",
        KEY_DEVICE_MAJ_REF: 0,
    }
    deviceSection = config.get(KEY_DEVICE_SECTION, {})
    if deviceSection:
        configKeys.update(deviceSection)

    year = configKeys.get(KEY_DEVICE_YEAR)
    batch = configKeys.get(KEY_DEVICE_BATCH)
    serial = configKeys.get(KEY_DEVICE_SERIAL)
    art = configKeys.get(KEY_DEVICE_ART_NR)
    majRev = configKeys.get(KEY_DEVICE_MAJ_REF)
    minRev = configKeys.get(KEY_DEVICE_MIN_REF)
    identity = "{:0>2}{:0>2}{:0>4} {:0>5}.{}{}".format(
        year, batch, serial, art, majRev, minRev
    )
    return identity


async def sendHelpMessages(
    interval: float,
    protocol: str,
    host: str,
    port: int,
    version: str,
    level: str,
    domain: str,
    message: str,
    logger: logging.Logger,
):
    """Main task running in the background for sending syslog messages.

    :param interval: interval in seconds between sending messages
    :param protocol: Type of protocol to use (TCP, UDP)
    :param host: Host address to send to
    :param port: Port to send to
    :param version: GELF version
    :param level: Log level
    :param domain: Internal str to use as domain
    :param message: Message to send
    :param logger: Logger instance
    """

    while True:

        if SYSLOG_TCP_CONFIG in protocol:
            handler = asyncgelf.GelfTcp(
                host=host,
                port=port,
                gelf_version=version,
                level=level,
                debug=True,
            )
            handler.domain = domain
            await handler.tcp_handler([message], timestamp=time.time())
        elif SYSLOG_UDP_CONFIG in protocol:
            handler = asyncgelf.GelfUdp(
                host=host,
                port=port,
                gelf_version=version,
                level=level,
                debug=True,
            )
            handler.domain = domain
            await handler.udp_handler([message], timestamp=time.time())
        logger.info(f"Sent message to {host}:{port} using {protocol}")
        await asyncio.sleep(interval)


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}")

    enabled, heartbeat, host, name, port, protocol = getSyslogConfigKeys(config)
    heartbeat = heartbeat * 60  # heartbeat config is in minutes
    interval = heartbeat or SEND_INTERVAL

    identity = getDeviceSerial(config)
    bootMessage = "RescueOS booted"
    facility = name or identity
    params = {
        FACILITY: facility,
        MESSAGE: bootMessage,
    }
    message = Template(MESSAGE_TEMPLATE).substitute(params)
    logger.info(f"Prepare message: {message}")
    loop = asyncio.new_event_loop()
    loop.run_until_complete(
        sendHelpMessages(
            interval=interval,
            protocol=protocol,
            host=host,
            port=port,
            version="",
            level=GRAYLOG_LEVEL,
            domain=GRAYLOG_HOST,
            message=message,
            logger=logger,
        )
    )
