import asyncio
from typing import Any, Dict, List, cast

from zeroconf import ServiceStateChange, Zeroconf
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf

from gtv.models import DeviceInfo


async def discover_devices(timeout: float = 5.0) -> List[DeviceInfo]:
    devices: List[DeviceInfo] = []
    found_names: set = set()

    async def on_service_state_change(
        zeroconf: Zeroconf,
        service_type: str,
        name: str,
        state_change: ServiceStateChange,
    ) -> None:
        if state_change is not ServiceStateChange.Added:
            return
        if name in found_names:
            return
        found_names.add(name)

        info = AsyncServiceInfo(service_type, name)
        await info.async_request(zeroconf, 3000)
        if not info:
            return

        addresses = info.parsed_scoped_addresses()
        if not addresses:
            return

        host = addresses[0]
        port = cast(int, info.port)
        props: Dict[str, Any] = {}
        if info.properties:
            props = {
                k.decode() if isinstance(k, bytes) else k:
                v.decode() if isinstance(v, bytes) else v
                for k, v in info.properties.items()
            }

        device = DeviceInfo(
            name=props.get("fn", name.split("._")[0]),
            host=host,
            port=port,
            properties=props,
        )
        devices.append(device)

    zc = AsyncZeroconf()
    service_type = "_androidtvremote2._tcp.local."

    browser = AsyncServiceBrowser(
        zc.zeroconf,
        [service_type],
        handlers=[
            lambda *args, **kwargs: asyncio.ensure_future(
                on_service_state_change(*args, **kwargs)
            )
        ],
    )

    await asyncio.sleep(timeout)
    await browser.async_cancel()
    await zc.async_close()

    return devices
