"""
This python module holds methods for the OTA main board
to check or control one or several displays.
"""

import asyncio
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
import logging
import json
import threading
import re
from urllib import request, error
from .utils import (
    OTA_ECP4_LOG,
    ECP3_DMS_PORT,
    DISPLAY_API_PORT,
    OtaState,
    addStatusEntry,
    OTA_DATA_FOLDER,
)

logger = logging.getLogger(OTA_ECP4_LOG)


def isBoardAvailable(ipAddress: str, port=ECP3_DMS_PORT) -> bool:
    """
    Check if a board is available by pinging their DMS
    """
    try:
        req = request.Request("http://{}:{}".format(ipAddress, port))
        with request.urlopen(req) as response:
            _ = response.read()  # bytes
    except error.URLError:
        logger.info("Could not reach device with IP address {}".format(ipAddress))
        return False
    else:
        logger.info(
            "Successfully obtained response from IP address: {}".format(ipAddress)
        )
        return True


def thread_function(server: HTTPServer):
    logger.info("Starting HTTP server for display boards to update their progress.")
    server.serve_forever()


class OtaStatusMonitor:
    """
    Data container to keep track of display status
    """

    def __init__(self, otaDoneEvent: threading.Event, clients: list[str]):
        self.clients: set[str] = set(clients)
        self.state: dict[str, str] = {c: "UNKNOWN" for c in self.clients}
        self.otaDoneEvent: threading.Event = otaDoneEvent
        self.completedClients: list[str] = []
        logger.info(f"Created OTA status monitor clients: {self.clients}")

    def updateClientState(self, client: str, state: str) -> None:
        """
        Update OtaStatus of one of the clients.
        """
        self.state[client] = state
        logger.info(f"{client} updated to {state}")

        if state == OtaState.NOTIFY_COMPLETE_ECP4:
            self.completedClients.append(client)

    def getAllStates(self) -> dict[str, str]:
        """
        Get states from all connected clients.

        Can be used for manual observation
        """
        return self.state

    def checkOtaDone(self):
        """
        Checks if all clients have completed their OTA flow and if so, sets otaDoneEvent.
        """
        numberOfClients = len(self.clients)
        numberOfCompletedClients = len(self.completedClients)

        if numberOfClients == numberOfCompletedClients:
            self.otaDoneEvent.set()


def buildHandler(statusMonitor: OtaStatusMonitor) -> BaseHTTPRequestHandler:
    class OtaRequestHandler(BaseHTTPRequestHandler):
        def log_message(self, format, *args):
            pass  # Override to suppress logging

        def do_POST(self):
            if self.path == "/state":
                client_ip, _ = self.client_address
                data = self.rfile.read(int(self.headers["Content-Length"]))
                dataDecoded = json.loads(data.decode("utf-8"))
                statusMonitor.updateClientState(client_ip, dataDecoded["message"])
                statusMonitor.checkOtaDone()
                self.send_response(200)
                self.send_header("Content-type", "application/json")
                self.end_headers()
                self.wfile.write(
                    json.dumps({"message": "data received"}).encode("utf-8")
                )

        def do_GET(self):
            if self.path == "/update":
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                self.wfile.write(json.dumps(statusMonitor.getAllStates()).encode())

            if self.path == "/restartDisplayService":
                self.send_response(200)
                self.send_header("Access-Control-Allow-Origin", "*")
                self.send_header("Content-type", "application/json")
                self.end_headers()
                response_data = {"message": "Restart display service"}
                self.wfile.write(json.dumps(response_data).encode())
                subprocess.Popen(["systemctl", "restart", "CMDisplay"])

    return OtaRequestHandler


def startServer(
    port: int, otaDoneEvent: threading.Event, clients: list[str]
) -> tuple[HTTPServer, OtaStatusMonitor]:
    """
    Starts a server for remote boards to send their status
    """

    statusContainer = OtaStatusMonitor(otaDoneEvent=otaDoneEvent, clients=clients)
    requestHandler = buildHandler(statusContainer)

    server = HTTPServer(("0.0.0.0", port), requestHandler)

    t = threading.Thread(target=thread_function, args=(server,))
    t.start()

    return server, statusContainer


def extract_ip(filename):
    # Define a regex pattern to match an IP address in the format X.X.X.X
    pattern = r"(\d{1,3})_(\d{1,3})_(\d{1,3})_(\d{1,3})"
    match = re.search(pattern, filename)
    if match:
        # Join the matched groups with dots to form the IP address
        return ".".join(match.groups())
    return None


def parseInventoryForConnectedHosts() -> list[str | None]:
    """
    Parse the inventory for connected hosts using backed up config files.
    """
    connectedBoards = []
    for item in OTA_DATA_FOLDER.iterdir():
        # search for all files named config_<ip>.json
        if (
            item.is_file()
            and item.name.startswith("config")
            and not item.name == "config.json"
        ):
            ipAddress = extract_ip(item.name)
            connectedBoards.append(ipAddress)

    if not connectedBoards:
        logger.info(f"No connected hosts found in {OTA_DATA_FOLDER}")
    if connectedBoards:
        logger.info(f"Found connected boards: {connectedBoards} in {OTA_DATA_FOLDER}")

    return connectedBoards
