#!/usr/bin/python3

import base64
import binascii
import json
import os
import re
import sys
import time
import subprocess
import tempfile
import hashlib
import threading
from pathlib import Path

LAUNCHER = ".droiddeck-launch.bat"
SIDECAR = ".droiddeck-store.json"
EPIC_CODE = ".droiddeck-epic-code"
EPIC_ARGS = ".droiddeck-epic-args"
EPIC_CLIENT = "/usr/local/bin/droiddeck-epic"
EPIC_CLIENT_CURRENT = ".local/share/droiddeck-epic/current.json"
EPIC_HANDLED = "DROIDDECK_EPIC_HANDLED"
CLIENT_TIMEOUT = 150.0
TIMEOUT = 5.0
STALE_SECONDS = 300
SERVER_WAIT = 8.0
BROWSER = "/usr/local/bin/droiddeck-open-url"
EOS_KEY = "[Software\\\\Epic Games\\\\EOS]"
BROWSER_KEY = "[Software\\\\Wine\\\\WineBrowser]"


def log(line):
    print("droiddeck-store-launch: " + line, file=sys.stderr, flush=True)


MAX_DEPTH = 6


def sidecar_at(folder):
    try:
        sidecar = json.loads((folder / SIDECAR).read_text())
    except (OSError, ValueError):
        return None
    if not isinstance(sidecar, dict) or not sidecar.get("store") or not sidecar.get("id"):
        return None
    return sidecar


def find_store_game(argv):
    for raw in argv:
        arg = raw.strip().strip('"').strip("'").replace("\\", "/")
        low = arg.lower()
        if not (low.endswith(LAUNCHER) or low.endswith(".exe") or low.endswith(".bat")) or "/" not in arg:
            continue
        folder = Path(arg).parent
        for _ in range(MAX_DEPTH):
            sidecar = sidecar_at(folder)
            if sidecar is not None:
                return folder, sidecar
            if folder.parent == folder:
                break
            folder = folder.parent
    return None


def drop_stale(folder, now=None):
    code = folder / EPIC_CODE
    try:
        if (now if now is not None else time.time()) - code.stat().st_mtime > STALE_SECONDS:
            code.unlink()
            return True
    except OSError:
        pass
    return False


def client_wanted():
    home = os.environ.get("HOME") or "/root"
    return os.access(EPIC_CLIENT, os.X_OK) and os.path.isfile(os.path.join(home, EPIC_CLIENT_CURRENT))


def through_client(folder, sidecar, timeout=CLIENT_TIMEOUT):
    extra = sidecar.get("extra") if isinstance(sidecar.get("extra"), dict) else {}
    namespace, item = str(extra.get("namespace") or ""), str(extra.get("catalogItemId") or "")
    if not namespace or not item:
        return "no-catalog-ids"
    command = [EPIC_CLIENT, "launch", "--app", str(sidecar.get("id")), "--namespace", namespace, "--item", item,
               "--folder", str(folder), "--owner", str(os.getppid()), "--timeout", str(int(timeout))]
    try:
        run = subprocess.run(command, stdout=subprocess.PIPE, stdin=subprocess.DEVNULL, timeout=timeout + 30)
        answer = json.loads(run.stdout.decode("utf-8", "replace").strip().splitlines()[-1])
    except (OSError, ValueError, IndexError, subprocess.TimeoutExpired) as error:
        return "error-" + type(error).__name__
    if not isinstance(answer, dict):
        return "no-answer"
    if answer.get("ok") and (folder / EPIC_ARGS).is_file():
        return "ok"
    return str(answer.get("reason") or "failed")


def ask(channel, request, timeout=TIMEOUT):
    req_dir, resp_dir = channel / "req", channel / "resp"
    req_dir.mkdir(parents=True, exist_ok=True)
    resp_dir.mkdir(parents=True, exist_ok=True)
    rid = "%d-%d" % (os.getpid(), int(time.time() * 1000))
    tmp = req_dir / (rid + ".json.tmp")
    tmp.write_text(json.dumps(request))
    os.rename(tmp, req_dir / (rid + ".json"))
    answer = resp_dir / (rid + ".json")
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            value = json.loads(answer.read_text())
            answer.unlink()
            return value
        except (OSError, ValueError):
            time.sleep(0.05)
    try:
        (req_dir / (rid + ".json")).unlink()
    except OSError:
        pass
    return None


def windows_path(unix):
    return "Z:" + unix.replace("/", "\\")


def reg_escape(value):
    return value.replace("\\", "\\\\").replace('"', '\\"')


def set_value(reg, key, name, value, now=None):
    if not reg.is_file():
        return False
    line = '"%s"="%s"' % (name, reg_escape(value))
    inside, last = False, None
    for row in reg.read_text(errors="replace").splitlines():
        if row.startswith("["):
            inside = row.startswith(key + " ") or row == key
        elif inside and row.startswith('"%s"=' % name):
            last = row
    if last == line:
        return True
    with open(reg, "a") as out:
        out.write("\n%s %d\n%s\n" % (key, int(now if now is not None else time.time()), line))
    return True


def remove_value(reg, key, name):
    if not reg.is_file():
        return False
    rows, inside, dropped = [], False, False
    for row in reg.read_text(errors="replace").splitlines(keepends=True):
        if row.startswith("["):
            inside = row.startswith(key + " ") or row.rstrip("\n") == key
        elif inside and row.startswith('"%s"=' % name):
            dropped = True
            continue
        rows.append(row)
    if dropped:
        tmp = reg.with_name(reg.name + ".dd-new")
        tmp.write_text("".join(rows))
        os.replace(tmp, reg)
    return dropped


def options(sidecar):
    e = sidecar.get("epic") if isinstance(sidecar.get("epic"), dict) else {}
    return {"eos": e.get("eos", True) is not False, "offline": e.get("offline", False) is True}


def server_socket(pfx):
    try:
        st = os.stat(pfx)
    except OSError:
        return None
    base = os.environ.get("BL_WINE_SERVER_BASE") or "/tmp/.wine-%d" % os.getuid()
    return Path(base) / ("server-%x-%x" % (st.st_dev, st.st_ino)) / "socket"


def wait_for_server(pfx, timeout=SERVER_WAIT):
    sock = server_socket(pfx)
    if sock is None:
        return True
    deadline = time.monotonic() + timeout
    while sock.exists() and time.monotonic() < deadline:
        time.sleep(0.1)
    return not sock.exists()


def has_value(reg, key, name, value=None):
    if not reg.is_file():
        return False
    inside, last = False, None
    for row in reg.read_text(errors="replace").splitlines():
        if row.startswith("["):
            inside = row.startswith(key + " ") or row == key
        elif inside and row.startswith('"%s"=' % name):
            last = row
    if last is None:
        return False
    return value is None or last == '"%s"="%s"' % (name, reg_escape(value))


def provision_prefix(compat_data, wait=SERVER_WAIT):
    if not compat_data:
        return "no-prefix"
    pfx = Path(compat_data) / "pfx"
    reg = pfx / "user.reg"
    if not reg.is_file():
        return "prefix-not-created"
    busy = not wait_for_server(pfx, wait)
    set_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open")
    removed = remove_value(reg, EOS_KEY, "OverlayPath")
    state = "browser-set" if has_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open") else "browser-not-written"
    if removed:
        state += " old-overlay-removed"
    return state + (" server-running" if busy else "")


def create_prefix(compat_data, proton, timeout=120):
    if not compat_data or not proton or not Path(proton).is_file():
        return "no-proton"
    reg = Path(compat_data) / "pfx/user.reg"
    if reg.is_file():
        return "present"
    try:
        Path(compat_data).mkdir(parents=True, exist_ok=True)
        subprocess.run([proton, "run", "cmd.exe", "/d", "/c", "exit"], stdin=subprocess.DEVNULL,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
    except subprocess.TimeoutExpired:
        return "timeout"
    except OSError:
        return "failed"
    return "created" if reg.is_file() else "failed"


CLOUD_TIMEOUT = 15.0
CLOUD_STORES = ("gog", "epic")
STORE_GAME = 10
RAN = 11
RAN_FAILED = 12
BATTLENET = "/usr/local/bin/droiddeck-battlenet"
BATTLENET_EXE = re.compile(r"(?i)/compatdata/2596113170/pfx/drive_c/Program Files \(x86\)/Battle\.net/Battle\.net\.exe$")
BATTLENET_LAUNCH = re.compile(r'(?i)^--exec=\s*"?launch\s+([A-Za-z0-9_]{1,16})"?$')


def battlenet(argv, env=os.environ):
    if env.get("BL_BATTLENET"):
        return None
    exe, code = False, None
    for raw in argv:
        arg = raw.strip().strip('"').strip("'")
        if BATTLENET_EXE.search(arg.replace("\\", "/")):
            exe = True
        found = BATTLENET_LAUNCH.match(arg)
        if found:
            code = found.group(1)
    if not exe:
        return None
    return [BATTLENET, "launch", code] if code else [BATTLENET, "open"]


def cloud_on(sidecar):
    return str(sidecar.get("store")) in CLOUD_STORES and sidecar.get("cloud", True) is not False


def exited(argv, code=0):
    found = find_store_game(argv)
    launch_dir = os.environ.get("BL_LAUNCH_DIR", "")
    if found is not None and str(found[1].get("store")) == "epic" and os.environ.get(EPIC_HANDLED) != "1" \
            and os.access(EPIC_CLIENT, os.X_OK):
        try:
            subprocess.run([EPIC_CLIENT, "exited", "--code", str(code)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, timeout=10)
        except (OSError, subprocess.TimeoutExpired):
            pass
    if found is None or not launch_dir or not cloud_on(found[1]):
        return 0
    store, game = str(found[1].get("store")), str(found[1].get("id"))
    ask(Path(launch_dir) / "stores", {"op": "cloud-up", "store": store, "id": game}, timeout=1.0)
    log("cloud %s %s up requested" % (store, game))
    return 0


COMET_DIR = Path("/usr/local/lib/droiddeck-comet")


def provision_galaxy(compat_data, proton, timeout=90):
    if not compat_data or not proton or not Path(proton).is_file():
        return "no-proton"
    data = Path(compat_data)
    data.mkdir(parents=True, exist_ok=True)
    marker = data / "pfx/.droiddeck-comet-service"
    try:
        version = (COMET_DIR / "version").read_text().strip()
        tools = [COMET_DIR / "GalaxyCommunication.exe", COMET_DIR / "update-permissions.exe"]
        if not all(p.is_file() for p in tools):
            return "not-installed"
        stamp = version + " " + " ".join(hashlib.sha256(p.read_bytes()).hexdigest() for p in tools)
        if marker.is_file() and marker.read_text() == stamp and (data / "pfx/system.reg").is_file():
            return "ready"
        with tempfile.TemporaryDirectory(prefix="droiddeck-galaxy-") as tmp:
            folder = Path(tmp)
            result = folder / "result"
            bat = folder / "install.bat"
            src = windows_path(str(COMET_DIR))
            answer = windows_path(str(result))
            target = r"C:\ProgramData\GOG.com\Galaxy\redists"
            lines = ["@echo off", "setlocal", 'set "DD_DIR=%s"' % target,
                     "sc query GalaxyCommunication >nul 2>&1",
                     'if not errorlevel 1 if not exist "%DD_DIR%\\droiddeck-comet" goto existing',
                     'if not exist "%DD_DIR%" mkdir "%DD_DIR%"',
                     'copy /y "%s\\GalaxyCommunication.exe" "%s\\GalaxyCommunication.exe" >nul' % (src, target),
                     'if errorlevel 1 exit /b 1',
                     'sc query GalaxyCommunication >nul 2>&1',
                     'if errorlevel 1 sc create GalaxyCommunication binpath= "%s\\GalaxyCommunication.exe"' % target,
                     'if errorlevel 1 exit /b 1',
                     '"%s\\update-permissions.exe"' % src,
                     'if errorlevel 1 exit /b 1',
                     'sc query GalaxyCommunication >nul 2>&1',
                     'if errorlevel 1 exit /b 1',
                     'echo managed>"%s\\droiddeck-comet"' % target,
                     ':existing', 'echo ready>"%s"' % answer, 'exit /b 0']
            bat.write_bytes(("\r\n".join(lines) + "\r\n").encode())
            env = os.environ.copy()
            env["BL_COMET_BOOTSTRAP"] = "1"
            run = subprocess.run([proton, "run", "cmd.exe", "/d", "/c", windows_path(str(bat))],
                                 env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
            if run.returncode or not result.is_file() or result.read_text().strip() != "ready":
                return "service-unavailable"
        marker.write_text(stamp)
        return "ready"
    except subprocess.TimeoutExpired:
        return "timeout"
    except (OSError, ValueError):
        return "service-unavailable"


def prepare_gog(sidecar, compat_data, launch_dir):
    if not launch_dir:
        log("gog achievements unavailable: no-channel")
        return False
    answer = ask(Path(launch_dir) / "stores", {"op": "gog-achievements", "store": "gog", "id": str(sidecar.get("id"))}, timeout=25.0)
    if not isinstance(answer, dict) or not answer.get("ready"):
        log("gog achievements: " + (str(answer.get("reason", "unavailable")) if isinstance(answer, dict) else "timeout"))
        return False
    state = provision_galaxy(compat_data, os.environ.get("BL_STORE_PROTON", ""))
    log("gog achievements prefix: " + state)
    return state == "ready"


GOG_SCRIPTS = "goggame-*.script"
GOG_REGISTRY_MARK = ".droiddeck-gog-registry"
GOG_ROOTS = {"HKEY_LOCAL_MACHINE": "HKEY_LOCAL_MACHINE", "HKLM": "HKEY_LOCAL_MACHINE",
             "HKEY_CURRENT_USER": "HKEY_CURRENT_USER", "HKCU": "HKEY_CURRENT_USER",
             "HKEY_CLASSES_ROOT": "HKEY_CLASSES_ROOT", "HKCR": "HKEY_CLASSES_ROOT"}
WINE_USER = "C:\\users\\steamuser"

# GOG completes some installs by running a support installer from the game folder (the
# goggame-<id>.script "Execute" action). Under ARM64 FEX that 32-bit self-extractor stops at
# "Path not found", so the values it would write are applied here instead. Only products GOG
# ships no setRegistry action for are listed; a script's own entries always win.
# The Witcher: Enhanced Edition (#694) exits during startup unless the installer's
# HKLM\Software\CD Projekt Red\The Witcher keys exist - the same ones The Witcher's own
# setup writes (InstallFolder, RegionVersion, IsDjinniInstalled) plus its language.
GOG_INSTALL_REGISTRY = {
    "1207658924": [
        ("HKLM", "Software\\CD Projekt Red\\The Witcher", "InstallFolder", "string", "{app}\\"),
        ("HKLM", "Software\\CD Projekt Red\\The Witcher", "RegionVersion", "string", "US"),
        ("HKLM", "Software\\CD Projekt Red\\The Witcher", "IsDjinniInstalled", "dword", 1),
        ("HKLM", "Software\\CD Projekt Red\\The Witcher", "Language", "string", "{language}"),
        ("HKCU", "Software\\CD Projekt RED\\Witcher\\Settings", "TextLanguage", "string", "{language}"),
        ("HKCU", "Software\\CD Projekt RED\\Witcher\\Settings", "VoiceLanguage", "string", "{language}"),
    ],
}
# The Witcher's text and voice language IDs, from its installer's registry data.
GOG_LANGUAGE_IDS = {"en": 3, "fr": 11, "de": 10, "es": 12, "it": 13, "ru": 14, "cs": 15, "hu": 16,
                    "zh": 21, "pl": 5}


def gog_paths(folder, product):
    app = windows_path(str(folder))
    support = app + "\\support\\" + product
    return {"app": app, "productid": product, "support": support, "supportdir": support,
            "localappdata": WINE_USER + "\\AppData\\Local", "userdocs": WINE_USER + "\\Documents"}


def gog_expand(text, paths):
    return re.sub(r"\{([A-Za-z]+)\}", lambda m: paths[m.group(1).lower()], text)


def gog_number(data):
    if isinstance(data, bool):
        raise ValueError
    if isinstance(data, int):
        return data
    text = str(data).strip()
    try:
        return int(text, 0)
    except ValueError:
        return int(text, 10)


def gog_value(kind, data):
    kind = str(kind).lower()
    if kind == "string":
        text = str(data)
        if any(c in text for c in "\r\n\0"):
            return None
        return '"%s"' % reg_escape(text)
    if kind == "dword":
        return "dword:%08x" % (gog_number(data) & 0xFFFFFFFF)
    if kind == "binary":
        return "hex:" + ",".join("%02x" % b for b in base64.b64decode(str(data), validate=True))
    return None


def gog_entry(arguments, paths):
    if not isinstance(arguments, dict):
        return None
    try:
        root = GOG_ROOTS.get(str(arguments.get("root") or "").upper())
        subkey = gog_expand(str(arguments.get("subkey") or ""), paths).strip("\\")
        if root is None or not subkey or any(c in subkey for c in "\r\n\0"):
            return None
        section = "[%s\\%s]" % (root, subkey)
        if not arguments.get("valueType"):
            return section
        data = arguments.get("valueData")
        value = gog_value(arguments["valueType"], gog_expand(data, paths) if isinstance(data, str) else data)
        name = gog_expand(str(arguments.get("valueName") or ""), paths)
        if value is None or any(c in name for c in "\r\n\0"):
            return None
        return section + "\r\n" + ('"%s"' % reg_escape(name) if name else "@") + "=" + value
    except (KeyError, ValueError, TypeError, binascii.Error):
        return None


def gog_installed_products(folder):
    products = {}
    for info in sorted(Path(folder).glob("goggame-*.info")):
        product = info.name[len("goggame-"):-len(".info")]
        if not product.isdigit():
            continue
        try:
            products[product] = json.loads(info.read_text(encoding="utf-8-sig"))
        except (OSError, ValueError):
            products[product] = {}
    return products


def gog_language_id(info):
    languages = info.get("languages") if isinstance(info, dict) else None
    if isinstance(languages, str):
        languages = [languages]
    code = str(languages[0]).lower()[:2] if isinstance(languages, list) and languages else ""
    return GOG_LANGUAGE_IDS.get(code, GOG_LANGUAGE_IDS["en"])


GOG_VALUE_HIVES = {"hklm": "local_machine", "hkey_local_machine": "local_machine",
                   "hkcu": "current_user", "hkey_current_user": "current_user",
                   "hkcr": "classes_root", "hkey_classes_root": "classes_root"}


def gog_value_key(root, section, name, view32=True):
    """A registry value's identity, keeping the root and the 32-bit view apart.

    The fallback import always targets the 32-bit view (`/reg:32`), which Wine keeps under
    `Software\\Wow6432Node` for HKLM: a native HKLM value must not pass for the 32-bit one.
    HKCU\\Software is shared by both views, so its values read the same either way.
    """
    text = section.replace("\\\\", "\\").strip("\\").lower()
    hive = GOG_VALUE_HIVES.get(root.lower(), root.lower())
    if text.startswith("software\\wow6432node\\"):
        text = text[len("software\\wow6432node\\"):]
    elif hive == "local_machine" and not view32 and text.startswith("software\\"):
        text = "native\\" + text
    elif text.startswith("software\\"):
        text = text[len("software\\"):]
    return "%s|%s|%s" % (hive, text, name.lower())


def gog_registry_values(pfx):
    """Values already in the prefix, so install defaults never overwrite a player's choice."""
    values = set()
    for file, root in (("system.reg", "hklm"), ("user.reg", "hkcu")):
        try:
            lines = (pfx / file).read_text(encoding="utf-8", errors="ignore").splitlines()
        except OSError:
            continue
        section = ""
        for line in lines:
            if line.startswith("["):
                end = line.find("]")
                section = line[1:end] if end > 0 else line[1:]
                continue
            match = re.match(r'^"([^"]+)"=', line)
            if match and section:
                values.add(gog_value_key(root, section, match.group(1), view32=False))
    return values


def gog_scripted_products(folder):
    products = set()
    for script in sorted(Path(folder).glob(GOG_SCRIPTS)):
        try:
            actions = json.loads(script.read_text(encoding="utf-8-sig")).get("actions")
        except (OSError, ValueError, AttributeError):
            continue
        if isinstance(actions, list) and any(
                isinstance(action, dict) and isinstance(action.get("install"), dict)
                and action["install"].get("action") == "setRegistry" for action in actions):
            products.add(script.name[len("goggame-"):-len(".script")])
    return products


def gog_expected_values(folder):
    """The fallback values a launch must find, so the marker never hides one that was deleted."""
    scripted = gog_scripted_products(folder)
    return [gog_value_key(root, subkey, name) for product in gog_installed_products(folder)
            if product in GOG_INSTALL_REGISTRY and product not in scripted
            for root, subkey, name, _kind, _value in GOG_INSTALL_REGISTRY[product]]


def gog_registry(folder, existing=()):
    entries, skipped, scripted = [], 0, set()
    for script in sorted(Path(folder).glob(GOG_SCRIPTS)):
        product = script.name[len("goggame-"):-len(".script")]
        try:
            actions = json.loads(script.read_text(encoding="utf-8-sig")).get("actions")
        except (OSError, ValueError, AttributeError):
            continue
        paths = gog_paths(folder, product)
        for action in actions if isinstance(actions, list) else []:
            install = action.get("install") if isinstance(action, dict) else None
            if not isinstance(install, dict) or install.get("action") != "setRegistry":
                continue
            entry = gog_entry(install.get("arguments"), paths)
            if entry is None:
                skipped += 1
            else:
                entries.append(entry)
                scripted.add(product)
    for product, info in gog_installed_products(folder).items():
        if product in scripted or product not in GOG_INSTALL_REGISTRY:
            continue
        paths = dict(gog_paths(folder, product), language=str(gog_language_id(info)))
        for root, subkey, name, kind, value in GOG_INSTALL_REGISTRY[product]:
            if gog_value_key(root, subkey, name) in existing:
                continue
            entry = gog_entry({"root": root, "subkey": subkey, "valueName": name,
                               "valueType": kind, "valueData": value}, paths)
            if entry is None:
                skipped += 1
            else:
                entries.append(entry)
    return entries, skipped


def apply_gog_registry(folder, compat_data, proton, timeout=90, installation=None):
    pfx = Path(compat_data) / "pfx" if compat_data else None
    existing = gog_registry_values(pfx) if pfx else set()
    entries, skipped = gog_registry(folder, existing)
    expected = gog_expected_values(folder)
    if installation:
        product = str(installation["id"])
        if not product.isdigit():
            raise ValueError("Invalid GOG product identifier")
        values = {"PATH": windows_path(str(folder))}
        identity = Path(folder) / ("goggame-%s.id" % product)
        if identity.is_file():
            actual = json.loads(identity.read_text(encoding="utf-8-sig")).get("buildId")
            if actual and installation.get("build") and str(actual) != str(installation["build"]):
                raise ValueError("GOG build metadata changed before patch setup")
        if installation.get("build"):
            values["BUILDID"] = installation["build"]
        if installation.get("version"):
            values["VER"] = installation["version"]
        for key, value in values.items():
            entry = gog_entry({"root": "HKLM", "subkey": "Software\\GOG.com\\Games\\" + product,
                               "valueName": key, "valueData": value, "valueType": "string"}, {})
            if entry:
                entries.append(entry)
    note = " skipped=%d" % skipped if skipped else ""
    if not entries:
        return "none" + note
    if not compat_data or not proton or not Path(proton).is_file():
        return "no-proton" + note
    text = "Windows Registry Editor Version 5.00\r\n\r\n" + "".join(entry + "\r\n\r\n" for entry in entries)
    stamp = hashlib.sha256(text.encode("utf-8")).hexdigest()
    marker = pfx / GOG_REGISTRY_MARK
    try:
        if (marker.is_file() and marker.read_text() == stamp and (pfx / "system.reg").is_file()
                and set(expected) <= existing):
            return "ready" + note
        Path(compat_data).mkdir(parents=True, exist_ok=True)
        with tempfile.TemporaryDirectory(prefix="droiddeck-gog-") as tmp:
            source = Path(tmp) / "registry.reg"
            source.write_bytes(b"\xff\xfe" + text.encode("utf-16-le"))
            executable = pfx / "drive_c/windows/system32/reg.exe"
            run = subprocess.run([proton, "run", str(executable) if executable.is_file() else "reg.exe", "import", windows_path(str(source)), "/reg:32"],
                                 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
        if run.returncode or not (pfx / "system.reg").is_file():
            return "import-failed" + note
        marker.write_text(stamp)
        return "written=%d%s" % (len(entries), note)
    except subprocess.TimeoutExpired:
        return "timeout" + note
    except OSError:
        return "import-failed" + note


def main(argv):
    if os.environ.get("DROIDDECK_GOG_MAINTENANCE") == "1":
        return 0
    if argv and argv[0] == "--exited":
        rest, code = argv[1:], 0
        if rest[:1] == ["--code"] and len(rest) > 1:
            try:
                code = int(rest[1])
            except ValueError:
                pass
            rest = rest[2:]
        return exited(rest, code)
    command = battlenet(argv)
    if command is not None:
        log("battlenet " + " ".join(command[1:]))
        return RAN if subprocess.call(command) == 0 else RAN_FAILED
    found = find_store_game(argv)
    if found is None:
        return 0
    folder, sidecar = found
    store, game = str(sidecar.get("store")), str(sidecar.get("id"))
    if store == "gog":
        helper = Path(__file__).resolve().with_name("droiddeck-gog-maintenance")
        if helper.is_file():
            child = subprocess.Popen([sys.executable, str(helper), "lease", str(folder), str(os.getppid())],
                                     stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
                                     start_new_session=True)
            ready = child.stdout.readline().decode().strip()
            child.stdout.close()
            threading.Thread(target=child.wait, daemon=True).start()
            if ready != "ready":
                log("gog: close or recover the maintenance operation before playing")
                return RAN_FAILED
    compat_data = os.environ.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
    prefix_id = os.path.basename(compat_data) if compat_data else ""
    launch_dir = os.environ.get("BL_LAUNCH_DIR", "")
    if launch_dir and cloud_on(sidecar):
        made = create_prefix(compat_data, os.environ.get("BL_STORE_PROTON", ""))
        if made != "present":
            log("cloud %s %s prefix=%s" % (store, game, made))
    if store == "gog":
        prepare_gog(sidecar, compat_data, launch_dir)
        log("gog registry: " + apply_gog_registry(folder, compat_data, os.environ.get("BL_STORE_PROTON", "")))
    if store == "epic":
        handled = os.environ.get(EPIC_HANDLED) == "1" and (folder / EPIC_ARGS).is_file()
        if not handled:
            drop_stale(folder)
            try:
                (folder / EPIC_ARGS).unlink()
            except OSError:
                pass
        choice = options(sidecar)
        client = None
        if handled:
            client = "ok"
        elif choice["eos"] and not choice["offline"] and client_wanted():
            client = through_client(folder, sidecar)
            log("epic launch id=%s client=%s" % (game, client))
        if client == "ok":
            try:
                (folder / EPIC_CODE).unlink()
            except OSError:
                pass
            log("epic launch id=%s through=client" % game)
        elif not (choice["eos"] and not choice["offline"]):
            try:
                (folder / EPIC_CODE).unlink()
            except OSError:
                pass
            log("epic launch id=%s code=no reason=%s" % (game, "offline" if choice["offline"] else "eos-off"))
        elif not launch_dir:
            log("epic launch id=%s code=no reason=no-channel" % game)
        else:
            answer = ask(Path(launch_dir) / "stores", {"op": "epic-code", "store": store, "id": game, "prefix": prefix_id})
            if answer is None:
                log("epic launch id=%s code=no reason=timeout" % game)
            else:
                got = bool(answer.get("code"))
                log("epic launch id=%s code=%s reason=%s" % (game, "yes" if got else "no", answer.get("reason", "")))
        log("epic prefix=%s %s" % (prefix_id or "?", provision_prefix(compat_data)))
    if not cloud_on(sidecar):
        if store in CLOUD_STORES:
            log("cloud %s %s down result=skipped reason=off" % (store, game))
        return 0
    if launch_dir:
        answer = ask(Path(launch_dir) / "stores", {"op": "cloud-down", "store": store, "id": game}, timeout=CLOUD_TIMEOUT)
        if answer is None:
            log("cloud %s %s down result=timeout" % (store, game))
        else:
            log("cloud %s %s down result=%s files=%s reason=%s" % (store, game, answer.get("result", "?"), answer.get("files", 0), answer.get("reason", "")))
    else:
        log("cloud %s %s down result=skipped reason=no-channel" % (store, game))
    return STORE_GAME


if __name__ == "__main__":
    try:
        sys.exit(main(sys.argv[1:]))
    except Exception as e:
        log("skipped: %s" % type(e).__name__)
        sys.exit(0)
