#!/usr/bin/env python3
"""
Badde PC agent: lets your phone's Badde control this computer ("Hey Badde, open Need for Speed on my PC").

It only performs a fixed set of actions (open/close an app, open a website, lock, status, shutdown/restart)
and never runs arbitrary commands. It connects out to MyBadde (no ports to open) with the device token the app
showed you when you added this PC.

    python badde_pc_agent.py --token YOUR_DEVICE_TOKEN          (first run: saves the token)
    python badde_pc_agent.py                                     (later runs)

Requires Python 3.8+ only (no extra packages). Works on Windows, macOS and Linux.
"""
import argparse, json, os, platform, re, shutil, socket, subprocess, sys, time, urllib.error, urllib.request, webbrowser
from pathlib import Path

SERVER = "https://api.mybadde.com"
CONFIG = Path.home() / ".badde_agent.json"
SYSTEM = platform.system()  # Windows / Darwin / Linux


def api(method, path, token, body=None, timeout=40):
    req = urllib.request.Request(SERVER + path, method=method, headers={"Authorization": f"Device {token}", "Content-Type": "application/json", "User-Agent": "BaddePCAgent/1.0"},
                                 data=json.dumps(body).encode() if body is not None else None)
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read() or b"{}")


def words(s):
    return [w for w in re.split(r"[^a-z0-9]+", s.lower()) if w]


def best_match(name, candidates):
    """Pick the candidate whose name shares the most words with what was asked ("need for speed" -> "Need for Speed Heat")."""
    want = set(words(name))
    scored = []
    for label, target in candidates:
        have = set(words(label))
        score = len(want & have) / max(len(want), 1)
        if name.lower() in label.lower():
            score += 1
        if score >= 0.6:
            scored.append((score, -len(label), label, target))
    return max(scored)[2:] if scored else None


def app_shortcuts():
    found = []
    if SYSTEM == "Windows":
        roots = [Path(os.environ.get("ProgramData", r"C:\ProgramData")) / "Microsoft/Windows/Start Menu/Programs",
                 Path(os.environ.get("APPDATA", "")) / "Microsoft/Windows/Start Menu/Programs",
                 Path.home() / "Desktop", Path(os.environ.get("PUBLIC", r"C:\Users\Public")) / "Desktop"]
        for root in roots:
            if root.exists():
                for p in root.rglob("*"):
                    if p.suffix.lower() in (".lnk", ".url", ".exe"):
                        found.append((p.stem, str(p)))
    elif SYSTEM == "Darwin":
        for root in [Path("/Applications"), Path.home() / "Applications", Path("/System/Applications")]:
            if root.exists():
                for p in root.glob("*.app"):
                    found.append((p.stem, str(p)))
    else:
        for root in [Path("/usr/share/applications"), Path.home() / ".local/share/applications", Path("/var/lib/flatpak/exports/share/applications")]:
            if root.exists():
                for p in root.glob("*.desktop"):
                    m = re.search(r"^Name=(.+)$", p.read_text(errors="ignore"), re.M)
                    found.append(((m.group(1) if m else p.stem).strip(), str(p)))
    return found


def do_open(app):
    match = best_match(app, app_shortcuts())
    if not match:
        return False, f"I couldn't find {app} on this PC."
    label, target = match
    if SYSTEM == "Windows":
        os.startfile(target)  # noqa: works for .lnk/.url/.exe
    elif SYSTEM == "Darwin":
        subprocess.Popen(["open", target])
    else:
        subprocess.Popen(["gtk-launch", Path(target).stem] if shutil.which("gtk-launch") else ["xdg-open", target])
    return True, f"Opened {label}."


def do_close(app):
    want = words(app)
    if SYSTEM == "Windows":
        out = subprocess.run(["tasklist", "/fo", "csv", "/nh"], capture_output=True, text=True).stdout
        procs = {row.split('","')[0].strip('"') for row in out.splitlines() if row}
        hits = [p for p in procs if any(w in p.lower() for w in want if len(w) > 2)]
        for p in hits:
            subprocess.run(["taskkill", "/IM", p, "/F"], capture_output=True)
    elif SYSTEM == "Darwin":
        match = best_match(app, app_shortcuts())
        hits = [match[0]] if match else []
        for label in hits:
            subprocess.run(["osascript", "-e", f'quit app "{label}"'], capture_output=True)
    else:
        hits = [w for w in want if len(w) > 2 and subprocess.run(["pkill", "-f", w], capture_output=True).returncode == 0]
    return (True, f"Closed {app}.") if hits else (False, f"{app} doesn't seem to be running.")


def do_lock():
    if SYSTEM == "Windows":
        subprocess.run(["rundll32.exe", "user32.dll,LockWorkStation"])
    elif SYSTEM == "Darwin":
        subprocess.run(["pmset", "displaysleepnow"])
    else:
        subprocess.run(["loginctl", "lock-session"])
    return True, "Locked your PC."


def do_power(restart):
    if SYSTEM == "Windows":
        subprocess.run(["shutdown", "/r" if restart else "/s", "/t", "60", "/c", "Badde: requested from your phone. Run 'shutdown /a' to cancel."])
    else:
        subprocess.run(["shutdown", "-r" if restart else "-h", "+1"])
    return True, f"{'Restarting' if restart else 'Shutting down'} in one minute. You can still cancel it on the PC."


def do_status():
    up = ""
    if SYSTEM == "Linux" and Path("/proc/uptime").exists():
        up = f", up {int(float(Path('/proc/uptime').read_text().split()[0]) // 3600)} hours"
    disk = shutil.disk_usage(Path.home().anchor or "/")
    return True, f"{socket.gethostname()} is online ({platform.system()} {platform.release()}{up}), {disk.free // 2**30} GB free disk."


def run(cmd):
    a, args = cmd["action"], cmd.get("args") or {}
    if a == "open":
        return do_open(args.get("app", ""))
    if a == "close":
        return do_close(args.get("app", ""))
    if a == "url":
        url = args.get("url", "")
        if not re.match(r"^https?://", url):
            return False, "Only web links can be opened."
        webbrowser.open(url)
        return True, f"Opened {url}."
    if a == "lock":
        return do_lock()
    if a == "status":
        return do_status()
    if a in ("shutdown", "restart"):
        return do_power(a == "restart")
    return False, f"This PC doesn't know how to {a}."


def fail(message):
    """Exit with a message. The Windows .exe runs in its own console window, so keep it open long enough to read."""
    print(message)
    if getattr(sys, "frozen", False):
        try:
            input("Press Enter to close.")
        except EOFError:
            pass
    sys.exit(1)


def main():
    p = argparse.ArgumentParser(description="Badde PC agent")
    p.add_argument("--token", help="device token from the MyBadde app (saved after the first run)")
    opts = p.parse_args()
    cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
    token = opts.token or cfg.get("token")
    if not token and sys.stdin and sys.stdin.isatty():
        # Double-clicked (e.g. BaddePCAgent.exe): ask for the token instead of needing a command line.
        print("Add this PC in the MyBadde app (Devices > Add > PC) and paste the device token it shows you.")
        try:
            token = input("Device token: ").strip()
        except EOFError:
            token = ""
        opts.token = token or None
    if not token:
        fail("Add this PC in MyBadde (Devices > Add > PC), then run: python badde_pc_agent.py --token <token>")
    if opts.token:
        CONFIG.write_text(json.dumps({"token": token}))
        try:
            os.chmod(CONFIG, 0o600)
        except OSError:
            pass
    print("Badde PC agent is running. Leave this window open (or add it to startup). Ctrl+C to stop.")
    backoff = 5
    while True:
        try:
            reply = api("GET", "/agent/commands?wait=25", token)
            for cmd in reply.get("commands", []):
                try:
                    ok, result = run(cmd)
                except Exception as e:  # never let one command kill the agent
                    ok, result = False, f"That failed on the PC: {e}"
                print(f"[{time.strftime('%H:%M:%S')}] {cmd['action']} {cmd.get('args') or ''} -> {result}")
                api("POST", f"/agent/commands/{cmd['id']}", token, {"ok": ok, "result": result[:1000]})
            backoff = 5
        except urllib.error.HTTPError as e:
            if e.code == 401:
                if CONFIG.exists():
                    CONFIG.unlink()  # so the next start asks for a fresh token
                fail("This PC was removed from Badde (or the token is wrong). Add it again in the app.")
            time.sleep(backoff); backoff = min(backoff * 2, 120)
        except Exception:
            time.sleep(backoff); backoff = min(backoff * 2, 120)


if __name__ == "__main__":
    main()
