import asyncio
import socket
import logging

log = logging.getLogger(__name__)

UDP_DST_PORT = 33333
RCV_BUFSIZ = 1024

GRACE_SECONDS = 2

DISCOVERY_MSG = b"\x00\x00\x00\x1a"


class DiscoveredDevice:
    @staticmethod
    def parse_UDP_discovery_info(discovery_info):
        data = {}
        if discovery_info[3] != int.from_bytes(b"\x1b"):
            raise AttributeError("Not device info")
        data["ssid"] = discovery_info[8:20].decode("utf-8").replace("\x00", "").strip()
        data["mac"] = discovery_info[20:26].hex(":")
        data["tcp"] = int.from_bytes(discovery_info[26:27])
        data["model"] = (
            discovery_info[28:42].decode("utf-8").replace("\x00", "").strip()
        )
        data["serial"] = (
            discovery_info[43:51].decode("utf-8").replace("\x00", "").strip()
        )
        data["modbus_address"] = discovery_info[136]
        data["sw_ver"] = discovery_info[52] / 100
        data["hw_ver"] = chr(discovery_info[54])
        try:
            data["description"] = (
                discovery_info[55:94].decode("utf-8").replace("\x00", "").strip()
            )
            data["location"] = (
                discovery_info[95:134].decode("utf-8").replace("\x00", "").strip()
            )
        except:
            pass
        return data

    def __init__(self, ip, port, basic_info_string):
        self.values = {}
        self.values["ip_address"] = ip
        self.values["port"] = port
        self.values.update(self.parse_UDP_discovery_info(basic_info_string))

    def __getattr__(self, name):
        return self.values[name]


class Discovery:
    def __init__(self):
        self.loop = asyncio.get_event_loop()
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(("", 0))
        self.sock.settimeout(GRACE_SECONDS)

        self.cancel_event = asyncio.Event()
        self.dev = {}

    async def broadcast_udp_packet(self, ip_address):
        try:
            log.debug(f"Sending Broadcast to {ip_address}")
            await self.loop.sock_sendto(
                self.sock, DISCOVERY_MSG, (ip_address, UDP_DST_PORT)
            )
        except Exception as e:
            log.error(f"Error broadcasting to {ip_address}: {e}")
            raise e

    async def poll(self, broadcast_addresses, stop_if_found=None):
        try:

            # Broadcast UDP packets to all IPs concurrently
            await asyncio.gather(
                *[
                    self.broadcast_udp_packet(ip_address)
                    for ip_address in broadcast_addresses
                ]
            )

            start_time = self.loop.time()
            while (
                not self.cancel_event.is_set()
                and (self.loop.time() - start_time) < GRACE_SECONDS
            ):
                try:
                    # Use asyncio.wait_for to set a timeout for sock_recvfrom
                    data, addr = await asyncio.wait_for(
                        self.loop.sock_recvfrom(self.sock, RCV_BUFSIZ), timeout=1.0
                    )
                    device = DiscoveredDevice(addr[0], addr[1], data)
                    log.info(
                        f"Found device {device.model} {device.serial} {device.ip_address}"
                    )
                    new_mac = device.mac
                    self.dev[new_mac] = device

                    if (
                        stop_if_found is not None
                        and device.serial.lower() == stop_if_found.lower()
                    ):
                        return list(self.dev.values())

                except asyncio.TimeoutError:
                    # Handle timeout and continue the loop
                    continue

                except ValueError as ve:
                    log.error(f"Error parsing discovery info: {ve}")
                    raise

        except Exception as e:
            log.error(f"Error during discovery: {e}")
            raise

        return list(self.dev.values())

    async def get_devices(self, broadcast_addresses):
        try:
            devices = await self.poll(broadcast_addresses)
            return devices
        except Exception as e:
            log.error(f"Error getting devices: {e}")
            raise

    async def get_serial(self, broadcast_addresses, serial):
        try:
            devices = await self.poll(broadcast_addresses, serial)
            for device in devices:
                if device.serial.lower() == serial.lower():
                    return device
            return None
        except Exception as e:
            log.error(f"Error getting serial: {e}")
            raise
