import os
import json
import socket
import subprocess
import re
import ipaddress
import csv
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

from scapy.all import ARP, Ether, srp
from tqdm import tqdm

# ============================================================
# CONFIG
# ============================================================

SUBNET = "192.168.188.0/24"
SUBNET_PREFIX = "192.168.188"
NETWORK = ipaddress.ip_network(SUBNET, strict=False)

STATE_FILE = Path("state.json")
def resource_path(relative):
    try:
        base = sys._MEIPASS
    except Exception:
        base = Path(__file__).parent
    return Path(base) / relative

import sys
OUI_FILE = resource_path("oui.txt")

CSV_DIR = Path("csv")
CSV_DIR.mkdir(exist_ok=True)

PING_WORKERS = 50
PING_TIMEOUT_MS = 200
BAR_WIDTH = 70

COL_IP       = 15
COL_MAC      = 17
COL_VENDOR   = 32
COL_HOSTNAME = 28
COL_STATUS   = 12

# ============================================================
# HULPFUNCTIES
# ============================================================

def fmt(text, width):
    text = text or "—"
    if len(text) > width:
        return text[:width - 1] + "…"
    return text.ljust(width)

def ip_sort(ip):
    return list(map(int, ip.split(".")))

def is_special_ip(ip):
    addr = ipaddress.ip_address(ip)
    return (
        addr == NETWORK.broadcast_address
        or ip == "255.255.255.255"
        or addr.is_multicast
        or addr.is_unspecified
        or addr.is_loopback
        or addr.is_reserved
        or addr.is_link_local
    )

# ============================================================
# TOETSENAFHANDELING
# ============================================================

def wait_for_key():
    """
    O / o → opnieuw scannen
    S / s → CSV opslaan
    ESC   → afsluiten
    """
    if os.name == "nt":
        import msvcrt
        while True:
            key = msvcrt.getch()
            if key in (b'o', b'O'):
                return "rescan"
            if key in (b's', b'S'):
                return "csv"
            if key == b'\x1b':
                return "exit"
    else:
        import sys
        import termios
        import tty

        fd = sys.stdin.fileno()
        old = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            while True:
                ch = sys.stdin.read(1)
                if ch in ("o", "O"):
                    return "rescan"
                if ch in ("s", "S"):
                    return "csv"
                if ord(ch) == 27:
                    return "exit"
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old)

# ============================================================
# VENDOR (OUI)
# ============================================================

def load_oui_db():
    vendors = {}
    if not OUI_FILE.exists():
        return vendors

    with open(OUI_FILE, "r", encoding="utf-8", errors="ignore") as f:
        lines = f.readlines()

    for line in tqdm(
        lines,
        desc="🏷️ Vendor-database",
        unit="regel",
        ncols=BAR_WIDTH,
        leave=False
    ):
        if "(hex)" in line:
            try:
                prefix, name = line.split("(hex)")
                mac = prefix.strip().replace("-", ":").upper()
                vendors[mac] = name.strip()
            except ValueError:
                pass

    return vendors

def lookup_vendor(mac, vendors):
    return vendors.get(mac.upper()[0:8])

# ============================================================
# NETWERK
# ============================================================

def ping_host(ip):
    cmd = (
        ["ping", "-n", "1", "-w", str(PING_TIMEOUT_MS), ip]
        if os.name == "nt"
        else ["ping", "-c", "1", "-W", "1", ip]
    )
    subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def ping_sweep(prefix):
    ips = [f"{prefix}.{i}" for i in range(1, 255)]
    with ThreadPoolExecutor(max_workers=PING_WORKERS) as executor:
        futures = [executor.submit(ping_host, ip) for ip in ips]
        for _ in tqdm(
            as_completed(futures),
            total=len(futures),
            desc="📡 Ping sweep",
            unit="host",
            ncols=BAR_WIDTH,
            leave=False
        ):
            pass

def resolve_hostname(ip):
    try:
        return socket.gethostbyaddr(ip)[0]
    except Exception:
        return None

def arp_scan(subnet):
    packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=subnet)
    with tqdm(
        total=1,
        desc="📡 ARP-scan",
        unit="scan",
        ncols=BAR_WIDTH,
        leave=False
    ) as pbar:
        result = srp(packet, timeout=2, verbose=False)[0]
        pbar.update(1)
    return {r.psrc: r.hwsrc for _, r in result}

def arp_table():
    devices = {}
    output = subprocess.check_output(
        "arp -a",
        shell=True,
        encoding="utf-8",
        errors="ignore"
    )

    for line in tqdm(
        output.splitlines(),
        desc="📋 ARP-table",
        unit="regel",
        ncols=BAR_WIDTH,
        leave=False
    ):
        match = re.search(
            r"(\d+\.\d+\.\d+\.\d+)\s+([a-f0-9:-]{17})",
            line,
            re.IGNORECASE
        )
        if match:
            ip, mac = match.groups()
            devices[ip] = mac.lower().replace("-", ":")

    return devices

def merge_devices(scan_devices, table_devices, vendors):
    merged = {}
    items = list({**scan_devices, **table_devices}.items())

    for ip, mac in tqdm(
        items,
        desc="🧠 Samenvoegen",
        unit="host",
        ncols=BAR_WIDTH,
        leave=False
    ):
        if is_special_ip(ip):
            continue

        merged[ip] = {
            "ip": ip,
            "mac": mac,
            "vendor": lookup_vendor(mac, vendors),
            "hostname": resolve_hostname(ip)
        }

    return merged

# ============================================================
# STATE & CSV
# ============================================================

def load_state():
    if STATE_FILE.exists():
        with open(STATE_FILE, "r", encoding="utf-8") as f:
            return json.load(f).get("devices", {})
    return {}

def save_state(devices):
    with tqdm(
        total=1,
        desc="💾 State opslaan",
        unit="bestand",
        ncols=BAR_WIDTH,
        leave=False
    ) as pbar:
        with open(STATE_FILE, "w", encoding="utf-8") as f:
            json.dump(
                {
                    "timestamp": datetime.now().isoformat(timespec="seconds"),
                    "devices": devices
                },
                f,
                indent=2
            )
        pbar.update(1)

def save_csv(devices, previous):
    timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    path = CSV_DIR / f"lan_scan_{timestamp}.csv"

    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["IP", "MAC", "Vendor", "Hostname", "Status"])

        for ip in sorted(devices.keys(), key=ip_sort):
            d = devices[ip]
            writer.writerow([
                ip,
                d["mac"],
                d.get("vendor") or "",
                d.get("hostname") or "",
                device_status(ip, previous, devices)
            ])

    print(f"\n💾 CSV opgeslagen: {path}")

def device_status(ip, old, new):
    if ip not in old:
        return "nieuw"
    if old[ip] != new[ip]:
        return "gewijzigd"
    return "bekend"

# ============================================================
# MAIN LOOP
# ============================================================

while True:
    os.system("cls" if os.name == "nt" else "clear")
    print("🔍 LAN Device Discovery\n")

    vendors = load_oui_db()

    ping_sweep(SUBNET_PREFIX)
    scan_devices = arp_scan(SUBNET)
    table_devices = arp_table()
    current = merge_devices(scan_devices, table_devices, vendors)
    previous = load_state()

    print("\n📋 Apparaten op het netwerk:\n")

    header = (
        f"{fmt('IP-adres', COL_IP)} "
        f"{fmt('MAC-adres', COL_MAC)} "
        f"{fmt('Vendor', COL_VENDOR)} "
        f"{fmt('Hostname', COL_HOSTNAME)} "
        f"{fmt('Status', COL_STATUS)}"
    )

    print(header)
    print("-" * len(header))

    for ip in sorted(current.keys(), key=ip_sort):
        d = current[ip]
        print(
            f"{fmt(ip, COL_IP)} "
            f"{fmt(d['mac'], COL_MAC)} "
            f"{fmt(d.get('vendor'), COL_VENDOR)} "
            f"{fmt(d.get('hostname'), COL_HOSTNAME)} "
            f"{fmt(device_status(ip, previous, current), COL_STATUS)}"
        )

    save_state(current)

    print("\n🔁 [O] opnieuw scannen  💾 [S] CSV opslaan  ⎋ ESC afsluiten")

    action = wait_for_key()
    if action == "csv":
        save_csv(current, previous)
        input("\nDruk op ENTER om verder te gaan...")
    elif action == "exit":
        break
