import threading
from enum import Enum
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
import sys
from typing import List
import asyncio
import json
import aiohttp

OTA_ECP3_LOG = "ecp-ota-updater-ecp3"
OTA_RESCUE_LOG = "ecp-ota-updater-rescue-os"
OTA_ECP4_LOG = "ecp-ota-updater-ecp4"

MNT_PERSISTENT_DATA = "/mnt/persistent-data/"
OTA_ECP_DATA = "ota-ecp-data"
OTA_ECP_DOWNLOAD = "ota-ecp-download"
EVTEC_ECP_DATA = "evtec-ecp-data"

LOG_FOLDER = "log"
CONFIG_FOLDER = "config"

ECP3_DMS_PORT = 8888  # DMS port on ECP3 side

DISPLAY_API_PORT = 3030
OTA_MAIN_BOARD_API_PORT = 3030
OTA_MAIN_BOARD_END_POINT = "state"

OTA_DATA_FOLDER = Path(MNT_PERSISTENT_DATA) / OTA_ECP_DATA
OTA_DOWNLOAD_FOLDER = Path(MNT_PERSISTENT_DATA) / OTA_ECP_DOWNLOAD
EVTEC_ECP_DATA_FOLDER = Path(MNT_PERSISTENT_DATA) / EVTEC_ECP_DATA

RESCUE_RAUCB_ARCHIVE_NAME = "rescue.raucb"  # do not change! (name is linked with otadebpayload/settings.py -> RESCUE_RAUCB_ARCHIVE_NAME)
RESCUE_RAUCB_ARCHIVE_PATH = (
    Path(MNT_PERSISTENT_DATA) / OTA_ECP_DOWNLOAD / RESCUE_RAUCB_ARCHIVE_NAME
)

SCREEN_PORT = 8090
SCREEN_TEMPLATE = "otaecp4-firmware-update.html"
SCREEN_STATUS_FILE = "status.json"
CLEANUP_STATE = "cleanup-state"
BROWSER_RELOAD_KEYWORD = "BROWSER-RELOAD"

logger = logging.getLogger(OTA_ECP4_LOG)

stopFirmwareUpdateScreenEvent = threading.Event()


class DeviceType(str, Enum):
    """
    Device types as configured under "Device/Type"
    """

    BRICCO = "bricco&charge"
    CAPPUCCINO = "cappuccino&charge"
    COFFEE = "coffee&charge"
    CORRETTO = "corretto&charge"
    CREMA = "crema&charge"
    ESPRESSO = "espresso&charge"
    MOKA = "moka&charge"
    MOVE = "move&charge"
    RISTRETTO = "ristretto&charge"
    SHOP = "shop&charge"
    SLEEP = "sleep&charge"
    SOSPESO = "sospeso&charge"
    WORK = "work&charge"
    UFC = "Ultra Fast Charger"
    UFC_500 = "UFC500"
    POWER_MANAGER = "Power Manager"
    AFC = "AFC"
    CHARGE_MASTER = "CM"
    PRE300 = "PRE300"
    PRE360AIO = "PRE360AIO"


class DeviceRole(str, Enum):
    CONTROLLER_REMOTE_DISPLAY = "ControllerRemoteDisplay"
    CONTROLLER_LOCAL_HMI = "ControllerLocalHMI"
    REMOTE_DISPLAY_1 = "RemoteDisplay1"
    REMOTE_HMI_1 = "RemoteHMI1"
    REMOTE_HMI_2 = "RemoteHMI2"
    POWER_SINK_CONTROL = "PowerSinkControl"

    def __str__(self):
        return self.value


class OtaState(str, Enum):
    """
    Macroscopic set of OTA state of a display board.

    These states could be sent to base board
    """

    NOTIFY_START_OTA_ECP3 = "START_OTA_ECP3"
    NOTIFY_COMPLETE_OTA_ECP3 = "COMPLETE_ECP3"
    NOTIFY_BOOT_RESCUE = "BOOT_RESCUE"
    NOTIFY_COMPLETE_RESCUE = "COMPLETE_RESCUE"
    NOTIFY_BOOT_ECP4 = "BOOT_ECP4"
    NOTIFY_COMPLETE_ECP4 = "COMPLETE_ECP4"


class MsiServices(str, Enum):
    ECP = "ECP"
    HMI_CONTROL = "HmiControl"
    ANDCHARGE_DISPLAY = "AndchargeDisplay"
    CM_DISPLAY = "CMDisplay"
    UFC300_DISPLAY = "1024x768Display"
    METER_CONTROL = "MeterControl"
    DCMS_CONTROL = "DCMSControl"
    WATCHDOG_SERVER = "WatchdogServer"
    POWER_SINK_CONTROL = "PowerSinkControl"

    def __str__(self):
        return self.value


def logHeader(text: str):
    logger.info(f"\n".ljust(100, "-"))
    logger.info(f"{text}")
    logger.info(f"".ljust(99, "-"))


def setupLogging():

    if not OTA_DATA_FOLDER.exists():
        OTA_DATA_FOLDER.mkdir()

    fileHandler = RotatingFileHandler(
        EVTEC_ECP_DATA_FOLDER / "log" / f"{OTA_ECP4_LOG}.log"
    )
    fileHandler.setFormatter(
        logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
    )

    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(logging.Formatter("%(message)s"))
    # add handlers to root logger
    logging.basicConfig(level=logging.DEBUG, handlers=[fileHandler, handler])

    return


async def addStatusEntry(message: str, delay=5):
    filePath = OTA_DATA_FOLDER / SCREEN_STATUS_FILE

    if filePath.exists() and filePath.__sizeof__() > 0:
        try:
            data = readJsonContentFromFile(filePath)
            messages = data.get("status", [])

        except (json.JSONDecodeError, KeyError):
            messages = []
    else:
        messages = []

    messages.append(message)

    # Write back the updated messages to the file
    with open(filePath, "w") as file:
        json.dump({"status": messages}, file)

    await asyncio.sleep(delay)


def buildIpAddressFromConfig(serial: int, role: DeviceRole) -> str:
    """
    Generate the IP address in the 10.2.x.x network from the serial number and the role.

    :param serial: serial number of the device
    :param role: role of the board configuration
    :return: IP address of the device with the specified serial number and role.
    """

    ipAddress = "10.2.{}.{}"
    lowByte = int(((serial - 1) % 100) + 1)
    highByte = int(((serial - 1) / 100) % 100)

    if role in [DeviceRole.CONTROLLER_REMOTE_DISPLAY, DeviceRole.CONTROLLER_LOCAL_HMI]:
        highByte = int(((serial - 1) / 100) % 100)
    elif role == DeviceRole.REMOTE_HMI_1:
        highByte = int(((serial - 1) / 100) % 100) + 100
    elif role == DeviceRole.REMOTE_HMI_2:
        highByte = int(((serial - 1) / 100) % 100) + 200

    return ipAddress.format(highByte, lowByte)


def readJsonContentFromFile(filePath):
    """
    Read json file from filePath and return it as dictionary.

    :param filePath: path to json file
    :return: file content as dictionary
    """
    content = {}
    try:
        with open(filePath, "r") as file:
            content = json.loads(file.read())
    except FileNotFoundError:
        logger.info("File {} not found for reading json content".format(filePath))
    except json.decoder.JSONDecodeError:
        logger.info("Failed to read file {} as JSON".format(filePath))
    except TypeError as e:
        logger.info("Failed to read {} with {}".format(filePath, e))
    return content


async def runSubprocess(command: List[str]) -> None:
    """
    :param command: eg. ["systemctl", "start", "example.service"]
    :return: None
    """

    # Run a subprocess asynchronously
    await asyncio.create_subprocess_exec(
        *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
    )


def getDisplayService(configuredServices: []):
    """
    Returns the first matching display service from the configured services,
    defaulting to ANDCHARGE_DISPLAY if none are found.

    :param configuredServices: List of configured services based on the device configuration
    :return: Active display service name.
    """

    possibleDisplayServices = [
        MsiServices.CM_DISPLAY,
        MsiServices.UFC300_DISPLAY,
        MsiServices.ANDCHARGE_DISPLAY,
    ]
    match = set(possibleDisplayServices) & set(configuredServices)
    return next(iter(match), MsiServices.ANDCHARGE_DISPLAY)


async def fetchUrl(
    session: aiohttp.ClientSession, url: str, data: dict = None, method: str = "GET"
) -> dict | None:
    """Fetches JSON data from the given URL using GET or POST, handling cancellation."""
    try:
        async with session.request(method, url, json=data, timeout=10) as response:
            if response.status == 200:
                jsonResponse = await response.json()
                logger.info(
                    f"{method} request {url} {'with ' + str(data['message']) + '' if data is not None else ''}-> {jsonResponse['message']}"
                )
                return jsonResponse
    except aiohttp.ClientConnectorError:
        pass  # Ignore connection errors
    except aiohttp.ClientResponseError as e:
        logger.error(f"HTTP error {e.status} for {url}: {e.message}")
    except aiohttp.ServerTimeoutError:
        logger.error(f"Timeout error: {url} did not respond in time.")
    except asyncio.TimeoutError:
        logger.error(f"Async timeout: {url} took too long to respond.")
    except aiohttp.ClientError as e:
        logger.error(f"General aiohttp error for {url}: {e}")
    except asyncio.CancelledError:
        logger.info(f"Cancelled {url}.")
        raise  # Ensure task cancellation works correctly
    except Exception as e:
        logger.error(f"Unexpected error with {url}: {e}")
    return None


async def sendGetRequest(url: str, retries: int = 10, sleep: int = 10) -> bool:
    """Retries fetching data from the given URL, allowing cancellation."""
    async with aiohttp.ClientSession() as session:
        for attempt in range(retries):
            try:
                response = await fetchUrl(session, url)
                if response:
                    return True
                logger.info(f"Retrying {url} ({attempt+1}/{retries})")
                await asyncio.sleep(sleep)  # Can be cancelled
            except asyncio.CancelledError:
                logger.warning(f"sendGetRequest to {url} was cancelled.")
                return False  # Exit immediately on cancellation
    return False
