"""
Desktop clicker client.
On launch it asks for a token and display name, briefly connects to show
the caller's current 4 button titles, then asks which button (1-4) to
listen for and restates the chosen one by name. The token's expiry clock
only starts once it's entered here and the server accepts the connection -
not when the token was created, and not just from launching the exe.

This is a "one-way street" by design: to listen to a different button,
close this and run it again picking a different one. It does not switch
channels while running.

Plays a short "ding" whenever its own button fires, and a lower "dong"
whenever the caller edits that same button's label (Windows only, via
winsound; falls back to a plain terminal bell elsewhere).

Checks its own CLIENT_VERSION against what the server currently expects
and prints a clear notice (with the download link) if this build is behind.

pip install websocket-client pyautogui
python desktop_client.py

Build to .exe:
pip install pyinstaller
pyinstaller --onefile --name ClickerClient desktop_client.py
(exe will be in dist/ClickerClient.exe)
"""

import json
import time
import sys

import websocket   # pip install websocket-client
import pyautogui   # pip install pyautogui

try:
    import winsound  # built into Python on Windows
except ImportError:
    winsound = None

# Edit this before building the exe — the one server every client talks to:
SERVER_URL = "ws://64.176.22.218:8080"
# Edit this to match EXE_DOWNLOAD_URL in caller.html - shown to clients so they
# always know where to get the latest version, and printed louder if this build
# is behind what the server currently expects (see admin.js set-exe-version):
EXE_DOWNLOAD_URL = "https://your-download-link-here"
# Bump this string every time you rebuild with real changes:
CLIENT_VERSION = "1.0.0"

FATAL_CODES = {4001, 4002, 4003, 4004, 4006}  # invalid role/code, invalid token, expired token, unassigned token, token in use elsewhere

RED = "\033[91m"
RESET = "\033[0m"


def ding():
    """Plays when THIS client's button fires."""
    if winsound:
        winsound.Beep(1000, 150)
    else:
        print("\a", end="", flush=True)  # fallback terminal bell on non-Windows


def dong():
    """Plays when the caller edits a button label (any button, as a heads-up)."""
    if winsound:
        winsound.Beep(400, 200)
    else:
        print("\a", end="", flush=True)


def fetch_labels(url, token):
    """Briefly connects to read the caller's current 4 button titles before
    the user has to pick one. Failure here is non-fatal - if it doesn't
    work (bad token, network hiccup), we just skip straight to the prompt
    and let the real connection report the actual error, if any."""
    preview_url = f"{url}?role=desktop&token={token}&name=preview&channel=1"
    try:
        conn = websocket.create_connection(preview_url, timeout=5)
        raw = conn.recv()
        conn.close()
        msg = json.loads(raw)
        if msg.get("type") == "labels":
            return msg.get("labels", {})
    except Exception as e:
        print(f"(couldn't preview button titles: {e})")
    return None


def prompt_credentials():
    print("=== Clicker client setup ===")
    token = input("Client token (given to you by the caller): ").strip()
    name = input("Your display name [default: this PC's name]: ").strip() or "client"

    labels = fetch_labels(SERVER_URL, token)
    if labels:
        print("Current buttons:")
        print_labels(labels, channel=0)  # channel=0 so nothing is marked "<- you" yet

    channel_raw = input("Which button are you listening for? (1-4) [default 1]: ").strip()
    try:
        channel = int(channel_raw) if channel_raw else 1
    except ValueError:
        channel = 1
    channel = min(4, max(1, channel))

    if labels:
        chosen = labels.get(str(channel), labels.get(channel, f"Button {channel}"))
        print(f"-> You picked button {channel}: {chosen}")

    return SERVER_URL, token, name, channel


def print_labels(labels, channel, updated_button=None):
    print("--- Buttons ---")
    for i in range(1, 5):
        text = labels.get(str(i), labels.get(i, f"Button {i}"))
        marker = " <- you" if i == channel else ""
        line = f"  {i}: {text}{marker}"
        if updated_button is not None and int(updated_button) == i:
            print(f"{RED}{line}{RESET}")
        else:
            print(line)
    print("---------------")


def connect(url, token, name, channel):
    def on_open(ws):
        print(f"[{name}] connected, listening on button {channel}...")
        print("(This supplements an audio service the caller already provides - it doesn't")
        print(" necessarily replace it unless both of you are crystal clear and experienced.)")
        print("--- Notes ---")
        print("A. Your token's lifespan began the moment you entered it just now.")
        print("B. A button click fires wherever your mouse is currently sitting - if")
        print("   you're stepping away for a while, park it over a harmless, empty spot.")
        print("C. You chose which button (1-4) your mouse responds to when you launched this.")
        print("D. The caller can change button labels anytime - this window reflects that live.")
        print("E. This exe only needs to be downloaded once.")
        print("F. This token only works for the caller who gave it to you - it won't work")
        print("   for a different caller.")
        print("G. To use a different caller, just run this exe again with a different token.")
        print("H. Labels are just descriptive text - they don't change what actually happens.")
        print("   A click always just clicks wherever your mouse is. Watch the labels below")
        print("   and keep your mouse in the correct, consistent spot for what they mean.")
        print(f"I. Save this link in case a newer version is ever released: {EXE_DOWNLOAD_URL}")
        print("-------------")

    def on_message(ws, message):
        msg = json.loads(message)
        if msg.get("type") == "go":
            ding()
            pyautogui.click()
            ws.send(json.dumps({"type": "clicked"}))
            print(f"[{name}] clicked (button {channel}), reporting back")
        elif msg.get("type") == "labels":
            updated = msg.get("updatedButton")
            if updated is not None and int(updated) == channel:
                dong()
            print_labels(msg.get("labels", {}), channel, updated)
        elif msg.get("type") == "versionInfo":
            latest = msg.get("latestVersion")
            if latest and latest != CLIENT_VERSION:
                print(f"{RED}*** A newer version ({latest}) is available - you're running {CLIENT_VERSION} ***{RESET}")
                print(f"{RED}*** Download the latest: {EXE_DOWNLOAD_URL} ***{RESET}")

    def on_error(ws, error):
        print(f"[{name}] error:", error)

    def on_close(ws, close_status_code=None, close_msg=None):
        if close_status_code in FATAL_CODES:
            print(f"[{name}] rejected by server: {close_msg} (code {close_status_code}).")
            print("This token is invalid, revoked, or expired. Ask the caller for a new one.")
            return
        print(f"[{name}] disconnected, reconnecting in 2s...")
        time.sleep(2)
        ws_app = build(url, token, name, channel)
        ws_app.run_forever()

    def build(u, t, n, c):
        full_url = f"{u}?role=desktop&token={t}&name={n}&channel={c}"
        return websocket.WebSocketApp(
            full_url,
            on_open=on_open,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
        )

    build(url, token, name, channel).run_forever()


if __name__ == "__main__":
    if len(sys.argv) > 3:
        # optional non-interactive mode: python desktop_client.py <token> <name> <channel>
        client_token, display_name, channel_arg = sys.argv[1], sys.argv[2], sys.argv[3]
        client_channel = min(4, max(1, int(channel_arg)))
        server_url = SERVER_URL
    else:
        server_url, client_token, display_name, client_channel = prompt_credentials()

    connect(server_url, client_token, display_name, client_channel)
