import os
import re
import socket
import subprocess
import threading
import json
from concurrent.futures import ThreadPoolExecutor

# Kamus prefiks MAC untuk identifikasi vendor perangkat secara sederhana
MAC_VENDORS = {
    "00:11:22": "Simulated Device",
    "00:0c:29": "VMware",
    "00:15:5d": "Microsoft Hyper-V",
    "08:00:27": "Oracle VirtualBox",
    "fc:fb:fb": "Apple",
    "00:17:f2": "Apple",
    "b8:27:eb": "Raspberry Pi Foundation",
    "dc:a6:32": "Raspberry Pi Foundation",
    "c4:ad:34": "Samsung",
    "00:26:37": "Samsung",
    "70:8b:cd": "ASUSTek Computer",
    "00:14:22": "Dell",
    "cc:96:e5": "Dell",
    "b8:cb:29": "Dell",
    "e0:db:55": "TP-Link",
    "50:c7:bf": "TP-Link",
    "c8:3a:35": "Tenda",
    "04:d6:aa": "Xiaomi",
    "50:4f:3b": "Xiaomi",
    "ac:cf:85": "Huawei",
    "00:0e:3b": "Intel",
    "c4:70:ab": "Ruijie Networks",
    "10:5f:02": "Ruijie Networks",
    "00:74:9c": "Ruijie Networks",
    "70:85:c4": "Ruijie Networks",
    "9c:2b:a6": "Ruijie Networks",
}

def get_local_ip():
    """Mengambil alamat IP lokal komputer ini."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        # Menghubungkan ke IP publik (tidak benar-benar mengirim paket)
        s.connect(("8.8.8.8", 80))
        local_ip = s.getsockname()[0]
        s.close()
        return local_ip
    except Exception:
        # Fallback jika tidak ada koneksi internet sama sekali
        try:
            return socket.gethostbyname(socket.gethostname())
        except Exception:
            return "127.0.0.1"

def get_mac_vendor(mac_address):
    """Mendeteksi vendor berdasarkan OUI MAC address."""
    if not mac_address:
        return "Unknown"
    mac_clean = mac_address.replace("-", ":").lower()
    prefix = mac_clean[:8]
    return MAC_VENDORS.get(prefix, "Unknown Device/Vendor")

def load_ssh_credentials():
    """Membaca berkas servers.json untuk mengumpulkan daftar kredensial SSH yang tersimpan."""
    creds = []
    servers_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "servers.json")
    if os.path.exists(servers_path):
        try:
            with open(servers_path, "r") as f:
                servers = json.load(f)
                for s in servers:
                    if s.get("type") == "ssh" and s.get("username") and s.get("password"):
                        creds.append((s["username"], s["password"]))
        except Exception:
            pass
    return list(set(creds))

def gethostbyaddr_timeout(ip, timeout=0.25):
    """Mengeksekusi gethostbyaddr dengan batasan waktu (timeout) dalam detik menggunakan daemon thread."""
    res = [None]
    def worker():
        try:
            res[0] = socket.gethostbyaddr(ip)
        except Exception:
            pass
    t = threading.Thread(target=worker)
    t.daemon = True
    t.start()
    t.join(timeout)
    return res[0]

def get_hostname(ip, vendor="Unknown", ssh_creds=None):
    """Melakukan pencarian nama komputer menggunakan DNS, NetBIOS, SSH login, gateway check, dan OUI vendor."""
    # 1. Coba DNS resolver standar dengan timeout ketat (250ms) agar Windows tidak hang
    try:
        dns_res = gethostbyaddr_timeout(ip, 0.25)
        if dns_res:
            hostname = dns_res[0]
            if hostname and hostname != ip:
                return hostname
    except Exception:
        pass

    # 2. Coba NetBIOS status query (port 137 UDP) dengan timeout 150ms
    try:
        query = b'\x80\xd0\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01'
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(0.15)
        sock.sendto(query, (ip, 137))
        data, _ = sock.recvfrom(1024)
        if len(data) > 57:
            num_names = data[56]
            if num_names > 0:
                name = data[57:57+15].decode('utf-8', errors='ignore').strip()
                clean_name = "".join(c for c in name if c.isalnum() or c in "-_")
                if clean_name:
                    return clean_name
    except Exception:
        pass

    # 3. Coba SSH lookup jika port 22 terbuka dan terdapat kredensial yang cocok
    if ssh_creds:
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(0.15)
            res = s.connect_ex((ip, 22))
            s.close()
            if res == 0:
                for user, password in ssh_creds:
                    try:
                        import paramiko
                        client = paramiko.SSHClient()
                        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                        client.connect(ip, username=user, password=password, timeout=1.0, banner_timeout=1.0)
                        stdin, stdout, stderr = client.exec_command('hostname')
                        hn = stdout.read().decode('utf-8', errors='ignore').strip()
                        client.close()
                        if hn:
                            return hn
                    except Exception:
                        pass
        except Exception:
            pass

    # 4. Deteksi jika perangkat adalah Router/Gateway (.1)
    ip_parts = ip.split(".")
    if len(ip_parts) == 4 and ip_parts[3] == "1":
        return "Router-Gateway"

    # 5. Gunakan nama vendor OUI sebagai fallback informatif
    last_octet = ip.split(".")[-1]
    if vendor and vendor != "Unknown" and vendor != "Unknown Device/Vendor":
        clean_vendor = vendor.split()[0].replace(",", "").replace("/", "-")
        return f"{clean_vendor}-{last_octet}"

    return f"Perangkat-{last_octet}"

def ping_ip(ip):
    """Mengirim paket ICMP echo request (ping) tunggal ke alamat IP untuk memicu entri ARP."""
    try:
        if os.name == 'nt':
            cmd = ["ping", "-n", "1", "-w", "150", ip]
        else:
            cmd = ["ping", "-c", "1", "-W", "1", ip]
        subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1.5)
    except Exception:
        pass

def ping_sweep(subnet_prefix):
    """Melakukan ICMP ping sweep paralel ke seluruh subnet /24 menggunakan ThreadPoolExecutor untuk mengumpulkan ARP."""
    ips = [f"{subnet_prefix}.{i}" for i in range(1, 255)]
    with ThreadPoolExecutor(max_workers=80) as executor:
        executor.map(ping_ip, ips)

def parse_arp_table(local_ip):
    """Mengeksekusi 'arp -a' dan mem-parsing perangkat yang ditemukan (cross-platform Windows & Linux)."""
    devices = []
    try:
        startupinfo = None
        if os.name == 'nt':
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            
        output = subprocess.check_output(
            ["arp", "-a"], 
            universal_newlines=True, 
            startupinfo=startupinfo
        )
        
        # Masukkan komputer lokal sendiri
        devices.append({
            "ip": local_ip,
            "mac": "N/A (Host Lokal)",
            "vendor": "PC Host Anda",
            "hostname": socket.gethostname(),
            "type": "Local Host",
            "is_local": True
        })
        
        seen_ips = {local_ip}
        
        for line in output.splitlines():
            line = line.strip()
            if not line:
                continue
                
            ip = None
            mac = None
            link_type = "dynamic"
            
            # 1. Coba pola Linux: ? (192.168.10.1) at 1c:5f:2b:aa:bb:cc [ether] on eth0
            linux_match = re.search(r"\(([0-9\.]+)\)\s+at\s+([0-9a-fA-F:]{17})", line)
            if linux_match:
                ip = linux_match.group(1)
                mac = linux_match.group(2)
            else:
                # 2. Coba pola Windows: 192.168.1.1           1c-5f-2b-aa-bb-cc     dynamic
                win_match = re.search(r"([0-9\.]+)\s+([0-9a-fA-F\-:]{17})(?:\s+(\w+))?", line)
                if win_match:
                    ip = win_match.group(1)
                    mac = win_match.group(2)
                    if win_match.group(3):
                        link_type = win_match.group(3)
                        
            if ip and mac:
                if ip == "255.255.255.255" or ip.startswith("224.") or ip.startswith("239.") or ip == "127.0.0.1":
                    continue
                if ip in seen_ips:
                    continue
                    
                seen_ips.add(ip)
                mac_clean = mac.lower().replace("-", ":")
                vendor = get_mac_vendor(mac_clean)
                
                devices.append({
                    "ip": ip,
                    "mac": mac_clean,
                    "vendor": vendor,
                    "hostname": f"IP-{ip.replace('.', '-')}",
                    "type": link_type,
                    "is_local": False
                })
                
    except Exception as e:
        print(f"Error parsing ARP table: {e}")
        
    return devices

def scan_network():
    """Fungsi utama untuk memindai jaringan lokal secara cepat."""
    local_ip = get_local_ip()
    if local_ip == "127.0.0.1":
        return []
        
    # Ambil 3 oktet pertama untuk subnet (contoh: 192.168.1)
    ip_parts = local_ip.split(".")
    if len(ip_parts) < 4:
        return []
        
    local_prefix = ".".join(ip_parts[:3])
    
    # Subnet yang akan disapu (lokal + kelas target 26, 110, 1, 20, 83, 38)
    subnets = {local_prefix}
    class_octets = ["26", "110", "1", "20", "83", "38"]
    for octet in class_octets:
        subnets.add(f"192.168.{octet}")
        
    # Lakukan ping sweep secara paralel di thread terpisah agar cepat
    threads = []
    for subnet in subnets:
        t = threading.Thread(target=ping_sweep, args=(subnet,))
        t.daemon = True
        t.start()
        threads.append(t)
        
    for t in threads:
        t.join(timeout=1.5)
        
    # Ambil isi tabel ARP terbaru yang terkumpul
    devices = parse_arp_table(local_ip)
    return devices

if __name__ == "__main__":
    print("Memulai pemindaian jaringan lokal...")
    local_ip = get_local_ip()
    print(f"IP Lokal: {local_ip}")
    results = scan_network()
    print(f"Ditemukan {len(results)} perangkat:")
    for dev in results:
        print(f"- IP: {dev['ip']} | MAC: {dev['mac']} | Hostname: {dev['hostname']} | Vendor: {dev['vendor']}")
