import socket
import subprocess
import threading
import time
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import os
import tempfile
from PIL import Image, ImageTk

PORT = 5555
TIMEOUT = 0.5
CHECK_INTERVAL = 10
known_devices = {}

# -------------------------
# NETWORK FUNCTIONS
# -------------------------
def ping(ip):
    try:
        subprocess.check_output(["ping", "-n", "1", "-w", "200", ip], stderr=subprocess.DEVNULL)
        return True
    except:
        return False

def get_hostname(ip):
    try:
        return socket.gethostbyaddr(ip)[0]
    except:
        return "Unknown"

def check_port(ip):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(TIMEOUT)
    try:
        s.connect((ip, PORT))
        return True
    except:
        return False
    finally:
        s.close()

# -------------------------
# ADB FUNCTIONS
# -------------------------
def adb_connect(ip):
    subprocess.getoutput(f"adb connect {ip}:5555")

def adb_ls(path, device):
    cmd = f'adb -s {device} shell "ls -1 \\"{path}\\""'
    return subprocess.getoutput(cmd).splitlines()

def adb_pull(remote, local, device):
    subprocess.call(f'adb -s {device} pull "{remote}" "{local}"', shell=True)

def adb_push(local, remote, device):
    subprocess.call(f'adb -s {device} push "{local}" "{remote}"', shell=True)

def adb_remove(remote, device):
    subprocess.call(f'adb -s {device} shell rm -r "{remote}"', shell=True)

def adb_install_apk(apk_path, device):
    subprocess.call(f'adb -s {device} install -r "{apk_path}"', shell=True)

def find_aapt():
    # Check PATH using return code (getoutput never raises, so check returncode)
    try:
        r = subprocess.run(["aapt", "version"], capture_output=True)
        if r.returncode == 0:
            return "aapt"
    except FileNotFoundError:
        pass
    # Search Android SDK build-tools directories
    sdk = os.environ.get("ANDROID_HOME") or os.environ.get("ANDROID_SDK_ROOT") or \
          os.path.expanduser("~/AppData/Local/Android/Sdk")
    bt = os.path.join(sdk, "build-tools")
    if os.path.isdir(bt):
        for v in sorted(os.listdir(bt), reverse=True):
            for name in ("aapt.exe", "aapt"):
                candidate = os.path.join(bt, v, name)
                if os.path.isfile(candidate):
                    return candidate
    return None

def adb_get_package_name(apk_path):
    aapt = find_aapt()
    if aapt:
        try:
            r = subprocess.run([aapt, "dump", "badging", apk_path],
                               capture_output=True, text=True, encoding="utf-8", errors="ignore")
            for line in r.stdout.splitlines():
                if line.startswith("package:"):
                    for part in line.split():
                        if part.startswith("name="):
                            return part.split("=")[1].strip("'")
        except Exception:
            pass
    # Fallback: scan binary AndroidManifest.xml for UTF-16LE encoded package name
    try:
        import zipfile, re
        with zipfile.ZipFile(apk_path, "r") as z:
            raw = z.read("AndroidManifest.xml")
        # Binary AXML: package name is stored as UTF-16LE
        text16 = raw.decode("utf-16-le", errors="ignore")
        m = re.search(r'([a-zA-Z][a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z][a-zA-Z0-9_]{1,50}){1,10})', text16)
        if m:
            return m.group(1)
    except Exception:
        pass
    return None

def adb_run_apk(package_name, device):
    subprocess.call(f'adb -s {device} shell monkey -p {package_name} -c android.intent.category.LAUNCHER 1', shell=True)

# -------------------------
# GUI FUNCTIONS
# -------------------------
def log(msg):
    log_box.insert(tk.END, msg + "\n")
    log_box.see(tk.END)

def update_table(ip, name, adb_status):
    if ip not in known_devices:
        color = "green" if adb_status=="OPEN" else "red"
        tree.insert("", "end", values=(ip, name, adb_status), tags=(color,))
        tree.tag_configure("green", background="#d0f0c0")
        tree.tag_configure("red", background="#f0d0d0")
        known_devices[ip] = True

def monitor_network():
    base = network_entry.get()
    log("Monitoring network...")
    while True:
        for i in range(1, 255):
            ip = f"{base}.{i}"
            if ip in known_devices:
                continue
            if ping(ip):
                name = get_hostname(ip)
                adb_open = check_port(ip)
                adb_status = "OPEN" if adb_open else "Closed"
                root.after(0, update_table, ip, name, adb_status)
                log(f"[DEVICE] {ip} ({name})")
                if adb_open:
                    log(f"  ➜ ADB port open")
                    adb_connect(ip)
        time.sleep(CHECK_INTERVAL)

# -------------------------
# FILE EXPLORER FUNCTIONS WITH THREADS
# -------------------------
def explore_device(event=None):
    selected = tree.selection()
    if not selected:
        return
    ip = tree.item(selected)["values"][0]
    device = f"{ip}:5555"

    def worker():
        try:
            files = adb_ls(current_path.get(), device)
            def update_gui():
                explorer.delete(*explorer.get_children())
                for f in files:
                    explorer.insert("", "end", values=(f,))
                preview_label.config(image="")
            root.after(0, update_gui)
        except Exception as e:
            log(f"Error exploring device: {e}")

    threading.Thread(target=worker, daemon=True).start()

def open_folder_manual(folder):
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    path = f"{current_path.get()}/{folder}"

    def worker():
        try:
            files = adb_ls(path, device)
            def update_gui():
                explorer.delete(*explorer.get_children())
                for f in files:
                    explorer.insert("", "end", values=(f,))
                current_path.set(path)
                preview_label.config(image="")
            root.after(0, update_gui)
        except Exception as e:
            log(f"Error opening folder: {e}")

    threading.Thread(target=worker, daemon=True).start()

def open_file_manual(filename):
    if "." not in filename:
        return
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    remote = f"{current_path.get()}/{filename}"
    temp_dir = os.path.join(tempfile.gettempdir(), "adb_preview")
    os.makedirs(temp_dir, exist_ok=True)
    local_path = os.path.join(temp_dir, filename)

    def worker():
        try:
            adb_pull(remote, local_path, device)
            root.after(0, lambda: os.startfile(local_path))
            log(f"{filename} opened successfully.")
        except Exception as e:
            log(f"Error opening file: {e}")

    threading.Thread(target=worker, daemon=True).start()

def download_file_manual(filename):
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    remote = f"{current_path.get()}/{filename}"
    local = filedialog.askdirectory()
    if not local:
        return

    def worker():
        try:
            adb_pull(remote, local, device)
            log(f"{filename} downloaded to {local}")
        except Exception as e:
            log(f"Error downloading file: {e}")

    threading.Thread(target=worker, daemon=True).start()

def upload_file_manual():
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    local_file = filedialog.askopenfilename()
    if not local_file:
        return
    remote_path = f"{current_path.get()}/{os.path.basename(local_file)}"

    def worker():
        try:
            adb_push(local_file, remote_path, device)
            log(f"{local_file} uploaded to {remote_path}")
            explore_device()
        except Exception as e:
            log(f"Error uploading file: {e}")

    threading.Thread(target=worker, daemon=True).start()

def remove_file_manual(filename):
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    remote = f"{current_path.get()}/{filename}"

    def worker():
        try:
            adb_remove(remote, device)
            log(f"{filename} removed from device")
            explore_device()
        except Exception as e:
            log(f"Error removing file: {e}")

    threading.Thread(target=worker, daemon=True).start()

def preview_image(event):
    selected = explorer.selection()
    if not selected:
        preview_label.config(image="")
        return
    filename = explorer.item(selected)["values"][0]
    if not filename.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif")):
        preview_label.config(image="")
        return
    device_item = tree.selection()
    if not device_item:
        return
    ip = tree.item(device_item)["values"][0]
    device = f"{ip}:5555"
    remote = f"{current_path.get()}/{filename}"
    temp_dir = os.path.join(tempfile.gettempdir(), "adb_preview")
    os.makedirs(temp_dir, exist_ok=True)
    local_path = os.path.join(temp_dir, filename)

    def worker():
        try:
            adb_pull(remote, local_path, device)
            img = Image.open(local_path)
            img.thumbnail((300,300))
            img_tk = ImageTk.PhotoImage(img)
            def update_gui():
                preview_label.img_tk = img_tk
                preview_label.config(image=img_tk)
            root.after(0, update_gui)
        except Exception as e:
            log(f"Error previewing image: {e}")

    threading.Thread(target=worker, daemon=True).start()

# -------------------------
# DEVICE RIGHT CLICK: Check ADB / Install / Run APK
# -------------------------
def check_adb_port_for_device(ip, tree_item=None):
    def worker():
        status = "OPEN" if check_port(ip) else "Closed"
        log(f"Checked {ip}: Port 5555 is {status}")
        def update_gui():
            if tree_item:
                tree.set(tree_item, column="ADB", value=status)
                color = "green" if status=="OPEN" else "red"
                tree.item(tree_item, tags=(color,))
                tree.tag_configure("green", background="#d0f0c0")
                tree.tag_configure("red", background="#f0d0d0")
        root.after(0, update_gui)
    threading.Thread(target=worker, daemon=True).start()

def install_apk_for_device(ip):
    apk_file = filedialog.askopenfilename(filetypes=[("APK Files", "*.apk")])
    if not apk_file:
        return
    device = f"{ip}:5555"
    def worker():
        try:
            adb_install_apk(apk_file, device)
            log(f"Installed APK {os.path.basename(apk_file)} on {ip}")
        except Exception as e:
            log(f"Error installing APK: {e}")
    threading.Thread(target=worker, daemon=True).start()

def run_apk_for_device(ip):
    device = f"{ip}:5555"

    def do_run(package_name):
        def worker():
            try:
                adb_run_apk(package_name, device)
                log(f"Ran {package_name} on {ip}")
            except Exception as e:
                log(f"Error running APK: {e}")
        threading.Thread(target=worker, daemon=True).start()

    def fetch_and_show():
        try:
            out = subprocess.getoutput(f"adb -s {device} shell pm list packages -3")
            packages = []
            for line in out.splitlines():
                if line.startswith("package:"):
                    packages.append(line.replace("package:", "").strip())
            if not packages:
                # fallback: all packages
                out2 = subprocess.getoutput(f"adb -s {device} shell pm list packages")
                for line in out2.splitlines():
                    if line.startswith("package:"):
                        packages.append(line.replace("package:", "").strip())
        except Exception as e:
            packages = []
            log(f"Error fetching packages: {e}")

        def show_dialog():
            if not packages:
                # No packages found — fallback to manual entry
                pkg = simpledialog.askstring("Run App",
                    "No packages found on device.\nEnter package name manually (e.g. tech.informatik.virus):")
                if pkg:
                    do_run(pkg.strip())
                return

            win = tk.Toplevel(root)
            win.title("Select App to Run")
            win.geometry("400x400")
            tk.Label(win, text="Select installed app:", font=("Helvetica", 11, "bold")).pack(pady=(10,5))
            lb = tk.Listbox(win, font=("Courier", 10))
            lb.pack(fill="both", expand=True, padx=10)
            for p in sorted(packages):
                lb.insert(tk.END, p)
            def on_run():
                sel = lb.curselection()
                if sel:
                    pkg = lb.get(sel[0])
                    win.destroy()
                    do_run(pkg)
            tk.Button(win, text="Run", bg="#4caf50", fg="white",
                      font=("Helvetica", 11, "bold"), command=on_run).pack(pady=8)
        root.after(0, show_dialog)

    log(f"Fetching installed apps from {ip}...")
    threading.Thread(target=fetch_and_show, daemon=True).start()

def device_right_click(event):
    item = tree.identify_row(event.y)
    if not item:
        return
    ip = tree.item(item)["values"][0]
    menu = tk.Menu(root, tearoff=0)
    menu.add_command(label="Check ADB Port", command=lambda: check_adb_port_for_device(ip, item))
    menu.add_command(label="Install APK", command=lambda: install_apk_for_device(ip))
    menu.add_command(label="Run APK", command=lambda: run_apk_for_device(ip))
    menu.tk_popup(event.x_root, event.y_root)

# -------------------------
# FILE EXPLORER RIGHT CLICK
# -------------------------
def explorer_right_click(event):
    selected = explorer.selection()
    if not selected:
        return
    filename = explorer.item(selected)["values"][0]
    menu = tk.Menu(root, tearoff=0)
    if "." in filename:
        menu.add_command(label="Open", command=lambda: open_file_manual(filename))
        menu.add_command(label="Download", command=lambda: download_file_manual(filename))
    else:
        menu.add_command(label="Open Folder", command=lambda: open_folder_manual(filename))
        menu.add_command(label="Upload", command=upload_file_manual)
    menu.add_command(label="Remove", command=lambda: remove_file_manual(filename))
    menu.tk_popup(event.x_root, event.y_root)

def show_about():
    messagebox.showinfo("About iN ADB Explorer",
                        "iN ADB Network Explorer v2.0\nBy iNformatik.tech\nScan and explore your Android devices over Wi-Fi/ADB.")

# -------------------------
# GUI SETUP
# -------------------------
root = tk.Tk()
root.title("iN ADB Network Explorer")
root.geometry("1100x750")
root.configure(bg="#f2f2f2")

root.rowconfigure(1, weight=2)
root.rowconfigure(2, weight=3)
root.rowconfigure(3, weight=1)
root.columnconfigure(0, weight=3)
root.columnconfigure(1, weight=1)

logo = tk.Label(root, text="iN", font=("Helvetica", 24, "bold"), fg="#ffffff", bg="#3b5998", width=4)
logo.place(x=10, y=10)

top_frame = tk.Frame(root, bg="#f2f2f2")
top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=10, pady=(50,10))
top_frame.columnconfigure(1, weight=1)
tk.Label(top_frame, text="Network Prefix:", bg="#f2f2f2").grid(row=0, column=0, sticky="w")
network_entry = tk.Entry(top_frame)
network_entry.grid(row=0, column=1, padx=5, sticky="ew")
tk.Button(top_frame, text="Start Monitor", bg="#4caf50", fg="white",
          command=lambda: threading.Thread(target=monitor_network, daemon=True).start()).grid(row=0, column=2, padx=5)
tk.Button(top_frame, text="About", bg="#2196f3", fg="white", command=show_about).grid(row=0, column=3, padx=5)

# Device table
tree_frame = tk.Frame(root)
tree_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=5)
tree_frame.rowconfigure(0, weight=1)
tree_frame.columnconfigure(0, weight=1)

tree = ttk.Treeview(tree_frame, columns=("IP", "Name", "ADB"), show="headings")
tree.heading("IP", text="IP Address")
tree.heading("Name", text="Device Name")
tree.heading("ADB", text="ADB Port")
tree.grid(row=0, column=0, sticky="nsew")
tree_scroll = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
tree_scroll.grid(row=0, column=1, sticky="ns")
tree.configure(yscrollcommand=tree_scroll.set)
tree.bind("<Double-1>", explore_device)
tree.bind("<Button-3>", device_right_click)

# File explorer
explorer_frame = tk.Frame(root)
explorer_frame.grid(row=2, column=0, sticky="nsew", padx=20, pady=5)
explorer_frame.rowconfigure(0, weight=1)
explorer_frame.columnconfigure(0, weight=1)

explorer = ttk.Treeview(explorer_frame, columns=("File"), show="headings")
explorer.heading("File", text="Device Storage")
explorer.grid(row=0, column=0, sticky="nsew")
explorer_scroll = ttk.Scrollbar(explorer_frame, orient="vertical", command=explorer.yview)
explorer_scroll.grid(row=0, column=1, sticky="ns")
explorer.configure(yscrollcommand=explorer_scroll.set)
explorer.bind("<ButtonRelease-1>", preview_image)
explorer.bind("<Button-3>", explorer_right_click)

file_controls = tk.Frame(root, bg="#f2f2f2")
file_controls.grid(row=3, column=0, sticky="ew", pady=5, padx=20)
current_path = tk.StringVar()
current_path.set("/storage/emulated/0")
tk.Label(file_controls, textvariable=current_path, bg="#f2f2f2").pack(side="left", padx=5)
tk.Button(file_controls, text="Upload File", bg="#ff9800", fg="white", command=upload_file_manual).pack(side="left")

preview_label = tk.Label(root, bg="#e0e0e0")
preview_label.grid(row=1, column=1, rowspan=2, sticky="nsew", padx=10, pady=10)

log_box = tk.Text(root)
log_box.grid(row=4, column=0, columnspan=2, sticky="nsew", padx=20, pady=5)
root.rowconfigure(4, weight=1)

root.mainloop()