"""ZCC Discovery Service Class"""

from __future__ import annotations

import asyncio
from enum import StrEnum
import json
from json.decoder import JSONDecodeError
import logging
import socket
from typing import Tuple

from zcc.constants import LEVEL_BY_VERBOSITY
from zcc.controller import ControlPoint
from zcc.description import ControlPointDescription
from zcc.errors import ( ControlPointError,
                     ControlPointConnectionRefusedError,
                     ControlPointCannotConnectError,
                     ControlPointInvalidHostError,
                     ControlPointTimeoutError)
from zcc.protocol import ControlPointProtocol

class ControlPointDiscoveryProtocol(asyncio.DatagramProtocol):
    """Listens for ZCC announcements on the defined UDP port."""

    def __init__(
        self, discovery_complete: asyncio.Future | None, discovery_result: object
    ) -> None:
        super().__init__()
        self.discovery_complete: asyncio.Future | None = discovery_complete
        self.discovery_result = discovery_result
        self.transport: asyncio.transports.DatagramTransport = None
        self.logger = logging.getLogger("ControlPointDiscoveryService")

    def connection_lost(self, exc) -> None:
        self.transport.close()
        return super().connection_lost(exc)

    def connection_made(self, transport: asyncio.transports.DatagramTransport) -> None:
        self.transport = transport
        return super().connection_made(transport)

    def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
        data = str(data.decode("UTF-8"))
        self.logger.debug("datagram_received() from %s\n%s", str(addr), data)

        lines = data.split("\n")
        for line in lines:
            try:
                if response := json.loads(line):
                    result = ControlPointDescription()
                    result.brand = response["brand"]
                    result.product = response["product"]
                    result.mac = response["mac"]
                    result.host = addr[0]
                    result.port = response["tcp"]
                    result.available_tcps = response["availableTcps"]
                    if api_version := response.get("apiVersion"):
                        result.api_version = api_version
                    if firmware_version := response.get("firmwareVersion"):
                        result.firmware_version = firmware_version
                    self.discovery_result.append(result)
                    if self.discovery_complete:
                        self.discovery_complete.set_result(True)
            except JSONDecodeError:
                break

        return super().datagram_received(data, addr)


class ControlPointDiscoveryService:
    """Provides a ZCC discovery service to discover ZIMI controllers on the local LAN."""

    def __init__(self, verbosity: int = 0):
        self.logger = logging.getLogger("ControlPointDiscoveryService")
        if verbosity > 2:
            verbosity = 2
        self.logger.setLevel(LEVEL_BY_VERBOSITY[verbosity])

        self.loop = asyncio.get_event_loop()
        self.discovery_complete = self.loop.create_future()
        self.discovery_result: list[ControlPointDescription] = []
        self.never_completes = self.loop.create_future()
        self.validation_result: ControlPointDescription

    async def discover(
        self, wait_for_all: bool = False
    ) -> ControlPointDescription | list[ControlPointDescription]:
        """Discover local ZIMI controllers on LAN and return (host,port)."""

        if wait_for_all:
            self.discovery_complete = None

        transport, _ = await self.loop.create_datagram_endpoint(
            lambda: ControlPointDiscoveryProtocol(
                self.discovery_complete, self.discovery_result
            ),
            local_addr=("0.0.0.0", ControlPointProtocol.UDP_RECV_PORT),
        )

        send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

        server_address = ("255.255.255.255", ControlPointProtocol.UDP_SEND_PORT)
        message = ControlPointProtocol.discover()

        send_socket.sendto(message.encode(), server_address)
        self.logger.info("Sending discovery message on local network")

        if wait_for_all:
            try:
                await asyncio.wait_for(self.never_completes, timeout=10)
            except asyncio.exceptions.TimeoutError as error:
                transport.close()
                if len(self.discovery_result) <= 0:
                    raise ControlPointError(
                        "Failure - Unable to discover ZCC by UDP broadcast."
                    ) from error
                self.logger.info(
                    "Success - discovered ZIMI controllers:",
                )
                for result in self.discovery_result:
                    self.logger.info(result)
                return self.discovery_result
        else:
            try:
                await asyncio.wait_for(self.discovery_complete, timeout=10)
                transport.close()
                self.logger.info("Success - discovered ZIMI controller:")
                self.logger.info(
                    self.discovery_result[0],
                )
                return self.discovery_result[0]
            except asyncio.exceptions.TimeoutError as error:
                transport.close()
                raise ControlPointError(
                    "Failure - Unable to discover ZCC by UDP broadcast."
                ) from error

    async def discovers(self) -> list[ControlPointDescription]:
        """Discover all local zimi controllers."""
        return await self.discover(wait_for_all=True)

    async def validate_connection(
        self, host: str, port: int
    ) -> ControlPointDescription:
        """Validate ability to connect and close a connection.

        Returns ControlPointDescription if OK.
        """

        try:
            socket.gethostbyname(host)
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(10)
            try:
                s.connect((host, port))
                s.close()
            except ConnectionRefusedError as e:
                raise ControlPointConnectionRefusedError() from e
            except TimeoutError as e:
                raise ControlPointTimeoutError() from e
            except socket.gaierror as e:
                raise ControlPointCannotConnectError() from e
        except socket.gaierror as e:
            raise ControlPointInvalidHostError() from e

        api = ControlPoint(ControlPointDescription(host=host, port=port))

        try:
            await api.connect(fast=True)
        except ControlPointError as e:
            raise ControlPointCannotConnectError() from e

        self.validation_result = ControlPointDescription(
            brand=api.brand,
            product=api.product,
            host=host,
            port=port,
            mac=api.mac,
            available_tcps=api.available_tcps,
            api_version=api.api_version,
            firmware_version=api.firmware_version
        )

        api.disconnect()
        
        self.logger.info(self.validation_result)

        return self.validation_result
