#!/usr/bin/env python3
"""
KOS ADB Bridge - Local Windows Client WebSocket Server
=======================================================
Lance ce script une fois sur le PC client Windows qui a les telephones
branches en USB. Il expose un WebSocket sur ws://127.0.0.1:27183 que
le navigateur (Chrome/Edge) pourra contacter depuis la page KOS Farm.

Usage:
    python kos-adb-bridge.py

Dependances (installer une seule fois):
    pip install websockets

Teste avec : Python 3.8+, Windows 10/11
"""

import asyncio
import json
import subprocess
import sys
import os
from pathlib import Path

ROOT_DIR = Path(__file__).resolve().parent
VENV_PYTHON = ROOT_DIR / "venv" / "Scripts" / "python.exe"
if not VENV_PYTHON.exists():
    VENV_PYTHON = ROOT_DIR / ".venv" / "Scripts" / "python.exe"

# Auto-switch to local project venv if invoked as main script from a different Python interpreter
if __name__ == "__main__" and VENV_PYTHON.exists() and Path(sys.executable).resolve() != VENV_PYTHON.resolve():
    if os.environ.get("_KVFM_BRIDGE_VENV") != "1":
        env = os.environ.copy()
        env["_KVFM_BRIDGE_VENV"] = "1"
        res = subprocess.call([str(VENV_PYTHON)] + sys.argv, env=env)
        sys.exit(res)
import time
import logging
import ipaddress
import re
import shutil
from typing import Optional
from pathlib import Path

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)]
)
log = logging.getLogger("kos-adb-bridge")

BRIDGE_HOST = os.environ.get("KOS_BRIDGE_HOST", "127.0.0.1")
BRIDGE_PORT = int(os.environ.get("KOS_BRIDGE_PORT", "27183"))
ADB_TIMEOUT = 30
ADB_EXE = "adb"
DEFAULT_NETWORK_CIDR = os.environ.get("KOS_NETWORK_CIDR", "172.16.1.0/23")


def parse_network_info(output, expected_cidr=DEFAULT_NETWORK_CIDR):
    matches = re.findall(r"inet\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)", output)
    valid_ips = [(ip, int(prefix)) for ip, prefix in matches if not ip.startswith("127.")]

    try:
        expected_net = ipaddress.ip_network(expected_cidr, strict=False)
    except Exception:
        expected_net = ipaddress.ip_network("172.16.1.0/23", strict=False)
    expected_prefix = expected_net.prefixlen

    if not valid_ips:
        route_match = re.search(r"src\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", output)
        if route_match and not route_match.group(1).startswith("127."):
            ip = route_match.group(1)
            prefix_match = re.search(r"([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+).*src", output)
            prefix = int(prefix_match.group(2)) if prefix_match else 24
            valid_ips.append((ip, prefix))

    if not valid_ips:
        return {
            "valid": False,
            "ip": "",
            "prefix": 0,
            "netmask": "",
            "expected_cidr": str(expected_net),
            "is_mask_correct": False,
            "is_ip_in_range": False,
            "message": "Aucune adresse IPv4 detectee sur l'interface Wi-Fi (wlan0)."
        }

    ip, prefix = valid_ips[0]
    try:
        netmask = str(ipaddress.IPv4Network(f"0.0.0.0/{prefix}").netmask)
    except Exception:
        netmask = "255.255.255.0"

    is_mask_correct = (prefix == expected_prefix)
    try:
        ip_obj = ipaddress.ip_address(ip)
        is_ip_in_range = (ip_obj in expected_net)
    except Exception:
        is_ip_in_range = False

    valid = is_mask_correct and is_ip_in_range

    if not is_mask_correct:
        if prefix == 24 and expected_prefix == 23:
            message = (
                f"Attention: Masque incorrect (/24 au lieu de /23). "
                f"L'appareil a l'IP {ip}/24 (masque {netmask}), attendu {expected_cidr} "
                f"(masque {expected_net.netmask}). Corrigez le masque dans les parametres Wi-Fi du telephone."
            )
        else:
            message = (
                f"Masque de sous-reseau incorrect: /{prefix} ({netmask}) au lieu de /{expected_prefix} "
                f"({expected_net.netmask}). L'IP {ip} risque de ne pas joindre tous les hotes du cluster."
            )
    elif not is_ip_in_range:
        message = f"L'adresse IP {ip} n'appartient pas au reseau attendu {expected_net}."
    else:
        message = f"Configuration reseau Wi-Fi optimale: IP {ip}/{prefix} (masque {netmask}) conforme au sous-reseau {expected_net}."

    return {
        "valid": valid,
        "ip": ip,
        "prefix": prefix,
        "netmask": netmask,
        "expected_cidr": str(expected_net),
        "is_mask_correct": is_mask_correct,
        "is_ip_in_range": is_ip_in_range,
        "message": message
    }


def find_adb():
    # 1. Bundled directory in project (scrcpy-win64-v4.1)
    bundled = Path(__file__).resolve().parent / "scrcpy-win64-v4.1" / "adb.exe"
    if bundled.exists():
        return str(bundled)

    cwd_bundled = Path.cwd() / "scrcpy-win64-v4.1" / "adb.exe"
    if cwd_bundled.exists():
        return str(cwd_bundled)

    # 2. Standard system candidates
    for candidate in ("adb", "adb.exe"):
        try:
            result = subprocess.run([candidate, "version"], capture_output=True, text=True, timeout=5)
            if result.returncode == 0:
                return candidate
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass

    which_adb = shutil.which("adb") or shutil.which("adb.exe")
    if which_adb:
        return which_adb

    # 3. Known installation directories
    common_paths = [
        Path(r"C:\platform-tools\adb.exe"),
        Path(r"C:\platform-tools\scrcpy-win64-v4.1\adb.exe"),
        Path(r"C:\scrcpy\adb.exe"),
        Path(r"C:\tools\scrcpy\adb.exe"),
        Path(os.environ.get("LOCALAPPDATA", "")) / "Android/Sdk/platform-tools/adb.exe",
        Path(os.environ.get("USERPROFILE", "")) / "AppData/Local/Android/Sdk/platform-tools/adb.exe",
        Path("C:/Program Files (x86)/Android/android-sdk/platform-tools/adb.exe"),
        Path("C:/Android/platform-tools/adb.exe"),
    ]
    for p in common_paths:
        if p.exists():
            return str(p)
    raise FileNotFoundError("adb.exe introuvable. Installez Android Platform Tools et ajoutez-le au PATH.")


def find_scrcpy() -> Optional[str]:
    # 1. Dossier bundle dans le projet
    bundled = Path(__file__).resolve().parent / "scrcpy-win64-v4.1" / "scrcpy.exe"
    if bundled.exists():
        return str(bundled)

    # 2. C:\platform-tools\scrcpy.exe et C:\platform-tools\scrcpy-win64-v4.1\scrcpy.exe
    pt_candidates = [
        Path(r"C:\platform-tools\scrcpy.exe"),
        Path(r"C:\platform-tools\scrcpy-win64-v4.1\scrcpy.exe"),
    ]
    for p in pt_candidates:
        if p.exists():
            return str(p)

    # 3. PATH systeme
    which_path = shutil.which("scrcpy") or shutil.which("scrcpy.exe")
    if which_path:
        return which_path

    # 4. Chemins standards
    std_candidates = [
        Path(r"C:\scrcpy\scrcpy.exe"),
        Path(r"C:\tools\scrcpy\scrcpy.exe"),
        Path(os.environ.get("LOCALAPPDATA", "")) / "Programs/scrcpy/scrcpy.exe",
    ]
    for p in std_candidates:
        if p.exists():
            return str(p)

    return None


def run_adb(args, timeout=ADB_TIMEOUT):
    try:
        result = subprocess.run([ADB_EXE] + args, capture_output=True, text=True, timeout=timeout)
        return {"success": result.returncode == 0, "stdout": result.stdout.strip(), "stderr": result.stderr.strip()}
    except subprocess.TimeoutExpired:
        return {"success": False, "stdout": "", "stderr": f"Timeout apres {timeout}s"}
    except Exception as e:
        return {"success": False, "stdout": "", "stderr": str(e)}


def get_devices():
    res = run_adb(["devices", "-l"])
    devices = []
    for line in res["stdout"].splitlines()[1:]:
        line = line.strip()
        if not line:
            continue
        parts = line.split()
        if len(parts) < 2:
            continue
        serial, status = parts[0], parts[1]
        model = next((p.split(":", 1)[1] for p in parts[2:] if p.startswith("model:")), "Android Device")
        devices.append({"serial": serial, "status": status, "model": model})
    return devices


def shell(serial, command, timeout=ADB_TIMEOUT):
    return run_adb(["-s", serial, "shell", command], timeout=timeout)


async def handle_message(websocket, raw):
    try:
        msg = json.loads(raw)
    except json.JSONDecodeError:
        return {"error": "JSON invalide"}

    action = msg.get("action", "")
    serial = msg.get("serial", "")

    if action == "ping":
        return {"action": "pong", "adb": ADB_EXE, "bridge_version": "1.0.0"}

    elif action == "devices":
        return {"action": "devices", "devices": get_devices()}

    elif action == "shell":
        if not serial:
            return {"error": "serial requis"}
        command = msg.get("command", "")
        timeout = int(msg.get("timeout", ADB_TIMEOUT))
        res = shell(serial, command, timeout)
        return {"action": "shell", "serial": serial, "command": command,
                "success": res["success"], "output": res["stdout"] if res["stdout"] else res["stderr"]}

    elif action == "provision":
        if not serial:
            return {"error": "serial requis"}
        phone_name = msg.get("phone_name", serial)
        logs = []

        def run_step(cmd, desc):
            t = time.strftime("%H:%M:%S")
            r = shell(serial, cmd)
            tag = "OK" if r["success"] else "WARN"
            detail = (r["stdout"] or r["stderr"])[:120] or "OK"
            logs.append(f"[{t}] [{tag}] {desc} : {detail}")

        run_step("svc power stayon true", "Maintien ecran allume")
        run_step("settings put global window_animation_scale 0", "Animations OFF")
        run_step("settings put global transition_animation_scale 0", "Transitions OFF")
        run_step("settings put global animator_duration_scale 0", "Animator OFF")
        run_step("settings put global adaptive_battery_management_enable 0", "Batterie adaptative OFF")
        run_step("settings put global low_power 0", "Mode eco OFF")
        run_step("dumpsys battery set level 100", "Batterie factice 100%")
        run_step("dumpsys battery set temp 100", "Temperature factice OK")
        run_step("dumpsys deviceidle whitelist +com.termux", "Whitelist Termux Doze")
        run_step("dumpsys deviceidle whitelist +com.termux.boot", "Whitelist Termux:Boot Doze")
        run_step("cmd media_session volume --stream 3 --set 0 2>/dev/null || settings put system volume_music 0; settings put system volume_ring 0; settings put system volume_notification 0; settings put system volume_system 0; settings put system volume_alarm 0; settings put global mode_ringer 0; settings put global zen_mode 1", "Volumes & Mode Silence")
        run_step("settings put system vibrate_when_ringing 0; settings put system vibrate_on 0; settings put system vibrate_in_silent 0; settings put system haptic_feedback_enabled 0; settings put system sound_effects_enabled 0; settings put system lockscreen_sounds_enabled 0; settings put system sec_touch_sounds 0; settings put system sec_touch_vibration 0; settings put system miui_vibrate_in_silent 0; settings put system miui_haptic_feedback_level 0; settings put system touch_vibrate_mode 0", "Vibrations & Retours Haptiques OFF")
        run_step("cmd wifi set-power-mode 0 2>/dev/null || true; cmd wifi set-low-latency-mode enabled 2>/dev/null || true; settings put global wifi_sleep_policy 2 2>/dev/null || true; settings put global wifi_power_save 0 2>/dev/null || true; settings put global wifi_scan_always_enabled 0 2>/dev/null || true", "Wi-Fi Haute Performance (Anti-DTIM / Ping stable)")
        logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Optimisation 1-Clic terminee pour {phone_name} !")
        return {"action": "provision", "serial": serial, "success": True, "logs": logs}

    elif action == "inject_termux":
        if not serial:
            return {"error": "serial requis"}
        command = msg.get("command", "")
        special = set("&|;<>\"'\\*?~#")
        escaped = ""
        for ch in command:
            if ch == " ":
                escaped += "%s"
            elif ch in special:
                escaped += "\\" + ch
            else:
                escaped += ch
        run_adb(["-s", serial, "shell", "monkey", "-p", "com.termux", "-c", "android.intent.category.LAUNCHER", "1"])
        await asyncio.sleep(4.0)
        r_text = run_adb(["-s", serial, "shell", "input", "text", escaped])
        await asyncio.sleep(0.5)
        r_enter = run_adb(["-s", serial, "shell", "input", "keyevent", "66"])
        ok = r_text["success"] and r_enter["success"]
        return {"action": "inject_termux", "serial": serial, "success": ok,
                "message": "Commande injectee dans Termux avec succes !" if ok else "Echec injection"}

    elif action == "check_termux":
        if not serial:
            return {"error": "serial requis"}
        pm = shell(serial, "pm list packages com.termux")
        has_termux = "package:com.termux" in pm["stdout"]
        has_boot = "package:com.termux.boot" in pm["stdout"]
        net = shell(serial, "netstat -tlpn 2>/dev/null || ss -tlpn 2>/dev/null")
        has_sshd = ":8022" in net["stdout"]
        ps = shell(serial, "ps -A 2>/dev/null || ps")
        is_mining = any(x in ps["stdout"] for x in ["ccminer", "xmrig", "verus"])
        summary = f"Termux: {'OK' if has_termux else 'MANQUANT'} | Boot: {'OK' if has_boot else 'MANQUANT'} | SSHD: {'ACTIF' if has_sshd else 'INACTIF'} | Minage: {'ACTIF' if is_mining else 'ARRETE'}"
        return {"action": "check_termux", "serial": serial, "has_termux": has_termux,
                "has_termux_boot": has_boot, "is_sshd_running": has_sshd, "is_mining": is_mining, "summary": summary}

    elif action == "reset_termux":
        if not serial:
            return {"error": "serial requis"}
        run_adb(["-s", serial, "shell", "pkill -f ccminer || true"])
        r = shell(serial, "pm clear com.termux")
        logs = [f"ccminer arrete.", f"Termux reinitialise : {r['stdout'] or r['stderr']}"]
        return {"action": "reset_termux", "serial": serial, "success": True, "logs": logs}

    elif action == "open_wifi_settings":
        if not serial:
            return {"error": "serial requis"}
        r = shell(serial, "am start -a android.settings.WIFI_SETTINGS")
        return {"action": "open_wifi_settings", "serial": serial, "success": r["success"],
                "message": "Paramètres Wi-Fi ouverts sur le smartphone." if r["success"] else f"Échec ouverture: {r['stderr']}"}

    elif action in ("screen_off", "finish_setup_screen_off"):
        if not serial:
            return {"error": "serial requis"}
        shell(serial, "svc power stayon false")
        r = shell(serial, "input keyevent 26")
        return {"action": action, "serial": serial, "success": r["success"],
                "message": "Écran éteint avec succès (fin de setup)." if r["success"] else "Échec extinction écran"}

    elif action == "check_network":
        if not serial:
            return {"error": "serial requis"}
        expected_cidr = msg.get("expected_cidr") or DEFAULT_NETWORK_CIDR
        res_wlan = shell(serial, "ip -4 addr show wlan0")
        output = res_wlan["stdout"] if res_wlan["success"] else ""
        if not output or "inet" not in output:
            res_all = shell(serial, "ip -4 addr show")
            res_route = shell(serial, "ip route")
            output = f"{output}\n{res_all['stdout']}\n{res_route['stdout']}"
        net_info = parse_network_info(output, expected_cidr)
        return {"action": "check_network", "serial": serial, **net_info}

    elif action == "network_info":
        if not serial:
            return {"error": "serial requis"}
        r = shell(serial, "ip -f inet addr show wlan0 2>/dev/null || ip addr show wlan0 2>/dev/null || ip addr 2>/dev/null || ifconfig wlan0 2>/dev/null")
        return {"action": "network_info", "serial": serial, "success": r["success"], "output": r["stdout"]}

    elif action == "all_in_one":
        if not serial:
            return {"error": "serial requis"}
        phone_name = msg.get("phone_name", serial)
        command = msg.get("command", "")
        expected_cidr = msg.get("expected_cidr") or DEFAULT_NETWORK_CIDR
        logs = []

        def run_step(cmd, desc):
            t = time.strftime("%H:%M:%S")
            r = shell(serial, cmd)
            tag = "OK" if r["success"] else "WARN"
            detail = (r["stdout"] or r["stderr"])[:120] or "OK"
            logs.append(f"[{t}] [{tag}] {desc} : {detail}")

        # Étape 1 : Optimisation 1-clic
        logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 1/5 : Démarrage Optimisation 1-Clic...")
        run_step("svc power stayon true", "Maintien écran allumé")
        run_step("settings put global window_animation_scale 0", "Animations OFF")
        run_step("settings put global transition_animation_scale 0", "Transitions OFF")
        run_step("settings put global animator_duration_scale 0", "Animator OFF")
        run_step("settings put global adaptive_battery_management_enable 0", "Batterie adaptative OFF")
        run_step("settings put global low_power 0", "Mode éco OFF")
        run_step("dumpsys battery set level 100", "Batterie factice 100%")
        run_step("dumpsys battery set temp 100", "Température factice OK")
        run_step("dumpsys deviceidle whitelist +com.termux", "Whitelist Termux Doze")
        run_step("dumpsys deviceidle whitelist +com.termux.boot", "Whitelist Termux:Boot Doze")
        run_step("cmd media_session volume --stream 3 --set 0 2>/dev/null || settings put system volume_music 0; settings put system volume_ring 0; settings put system volume_notification 0; settings put system volume_system 0; settings put system volume_alarm 0; settings put global mode_ringer 0; settings put global zen_mode 1", "Volumes & Mode Silence")
        run_step("settings put system vibrate_when_ringing 0; settings put system vibrate_on 0; settings put system vibrate_in_silent 0; settings put system haptic_feedback_enabled 0; settings put system sound_effects_enabled 0; settings put system lockscreen_sounds_enabled 0; settings put system sec_touch_sounds 0; settings put system sec_touch_vibration 0; settings put system miui_vibrate_in_silent 0; settings put system miui_haptic_feedback_level 0; settings put system touch_vibrate_mode 0", "Vibrations & Retours Haptiques OFF")
        run_step("cmd wifi set-power-mode 0 2>/dev/null || true; cmd wifi set-low-latency-mode enabled 2>/dev/null || true; settings put global wifi_sleep_policy 2 2>/dev/null || true; settings put global wifi_power_save 0 2>/dev/null || true; settings put global wifi_scan_always_enabled 0 2>/dev/null || true", "Wi-Fi Haute Performance (Anti-DTIM / Ping stable)")
        run_step("setprop service.adb.tcp.port 5555 2>/dev/null || true; setprop persist.adb.tcp.port 5555 2>/dev/null || true; settings put global adb_wifi_enabled 1 2>/dev/null || true", "Mode ADB over TCP/IP Persistant (Port 5555)")
        run_adb(["-s", serial, "tcpip", "5555"])
        logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Optimisation 1-Clic terminée.")

        # Étape 2 : Vérification IP & Masque CIDR
        logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 2/5 : Vérification IP & Masque CIDR...")
        res_wlan = shell(serial, "ip -4 addr show wlan0")
        out_net = res_wlan["stdout"] if res_wlan["success"] else ""
        if not out_net or "inet" not in out_net:
            res_all = shell(serial, "ip -4 addr show")
            res_route = shell(serial, "ip route")
            out_net = f"{out_net}\n{res_all['stdout']}\n{res_route['stdout']}"
        net_info = parse_network_info(out_net, expected_cidr)
        if not net_info["valid"]:
            if not net_info["is_mask_correct"] and net_info["prefix"] == 24:
                logs.append(f"[{time.strftime('%H:%M:%S')}] [ERR] Réseau Wi-Fi : Masque incorrect (/24 au lieu de /23) ! IP: {net_info['ip']}/24 (masque {net_info['netmask']}), attendu {net_info['expected_cidr']}. Veuillez corriger le masque.")
            else:
                logs.append(f"[{time.strftime('%H:%M:%S')}] [ERR] Réseau Wi-Fi : {net_info['message']}")
            return {"action": "all_in_one", "serial": serial, "success": False, "network": net_info, "logs": logs}
        else:
            logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Réseau Wi-Fi : IP {net_info['ip']}/{net_info['prefix']} ({net_info['netmask']}) conforme au réseau attendu.")

        # Étape 3 : Diagnostic Termux
        logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 3/5 : Vérification Termux & SSH...")
        pm = shell(serial, "pm list packages com.termux")
        has_termux = "package:com.termux" in pm["stdout"]
        has_boot = "package:com.termux.boot" in pm["stdout"]
        net = shell(serial, "netstat -tlpn 2>/dev/null || ss -tlpn 2>/dev/null")
        has_sshd = ":8022" in net["stdout"]
        ps = shell(serial, "ps -A 2>/dev/null || ps")
        is_mining = any(x in ps["stdout"] for x in ["ccminer", "xmrig", "verus"])
        termux_summary = f"Termux: {'OK' if has_termux else 'MANQUANT'} | Boot: {'OK' if has_boot else 'MANQUANT'} | SSHD: {'ACTIF' if has_sshd else 'INACTIF'} | Minage: {'ACTIF' if is_mining else 'ARRETE'}"
        termux_info = {"has_termux": has_termux, "has_termux_boot": has_boot, "is_sshd_running": has_sshd, "is_mining": is_mining, "summary": termux_summary}
        logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Diagnostic Termux : {termux_summary}")

        # Étape 4 : Injection commande Termux
        if command and command.strip():
            logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 4/5 : Lancement Termux et injection de la commande d'enrôlement...")
            special = set("&|;<>\"'\\*?~#")
            escaped = ""
            for ch in command.strip():
                if ch == " ":
                    escaped += "%s"
                elif ch in special:
                    escaped += "\\" + ch
                else:
                    escaped += ch
            run_adb(["-s", serial, "shell", "monkey", "-p", "com.termux", "-c", "android.intent.category.LAUNCHER", "1"])
            await asyncio.sleep(7.0)
            r_text = run_adb(["-s", serial, "shell", "input", "text", escaped])
            await asyncio.sleep(0.5)
            r_enter = run_adb(["-s", serial, "shell", "input", "keyevent", "66"])
            if r_text["success"] and r_enter["success"]:
                logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Commande d'enrôlement injectée et exécutée dans Termux.")
            else:
                logs.append(f"[{time.strftime('%H:%M:%S')}] [WARN] Échec injection commande dans Termux.")
        else:
            logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 4/5 : Aucune commande fournie, injection ignorée.")

        # Étape 5 : Surveillance active du démarrage du minage
        logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Étape 5/5 : Surveillance active de l'installation et du démarrage du minage...")
        mining_detected = False
        max_checks = 35  # 35 * 3s = 105 secondes max pour téléchargement & démarrage
        for check in range(1, max_checks + 1):
            await asyncio.sleep(3.0)
            ps_res = shell(serial, "ps -A 2>/dev/null || ps")
            net_res = shell(serial, "netstat -tlpn 2>/dev/null || ss -tlpn 2>/dev/null")
            ps_out = ps_res["stdout"] if ps_res["success"] else ""
            net_out = net_res["stdout"] if net_res["success"] else ""

            is_mining_proc = any(x in ps_out for x in ["ccminer", "primo-arm-miner", "xmrig", "verus"])
            is_rpc_open = ":4068" in net_out

            if is_mining_proc or is_rpc_open:
                mining_detected = True
                active_bin = "ccminer" if "ccminer" in ps_out else ("primo-arm-miner" if "primo-arm-miner" in ps_out else "processus de minage")
                logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Minage actif confirmé ! ({active_bin} détecté, Port RPC 4068: {'🟢 En écoute' if is_rpc_open else 'Initialisation'}).")
                break

            if check % 4 == 0:
                logs.append(f"[{time.strftime('%H:%M:%S')}] [INFO] Installation / Compilation en cours... ({check * 3}s écoulées)")

        if not mining_detected:
            logs.append(f"[{time.strftime('%H:%M:%S')}] [WARN] Le minage n'a pas été détecté après {max_checks * 3}s. L'écran est MAINTENU ALLUMÉ pour inspection.")

        logs.append(f"[{time.strftime('%H:%M:%S')}] [OK] Pipeline ALL-IN-ONE exécuté avec succès pour {phone_name} !")
        return {
            "action": "all_in_one",
            "serial": serial,
            "success": True,
            "network": net_info,
            "termux": termux_info,
            "logs": logs,
            "message": f"Pipeline ALL-IN-ONE terminé avec succès pour {phone_name} !"
        }

    elif action == "scrcpy":
        if not serial:
            return {"error": "serial requis"}
        model = msg.get("model") or "Android Device"
        scrcpy_path = find_scrcpy()
        if not scrcpy_path:
            return {
                "action": "scrcpy",
                "serial": serial,
                "success": False,
                "error": "scrcpy.exe introuvable. Veuillez placer scrcpy dans scrcpy-win64-v4.1 ou C:\\platform-tools\\scrcpy.exe ou l'ajouter au PATH."
            }
        try:
            target_serial = serial
            if "." in target_serial and ":" not in target_serial:
                target_serial = f"{target_serial}:5555"
            
            if "." in target_serial:
                try:
                    subprocess.run([ADB_EXE, "connect", target_serial], capture_output=True, timeout=4)
                except Exception:
                    pass

            window_title = f"KVFM - {model} ({target_serial})"
            subprocess.Popen([scrcpy_path, "-s", target_serial, "--window-title", window_title])
            return {
                "action": "scrcpy",
                "serial": target_serial,
                "success": True,
                "message": f"Fenêtre scrcpy ouverte pour {target_serial} ({model}) via {scrcpy_path}"
            }
        except Exception as e:
            return {
                "action": "scrcpy",
                "serial": serial,
                "success": False,
                "error": f"Échec du lancement de scrcpy: {e}"
            }

    else:
        return {"error": f"Action inconnue: {action}"}


async def handler(websocket):
    client = getattr(websocket, "remote_address", "?")
    log.info(f"Connexion depuis {client}")
    try:
        async for raw in websocket:
            msg_id = None
            try:
                msg = json.loads(raw)
                msg_id = msg.get("_id")
            except Exception:
                pass
            try:
                response = await handle_message(websocket, raw)
            except Exception as e:
                response = {"error": str(e)}
            if isinstance(response, dict) and msg_id and "_id" not in response:
                response["_id"] = msg_id
            await websocket.send(json.dumps(response, ensure_ascii=False))
    except Exception as e:
        log.warning(f"Connexion fermee ({client}): {e}")


async def main():
    global ADB_EXE
    try:
        ADB_EXE = find_adb()
        log.info(f"ADB trouve : {ADB_EXE}")
    except FileNotFoundError as e:
        log.error(str(e))
        sys.exit(1)

    test = run_adb(["version"])
    if not test["success"]:
        log.error(f"ADB ne repond pas : {test['stderr']}")
        sys.exit(1)
    log.info(f"ADB OK : {test['stdout'].splitlines()[0]}")

    scrcpy_bin = find_scrcpy()
    if scrcpy_bin:
        log.info(f"scrcpy trouve : {scrcpy_bin}")
    else:
        log.info("scrcpy non detecte (optionnel pour screen mirroring).")

    try:
        import websockets
    except ImportError:
        log.error("Module 'websockets' absent. Lancez : pip install websockets")
        sys.exit(1)

    def process_response(*args, **kwargs):
        response = kwargs.get("response") or (args[-1] if args else None)
        if response and hasattr(response, "headers"):
            response.headers["Access-Control-Allow-Origin"] = "*"
            response.headers["Access-Control-Allow-Private-Network"] = "true"
            return response
        return None

    log.info(f"KOS ADB Bridge pret sur ws://{BRIDGE_HOST}:{BRIDGE_PORT} (0.0.0.0 / 127.0.0.1)")
    log.info("En attente de connexions depuis le navigateur...")

    try:
        server = websockets.serve(
            handler,
            BRIDGE_HOST,
            BRIDGE_PORT,
            origins=None,
            process_response=process_response
        )
    except TypeError:
        server = websockets.serve(handler, BRIDGE_HOST, BRIDGE_PORT)

    async with server:
        await asyncio.Future()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log.info("Bridge arrete.")
