"""IP scanner module for OnWords - discovers devices on local network."""

from __future__ import annotations

import asyncio
import socket
import subprocess
import re
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor
from typing import Optional

# Brand colors for terminal output
RED = "\033[31m"
WHITE = "\033[37m"
CYAN = "\033[36m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
DIM = "\033[2m"
RESET = "\033[0m"


@dataclass
class NetworkDevice:
    """Network device information."""

    ip: str
    mac: Optional[str] = None
    hostname: Optional[str] = None
    vendor: Optional[str] = None
    latency_ms: Optional[float] = None

    def __str__(self) -> str:
        parts = [self.ip]
        if self.hostname:
            parts.append(f"({self.hostname})")
        if self.mac:
            parts.append(f"[{self.mac}]")
        return " ".join(parts)


def get_local_ip() -> str:
    """Get the local IP address of this machine."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "192.168.1.1"


def get_network_prefix(ip: str) -> str:
    """Get the network prefix (first 3 octets) from an IP."""
    parts = ip.split(".")
    return ".".join(parts[:3])


def ping_host(ip: str, timeout: float = 1.0) -> Optional[float]:
    """
    Ping a host and return latency in ms, or None if unreachable.
    Works on Windows, macOS, and Linux.
    """
    import platform
    
    try:
        system = platform.system().lower()
        
        if system == "windows":
            # Windows: -n count, -w timeout in milliseconds
            cmd = ["ping", "-n", "1", "-w", str(int(timeout * 1000)), ip]
        else:
            # macOS/Linux: -c count, -W timeout in seconds (macOS) or seconds (Linux)
            if system == "darwin":
                cmd = ["ping", "-c", "1", "-W", str(int(timeout * 1000)), ip]
            else:
                cmd = ["ping", "-c", "1", "-W", str(int(timeout)), ip]
        
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout + 2,
        )
        
        if result.returncode == 0:
            # Extract latency - works for all platforms
            match = re.search(r"time[=<](\d+\.?\d*)", result.stdout)
            if match:
                return float(match.group(1))
            return 0.0
        return None
    except (subprocess.TimeoutExpired, Exception):
        return None


def get_arp_table() -> dict[str, str]:
    """
    Get the ARP table mapping IP to MAC addresses.
    Works on Windows, macOS, and Linux.
    """
    import platform
    
    arp_map: dict[str, str] = {}
    try:
        result = subprocess.run(
            ["arp", "-a"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        
        system = platform.system().lower()
        
        for line in result.stdout.split("\n"):
            if system == "windows":
                # Windows format: 192.168.1.1    aa-bb-cc-dd-ee-ff    dynamic
                match = re.search(r"(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]+)", line)
                if match:
                    ip, mac = match.groups()
                    mac = mac.replace("-", ":").upper()
                    arp_map[ip] = mac
            else:
                # macOS/Linux format: hostname (ip) at mac on interface
                match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)", line)
                if match:
                    ip, mac = match.groups()
                    if mac != "(incomplete)":
                        arp_map[ip] = mac.upper()
    except Exception:
        pass
    return arp_map


def resolve_hostname(ip: str) -> Optional[str]:
    """Try to resolve hostname for an IP address."""
    try:
        hostname, _, _ = socket.gethostbyaddr(ip)
        return hostname
    except (socket.herror, socket.gaierror):
        return None


def scan_ip(ip: str, arp_table: dict[str, str]) -> Optional[NetworkDevice]:
    """Scan a single IP address."""
    latency = ping_host(ip, timeout=0.5)
    if latency is not None:
        mac = arp_table.get(ip)
        hostname = resolve_hostname(ip)
        return NetworkDevice(
            ip=ip,
            mac=mac,
            hostname=hostname,
            latency_ms=latency,
        )
    return None


def scan_network(
    network_prefix: Optional[str] = None,
    start: int = 1,
    end: int = 254,
    max_workers: int = 50,
    progress_callback=None,
) -> list[NetworkDevice]:
    """
    Scan a network range for active devices.

    Args:
        network_prefix: First 3 octets (e.g., "192.168.1"). Auto-detected if None.
        start: Starting host number (default 1).
        end: Ending host number (default 254).
        max_workers: Number of parallel threads (default 50).
        progress_callback: Optional callback(current, total) for progress updates.

    Returns:
        List of NetworkDevice objects for reachable hosts.
    """
    if network_prefix is None:
        local_ip = get_local_ip()
        network_prefix = get_network_prefix(local_ip)

    # Pre-fetch ARP table
    arp_table = get_arp_table()

    # Generate IP list
    ips = [f"{network_prefix}.{i}" for i in range(start, end + 1)]
    total = len(ips)
    devices: list[NetworkDevice] = []
    scanned = 0

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(scan_ip, ip, arp_table): ip for ip in ips}

        for future in futures:
            result = future.result()
            scanned += 1
            if progress_callback:
                progress_callback(scanned, total)
            if result:
                devices.append(result)

    # Sort by IP address
    devices.sort(key=lambda d: [int(x) for x in d.ip.split(".")])
    return devices


def scan_network_async(
    network_prefix: Optional[str] = None,
    start: int = 1,
    end: int = 254,
) -> list[NetworkDevice]:
    """Synchronous wrapper for network scanning."""
    return scan_network(network_prefix, start, end)


def print_brand() -> None:
    """Print the OnWords brand header."""
    print(f"{RED}on{WHITE}words{RESET} · IP Scanner\n")


def print_devices(devices: list[NetworkDevice], local_ip: str) -> None:
    """Pretty-print discovered devices."""
    if not devices:
        print(f"{DIM}No devices found.{RESET}")
        return

    print(f"Found {CYAN}{len(devices)}{RESET} device(s) on the network:\n")

    for i, device in enumerate(devices, 1):
        is_local = device.ip == local_ip
        marker = f" {GREEN}← this device{RESET}" if is_local else ""

        print(f"  {DIM}{i:>2}.{RESET} {WHITE}{device.ip}{RESET}{marker}")

        if device.hostname:
            print(f"      {DIM}Hostname:{RESET} {device.hostname}")
        if device.mac:
            print(f"      {DIM}MAC:{RESET}      {device.mac}")
        if device.latency_ms is not None:
            color = GREEN if device.latency_ms < 10 else (YELLOW if device.latency_ms < 50 else RED)
            print(f"      {DIM}Latency:{RESET}  {color}{device.latency_ms:.1f} ms{RESET}")
        print()


def main() -> None:
    """CLI entry point for IP scanning."""
    print_brand()

    local_ip = get_local_ip()
    network_prefix = get_network_prefix(local_ip)

    print(f"{DIM}Local IP:{RESET} {local_ip}")
    print(f"{DIM}Scanning:{RESET} {network_prefix}.1-254\n")

    scanned = [0]

    def progress(current: int, total: int) -> None:
        scanned[0] = current
        pct = int(current / total * 100)
        bar_len = 30
        filled = int(bar_len * current / total)
        bar = "█" * filled + "░" * (bar_len - filled)
        print(f"\r{DIM}Progress:{RESET} [{CYAN}{bar}{RESET}] {pct}%", end="", flush=True)

    try:
        devices = scan_network(network_prefix, progress_callback=progress)
        print("\r" + " " * 60 + "\r")  # Clear progress line
        print_devices(devices, local_ip)
    except Exception as e:
        print(f"\n{RED}Error:{RESET} {e}")


if __name__ == "__main__":
    main()

