"""Discover Topbox Pis on the local LAN for OHD remote setup."""
from __future__ import annotations

import concurrent.futures
import ipaddress
import json
import socket
import urllib.error
import urllib.request
from typing import Any, Dict, List, Optional, Sequence, Tuple

DEFAULT_WEB_PORT = 31234
INFO_PATH = "/Topbox/info.json"
SCAN_CONNECT_TIMEOUT_S = 0.35
MAX_SCAN_WORKERS = 64


def _hostname_starts_with_topbox(hostname: str) -> bool:
    host = (hostname or "").strip().lower()
    if host.endswith(".local"):
        host = host[: -len(".local")]
    return host.startswith("topbox")


def is_topbox_master(device: Dict[str, Any]) -> bool:
    role = str(device.get("machine_role") or "").strip().upper()
    if role == "M":
        return True
    host = str(device.get("hostname") or "").strip().lower()
    if host.endswith(".local"):
        host = host[: -len(".local")]
    return host.startswith("topbox") and host.endswith("m")


def sort_topbox_devices(devices: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
    return sorted(
        devices,
        key=lambda d: (0 if is_topbox_master(d) else 1, str(d.get("hostname") or "").lower()),
    )


def filter_topbox_devices(devices: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
    filtered = [d for d in devices if _hostname_starts_with_topbox(str(d.get("hostname") or ""))]
    return sort_topbox_devices(filtered)


def local_ipv4_addresses() -> List[str]:
    addrs: List[str] = []
    try:
        probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        probe.connect(("8.8.8.8", 80))
        addrs.append(probe.getsockname()[0])
        probe.close()
    except OSError:
        pass

    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET, socket.SOCK_DGRAM):
            ip = info[4][0]
            if not ip.startswith("127."):
                addrs.append(ip)
    except OSError:
        pass

    unique: List[str] = []
    for ip in addrs:
        if ip not in unique:
            unique.append(ip)
    return unique


def iter_subnet_hosts(ip_str: str, *, prefix: int = 24) -> List[str]:
    network = ipaddress.ip_network(f"{ip_str}/{prefix}", strict=False)
    return [str(host) for host in network.hosts()]


def _resolve_hostname(ip: str) -> str:
    try:
        host, _, _ = socket.gethostbyaddr(ip)
        return host.split(".")[0]
    except OSError:
        return ip


def _fetch_topbox_info(ip: str, port: int = DEFAULT_WEB_PORT) -> Optional[Dict[str, Any]]:
    url = f"http://{ip}:{port}{INFO_PATH}"
    try:
        req = urllib.request.Request(url, headers={"Accept": "application/json"})
        with urllib.request.urlopen(req, timeout=SCAN_CONNECT_TIMEOUT_S) as resp:
            payload = json.loads(resp.read().decode("utf-8", errors="replace"))
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
        return None

    if not isinstance(payload, dict):
        return None

    hostname = str(payload.get("hostname") or _resolve_hostname(ip)).strip()
    payload["ip"] = ip
    payload["port"] = port
    payload["hostname"] = hostname
    payload["progressives_url"] = f"http://{ip}:{port}/EtherGold/progressives.csv"
    return payload


def scan_local_topbox_devices(
    *,
    port: int = DEFAULT_WEB_PORT,
    prefix: int = 24,
    max_workers: int = MAX_SCAN_WORKERS,
) -> Tuple[List[Dict[str, Any]], List[str]]:
    """Scan local /24 subnets for DyzineFrame Topbox info endpoints."""
    scanned_subnets: List[str] = []
    candidate_ips: List[str] = []

    for local_ip in local_ipv4_addresses():
        subnet = str(ipaddress.ip_network(f"{local_ip}/{prefix}", strict=False))
        scanned_subnets.append(subnet)
        for host in iter_subnet_hosts(local_ip, prefix=prefix):
            if host not in candidate_ips:
                candidate_ips.append(host)

    found: List[Dict[str, Any]] = []
    workers = min(max_workers, max(1, len(candidate_ips)))
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
        futures = {pool.submit(_fetch_topbox_info, ip, port): ip for ip in candidate_ips}
        for future in concurrent.futures.as_completed(futures):
            info = future.result()
            if info is not None:
                found.append(info)

    return filter_topbox_devices(found), scanned_subnets
