import asyncio
from contextlib import AbstractAsyncContextManager
import copy
import hashlib
import datetime
import socket
import struct
import email.utils
import uuid
import re
import sys
import logging
import logging.handlers
import json
from .defaults import ALL, GETSTATE

M_SEARCH_REQ_MATCH = "M-SEARCH"

UPNP_BROADCAST = """NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=100
LOCATION: http://{}:{}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
NTS: ssdp:alive
NT: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-{}::upnp:rootdevice

""".replace("\n", "\r\n")


# IP, PORT, ST
UPNP_RESPOND_TEMPLATE = """HTTP/1.1 200 OK
CACHE-CONTROL: max-age=100
EXT:
LOCATION: http://{}:{}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
ST: {}
USN: uuid:2f402f80-da50-11e1-9b23-{}::upnp:rootdevice

""".replace("\n", "\r\n")

# TODO: jimboca: check others to see if they are specifying this?
# CACHE-CONTROL: max-age=86400
# EXT:
# LOCATION: http://{}:{}/description.xml
# SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
# ST: urn:schemas-upnp-org:device:basic:1
# USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1

# updated modelName and removed extra tabs and \r to match examples on web
DESCRIPTION_XML = """HTTP/1.1 200 OK
Content-type: text/xml
Connection: Keep-Alive

<?xml version="1.0" encoding="UTF-8" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{}:{}/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>Philips hue ({})</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2012</modelName>
<modelNumber>929000226503</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>{}</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-{}</UDN>
<serviceList>
<service>
<serviceType>(null)</serviceType>
<serviceId>(null)</serviceId>
<controlURL>(null)</controlURL>
<eventSubURL>(null)</eventSubURL>
<SCPDURL>(null)</SCPDURL>
</service>
</serviceList>
<presentationURL>index.html</presentationURL>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<height>48</height>
<width>48</width>
<depth>24</depth>
<url>hue_logo_0.png</url>
</icon>
<icon>
<mimetype>image/png</mimetype>
<height>120</height>
<width>120</width>
<depth>24</depth>
<url>hue_logo_3.png</url>
</icon>
</iconList>
</device>
</root>
""".replace("\n", "\n")  # was \r\n

# 20150920-Added in case it is used for discovery
APICONFIG_JSON = """
[{"swversion":"01008227","apiversion":"1.2.1","name":"Smartbridge 1","mac":"%s",}]
"""

NEWDEVELOPERSYNC_JSON = """
[{"success":{"%s":"c6260f982b43a226b5542b967f612ce"}}]
""" % ("username")

# example template values: "success", "1", "on", "true"
PUTRESP_TEMPLATE_JSON = """
[{"%s":{"/lights/%s/state/%s":%s}}]
"""

JSON_HEADERS = """HTTP/1.1 200 OK
CONTENT-LENGTH: %d
CONTENT-TYPE: application/json charset="utf-8"
DATE: %s
EXT:
SERVER: Unspecified, UPnP/1.0, Unspecified
CONNECTION: close

%s"""


ICON_HEADERS = """HTTP/1.1 200 OK
Content-type: image/png

""".replace("\n", "\r\n")

ICON_SMALL = b"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"
ICON_BIG = b" "


class Broadcaster:
    def __init__(self, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

        self.sock.bind((self.config["IP"], 0))
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        self.logger.info("Starting broadcast loop")
        await self.loop()  # Start the loop broadcast task

    async def loop(self):
        while True:
            self.logger.debug("Sending broadcast")
            await self.event_loop.sock_sendto(
                self.sock,
                UPNP_BROADCAST.encode(),
                (self.config["BCAST_IP"], self.config["UPNP_PORT"]),
            )
            await asyncio.sleep(self.config["BROADCAST_INTERVAL"])

    async def stop(self):
        self.logger.debug("Stopping broadcast loop")
        await asyncio.to_thread(self.sock.close)


class Responder:
    def __init__(self, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(("", self.config["UPNP_PORT"]))
        mreq = struct.pack(
            "4sl", socket.inet_aton(self.config["BCAST_IP"]), socket.INADDR_ANY
        )
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
        self.sock.setblocking(False)

        # Issue 9: create separate response socket bound to assigned interface
        self.sockresp = socket.socket(
            socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP
        )
        self.sockresp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sockresp.bind((self.config["IP"], self.config["UPNP_PORT"]))
        self.sock.setblocking(False)

        self.logger.info("Starting response loop")
        await self.loop()  # Start the loop response task

    async def loop(self):
        while True:
            try:
                self.logger.debug("Waiting for M-SEARCH")
                data, addr = await self.event_loop.sock_recvfrom(self.sock, 1024)
                data = data.decode()  # type: ignore
            # if socket closed by stop() method
            except ConnectionResetError:
                break
            except socket.error as e:
                if e.winerror == 995:
                    break
                else:
                    self.logger.error(e)
                    continue
            else:
                if M_SEARCH_REQ_MATCH in data:
                    self.logger.debug("Received M-SEARCH from {}".format(addr))

                    if "urn:schemas-upnp-org:device:basic:1" in data:
                        self.logger.debug(
                            "received urn:schemas-upnp-org:device:basic:1"
                        )

                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config["IP"],
                            self.config["HTTP_PORT"],
                            "urn:schemas-upnp-org:device:basic:1",
                            self.config["SERIALNO"],
                        )

                        await self.event_loop.sock_sendto(
                            self.sockresp, resp.encode(), addr
                        )  # type: ignore
                        # self.logger.debug("Response sent: "+resp)
                    elif "upnp:rootdevice" in data:
                        self.logger.debug("received upnp:rootdevice")
                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config["IP"],
                            self.config["HTTP_PORT"],
                            "upnp:rootdevice",
                            self.config["SERIALNO"],
                        )
                        await self.event_loop.sock_sendto(
                            self.sockresp, resp.encode(), addr
                        )  # type: ignore
                        # self.logger.debug("Response sent: "+resp)
                    elif "ssdp:all" in data:
                        self.logger.debug(
                            "received ssdp:all responding with upnp:rootdevice"
                        )
                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config["IP"],
                            self.config["HTTP_PORT"],
                            "upnp:rootdevice",
                            self.config["SERIALNO"],
                        )
                        await self.event_loop.sock_sendto(
                            self.sockresp, resp.encode(), addr
                        )  # type: ignore
                        # self.logger.debug("Response sent: "+resp)
                    else:
                        self.logger.debug("ignoring")
                    self.logger.debug("----------------------")
                    self.logger.debug("  ")

    async def stop(self):
        self.logger.debug("Stopping response loop")
        await asyncio.to_thread(self.sock.close)
        await asyncio.to_thread(self.sockresp.close)


class Httpd:
    def __init__(self, devices, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.devices = devices

        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        try:
            self.logger.info(
                "Starting HTTP server on {IP}:{HTTP_PORT}".format(**self.config)
            )

            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.sock.bind((self.config["IP"], self.config["HTTP_PORT"]))
            self.sock.listen(100)
            self.sock.setblocking(False)

            await self.loop()

        except socket.error as msg:
            self.logger.error("Http Socket Error: {}".format(msg))

    async def loop(self):
        while True:
            try:
                client, addr = await self.event_loop.sock_accept(self.sock)
            except ConnectionResetError:
                break
            # if socket closed by stop() method [WinError 995] Der E/A-Vorgang wurde wegen eines Threadendes oder einer Anwendungsanforderung abgebrochen
            except socket.error as e:
                if e.winerror == 995:
                    break
                else:
                    self.logger.error(e)
                    continue
            else:
                self.logger.debug("Received connection from {}".format(addr))
                self.event_loop.create_task(self.handle(client, addr))

    async def handle(self, client: socket.socket, addr):
        try:
            # all data isnt always sent right away--try a couple more times
            # 2015-08: Logitech change the data flow slightly.  We seem to need to
            # return the payload for "GET /api/lights" sooner.  The 1 sec sleeps
            # have been removed and we only do another "request.recv" if the
            # content-length is found and greater than 0
            # 2015-10: Header size is now computed to determine exactly how much
            # data still needs to be pulled
            data = ""
            for i in range(3):
                data += (await self.event_loop.sock_recv(client, 1024)).decode()
                if "\r\n\r\n" in data:
                    break
        except socket.error:
            self.logger.error("Socket error")
        else:
            if not data:
                self.logger.debug("Connection closed by {}".format(addr))
            else:
                self.logger.debug("Received data from {}: {}".format(addr, data))
                await self.handle_request(client, data)
                client.close()

    async def handle_request(self, client, data):
        if "test" in data:
            await self.event_loop.sock_sendall(client, "ok".encode())
            return

        searchObj = re.search(r"content-length: (\d+)", data, re.I)
        if searchObj and int(searchObj.group(1)) > 0:
            contentLength = int(searchObj.group(1))
            headerLength = data.find("\r\n\r\n") + 4
            self.logger.debug(
                "Header-Length={} Content-Length={}".format(headerLength, contentLength)
            )
            # got the header--now grab the remaining content if any
            # if len(data) < headerLength + contentLength:
            # data += client.recv(headerLength + contentLength - len(data)).decode() #Cause of problem maybe?
        else:
            contentLength = 0

        self.logger.debug("Content-Length={}".format(contentLength))

        if "description.xml" in data:
            # send description.xml and end for get request
            await self.event_loop.sock_sendall(client, DESCRIPTION_XML.encode())
            self.logger.debug("{} Sent HTTP description.xml Response".format(client))
            # alexa discovery request
            self.logger.debug("Alexa, discover devices")

        elif "hue_logo_0.png" in data:
            await self.event_loop.sock_sendall(client, ICON_HEADERS.encode())
            await self.event_loop.sock_sendall(
                client, ICON_SMALL.decode("base64").encode()
            )
        elif "hue_logo_3.png" in data:
            await self.event_loop.sock_sendall(client, ICON_HEADERS.encode())
            await self.event_loop.sock_sendall(
                client, ICON_BIG.decode("base64").encode()
            )

        elif re.match(r"GET /api/.*lights ", data, re.I):
            resp = "\n{"
            i = 1
            for device in self.devices.values():
                # TODO: Force update of device? dst = device.st()
                resp += f'"{device.id}":'
                resp += await self.get_onelight_json(device)
                if i < len(self.devices):
                    resp += ","
                i += 1
            resp += "}\n"
            await self.send_json(client, resp)

        elif "PUT /api" in data:
            matchObj = re.match(r"PUT /api/(.*)lights/(.+)/state", data, re.I)
            # if "/lights/" in data and "/state" in data:
            if matchObj:
                self.logger.debug("{} Got PUT request to do something".format(client))
                # reqId is what Alexa passes, the match will include the trailing / for now.
                reqId = matchObj.group(1)
                device_id = matchObj.group(2)
                # Just the content
                # Examples:
                #   Harmony: {"on":true,"bri":254}
                #   Echo: {"on": true}
                self.logger.debug(
                    "%s Content data=---\n%s\n---" % (client, data[-contentLength:])
                )

                parsedContent = json.loads(data[-contentLength:])

                self.logger.debug(
                    "%s Parsed Content data=---\n%s\n---" % (client, str(parsedContent))
                )
                #
                # Update the specified device
                #

                self.logger.debug(f"device number: {device_id}")
                device = self.devices.get(device_id)
                if not device:
                    await self.send_json(client, "{}")
                    return

                erg = await device.set(parsedContent)

                await self.send_json(client, json.dumps(erg))

            # All other PUT /api/ send back a blank response
            else:
                await self.send_json(client, "")

        # Requesting the state of just one light
        elif re.match(r"GET /api/.*lights/(.+) ", data, re.I):
            self.logger.debug("{} Got request for one light".format(client))
            matchObj = re.match(r"GET /api/.*lights/(.+) ", data, re.I)
            if not matchObj:
                await self.send_json(client, "{}")
                return
            device_id = matchObj.group(1)
            if device_id not in self.devices:
                await self.send_json(client, "{}")
                return
            device = self.devices[device_id]
            # TODO: Force update of device? dst = device.st()
            OneResp = await self.get_onelight_state_json(device)
            await self.send_json(client, OneResp)

        # Assuming this is a new device registration or config request
        elif "GET /api" in data:
            if "/config" in data:
                self.logger.debug("{} Got request for /config".format(client))
                await self.send_json(client, APICONFIG_JSON)
                self.logger.debug("{} Sent API Config".format(client))
            else:
                newDev = "newdeveloper"
                matchObj = re.match(r"GET /api/(.+) ", data, re.I)
                if matchObj:
                    newDev = matchObj.group(1)
                self.logger.debug(
                    "{} Got request for new dev: {}".format(client, newDev)
                )
                json_resp = """{"lights":{"""
                i = 1
                for device in self.devices.values():
                    json_resp += f""""{device.id}":"""
                    json_resp += await self.get_onelight_json(device)
                    if i < len(self.devices):
                        json_resp += ","
                    i += 1

                json_resp += (
                    """},"schedules":{"1":{"time":"2012-10-29T12:00:00","description":"","name":"schedule","command":{"body":{"on":true,"xy":null,"bri":null,"transitiontime":null},"address":"/api/newdeveloper/groups/0/action","method":"PUT"}}},"config":{"portalservices":false,"gateway":"%s","mac":"%s","swversion":"01005215","linkbutton":false,"ipaddress":"%s:%s","proxyport":0,"swupdate":{"text":"","notify":false,"updatestate":0,"url":""},"netmask":"255.255.255.0","name":"Philips hue","dhcp":true,"proxyaddress":"","whitelist":{"newdeveloper":{"name":"test user","last use date":"2015-02-04T21:35:18","create date":"2012-10-29T12:00:00"}},"UTC":"2012-10-29T12:05:00"},"groups":{"1":{"name":"Group 1","action":{"on":true,"bri":254,"hue":33536,"sat":144,"xy":[0.346,0.3568],"ct":201,"alert":null,"effect":"none","colormode":"xy","reachable":null},"lights":["1","2"]}},"scenes":{}}\n"""
                    % (
                        self.config["GATEWAYIP"],
                        self.config["MACADDRESS"],
                        self.config["IP"],
                        self.config["HTTP_PORT"],
                    )
                )
                await self.send_json(client, json_resp)
                self.logger.debug("{} Sent HTTP New Dev Response".format(client))

        # I only saw a POST when registering the username
        elif "POST /api" in data:
            await self.send_json(client, NEWDEVELOPERSYNC_JSON)
            self.logger.debug("{} Sent HTTP New Dev Sync Response".format(client))
        else:
            await self.event_loop.sock_sendall(
                client, "HTTP/1.1 404 Not Found".encode()
            )

        self.logger.debug("-------------------------------")
        self.logger.debug("    ")

    async def get_onelight_json(self, device):
        # example template values: "on", "[0.0,0.0]", "Hue Lamp 1", "254", "201"
        # on, bri, xy, ct, name
        data = await self.get_json_att(device, ALL)
        data["uniqueid"] = self.gen_unique_id()
        return json.dumps(data)

    def gen_unique_id(self):
        # gen "00:11:22:33:44:55:66:77-88" like id
        serial = uuid.uuid4().hex[:18]
        return (
            ":".join([serial[i : i + 2] for i in range(0, len(serial) - 2, 2)])
            + "-"
            + str(serial[-2:])
        )

    async def get_onelight_state_json(self, device):
        # example template values: "on", "[0.0,0.0]", "Hue Lamp 1", "254", "201"
        # on, bri, xy, ct, name
        return json.dumps(await self.get_json_att(device, GETSTATE))

    async def get_json_att(self, device, template):
        json_resp = copy.deepcopy(template)

        json_resp["state"]["on"] = device.on
        json_resp["state"]["hue"] = device.hue
        json_resp["state"]["sat"] = device.sat
        json_resp["state"]["bri"] = device.bri
        json_resp["state"]["ct"] = device.ct
        json_resp["state"]["xy"] = device.xy
        json_resp["state"]["colormode"] = device.colormode
        json_resp["name"] = device.name
        json_resp["swupdate"]["lastinstall"] = (
            datetime.datetime.now().isoformat().split(".")[0]
        )

        return json_resp

    async def send_json(self, client, resp):
        date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
        full_resp = (JSON_HEADERS % (len(resp), date_str, resp)).replace("\n", "\r\n")
        await self.event_loop.sock_sendall(client, full_resp.encode())

    async def stop(self):
        self.logger.debug("Stopping HTTP Server")
        await asyncio.to_thread(self.sock.close)


#
# This is the main object which all other handlers inherit from:


class hue_upnp_super_handler(object):
    def __init__(self, name, id, logger, on=False, bri=1):
        self.name = name
        self.id = id
        self.logger = logger

        self.on = on
        self.bri = bri

        self.get_default()

    # Set default initial values
    # Can be overridden, or used as a super, or just use the defaults.
    def get_default(self):
        self.xy = [0.0, 0.0]
        self.ct = 199
        self.hue = 0
        self.sat = 254
        self.colormode = "ct"

    # Super set method, parses incomming data and runs the appropriate method.
    async def set(self, data):
        results = []

        for elm in data:
            match elm:
                case "on":
                    self.logger.debug("on received: {}".format(data["on"]))
                    if data["on"]:
                        ret = await self.set_on()
                    else:
                        ret = await self.set_off()
                case "bri":
                    self.logger.debug("bri received: {}".format(data["bri"]))
                    ret = await self.set_bri(data["bri"])

                case "ct":
                    self.logger.debug("ct received: {}".format(data["ct"]))
                    if ret := await self.set_ct(data["ct"]):
                        self.colormode = "ct"

                case "xy":
                    self.logger.debug("xy received: {}".format(data["xy"]))

                    if ret := await self.set_xy(data["xy"]):
                        self.colormode = "hs"

                case "hue":
                    self.logger.debug("hue received: {}".format(data["hue"]))

                    if ret := await self.set_hue(data["hue"]):
                        self.colormode = "hs"

                case "sat":
                    self.logger.debug("sat received: {}".format(data["sat"]))

                    if ret := await self.set_sat(data["sat"]):
                        self.colormode = "hs"

                case _:  # default
                    self.logger.error("ERROR: Unknown command: {}".format(elm))
                    ret = False

            if ret:
                results.append(
                    {"success": {f"/lights/{self.id}/state/{elm}": data[elm]}}
                )
            else:
                results.append(
                    {
                        "error": {
                            "type": 901,
                            "address": f"/lights/{self.id}/state/{elm}",
                            "description": "Internal error",
                        }
                    }
                )

        return results

    # Default, should always be overridden
    async def set_on(self):
        self.logger.error(
            "ERROR: Device " + self.name + " does not have an on command?"
        )

    # Default, should always be overridden
    async def set_off(self):
        self.logger.error(
            "ERROR: Device " + self.name + " does not have an off command?"
        )

    # Default, should always be overridden
    async def set_bri(self, value):
        self.logger.error(
            "ERROR: Device " + self.name + " does not have a bri command?"
        )

    # Default, should always be overridden
    async def set_ct(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a ct command?")

    # Default, should always be overridden
    async def set_xy(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a xy command?")

    # Default, should always be overridden
    async def set_hue(self, value):
        self.logger.error(
            "ERROR: Device " + self.name + " does not have a hue command?"
        )

    # Default, should always be overridden
    async def set_sat(self, value):
        self.logger.error(
            "ERROR: Device " + self.name + " does not have a sat command?"
        )


class Device(hue_upnp_super_handler):
    def __init__(self, name: str, on=False, bri=1, id=None) -> None:
        self.id = id
        self.name = name
        super().__init__(
            self.name, id, None, on, bri if bri <= 254 and bri >= 1 else 1
        )

    def init(self, logger):
        self.logger = logger
        if not self.id:
            self.id = str(int.from_bytes(hashlib.md5(self.name.encode()).digest()))[:10]

    async def set_on(self):
        self.logger.debug(f"Device: {self.name} set ON!")

        if await self.on_on() != False:
            self.on = True
            return True
        return False

    async def set_off(self):
        self.logger.debug(f"Device: {self.name} set OFF!")

        if await self.on_off() != False:
            self.on = False
            return True
        return False

    async def set_bri(self, value):
        self.logger.debug(f"Device: {self.name} set BRI {self.bri}!")

        if await self.on_bri(value) != False:
            self.bri = value
            return True
        return False

    async def set_ct(self, value):
        self.logger.debug(f"Device: {self.name} set CT {self.ct}!")

        if await self.on_ct(value) != False:
            self.ct = value
            return True
        return False

    async def set_xy(self, value):
        self.logger.debug(f"Device: {self.name} set XY {self.xy}!")

        if await self.on_xy(value) != False:
            self.xy = value
            return True
        return False

    async def set_hue(self, value):
        self.logger.debug(f"Device: {self.name} set HUE {self.hue}!")

        if await self.on_hue(value) != False:
            self.hue = value
            return True
        return False

    async def set_sat(self, value):
        self.logger.debug(f"Device: {self.name} set SAT {self.sat}!")

        if await self.on_sat(value) != False:
            self.sat = value
            return True
        return False

    async def on_on(self):
        return True

    async def on_off(self):
        return True

    async def on_bri(self, value):
        return True

    async def on_ct(self, value):
        return True

    async def on_xy(self, value):
        return True

    async def on_hue(self, value):
        return True

    async def on_sat(self, value):
        return True


class Hub(AbstractAsyncContextManager):
    def __init__(self, debug=False) -> None:
        self.devices = {}
        self.config = {}

        self.setup_debug(debug)
        self.gen_config()

    def gen_config(self):
        self.config["GATEWAYIP"] = "1.1.1.1"
        self.config["HTTP_PORT"] = 80  # only port 80 is supported
        self.config["BCAST_IP"] = "239.255.255.250"
        self.config["UPNP_PORT"] = 1900  # type: ignore
        self.config["BROADCAST_INTERVAL"] = 200  # type: ignore

        self.gen_uuids()

    def gen_uuids(self):
        serial = uuid.uuid4().hex[:12].upper()

        self.config["MACADDRESS"] = ":".join(
            [serial[i : i + 2] for i in range(0, len(serial), 2)]
        )
        self.config["SERIALNO"] = serial

    def get_ip(self):
        try:
            self.logger.debug("Retrieving local IP address...")
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.settimeout(0)
            try:
                s.connect(("10.254.254.254", 1))
                ip = s.getsockname()[0]
            except:
                ip = "127.0.0.1"
            finally:
                s.close()

            self.logger.debug("Local IP address is: {}".format(ip))

            if ip != "127.0.0.1":
                return ip
            else:
                self.logger.exception("No valid IP address found.")
        except socket.error as e:
            self.logger.exception("Failed to retrieve local IP address: {}".format(e))

    def setup_debug(self, debug):
        self.logger = logging.getLogger("AlexaHue")
        if debug is True:
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.INFO)
        consoleHandlerDebug = logging.StreamHandler(sys.stdout)
        consoleHandlerInfo = logging.StreamHandler(sys.stdout)
        consoleHandlerError = logging.StreamHandler(sys.stdout)
        # change color of debug messages
        consoleHandler = logging.StreamHandler(sys.stdout)
        consoleHandlerDebug.addFilter(lambda record: record.levelno == logging.DEBUG)
        consoleHandlerDebug.setFormatter(
            logging.Formatter(
                "\033[1;30m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"
            )
        )
        # change color of info messages
        consoleHandlerInfo.addFilter(lambda record: record.levelno == logging.INFO)
        consoleHandlerInfo.setFormatter(
            logging.Formatter(
                "\033[1;32m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"
            )
        )
        # change color of error messages
        consoleHandlerError.addFilter(lambda record: record.levelno == logging.ERROR)
        consoleHandlerError.setFormatter(
            logging.Formatter(
                "\033[1;31m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"
            )
        )

        consoleHandler.addFilter(
            lambda record: record.levelno != logging.DEBUG
            and record.levelno != logging.INFO
            and record.levelno != logging.ERROR
        )

        self.logger.addHandler(consoleHandlerDebug)
        self.logger.addHandler(consoleHandlerInfo)
        self.logger.addHandler(consoleHandlerError)
        self.logger.addHandler(consoleHandler)

    def add(self, *devices: list[Device]):
        for device in devices:
            self.logger.debug("Adding device: " + device.name)
            device.init(self.logger)
            self.devices[device.id] = device

    async def run(self):
        global UPNP_BROADCAST, DESCRIPTION_XML, APICONFIG_JSON
        # same as the MACADDRESS with colons removed
        # Put our info in the responses
        if self.config.get("IP") is None:
            self.config["IP"] = self.get_ip()

        UPNP_BROADCAST = UPNP_BROADCAST.format(
            self.config["IP"], self.config["HTTP_PORT"], self.config["SERIALNO"]
        )
        DESCRIPTION_XML = DESCRIPTION_XML.format(
            self.config["IP"],
            self.config["HTTP_PORT"],
            self.config["IP"],
            self.config["SERIALNO"],
            self.config["SERIALNO"],
        )
        APICONFIG_JSON = APICONFIG_JSON % (self.config["MACADDRESS"])

        self.responder = Responder(self.config, self.logger)
        self.broadcaster = Broadcaster(self.config, self.logger)
        self.httpd = Httpd(self.devices, self.config, self.logger)

        async with asyncio.TaskGroup() as tg:
            tg.create_task(self.responder.run())
            tg.create_task(self.broadcaster.run())
            tg.create_task(self.httpd.run())

    async def stop(self):
        self.logger.debug("Stopping hub...")
        try:
            async with asyncio.TaskGroup() as tg:
                tg.create_task(self.responder.stop())
                tg.create_task(self.broadcaster.stop())
                tg.create_task(self.httpd.stop())
        except Exception as e:
            self.logger.exception("Failed to stop hub: {}".format(e))
        self.logger.debug("Hub stopped.")

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.stop()


async def test():
    async with Hub(True) as hub:
        hub.add(Device("Test Device", True, 254))
        await hub.run()


if "__main__" == __name__:
    asyncio.run(test())
