#!/usr/bin/env python3
"""okama-run — strict game launcher for OkamaOS.

Only one game may run at a time. Enforces the lock file, validates the
manifest, stops non-essential services, launches the game fullscreen,
waits for exit, recovers from crashes, and returns to okama-shell.

Usage:
  okama-run <game_id>
  okama-run <game_id> --no-suspend
"""

import sys
import os

_lib = os.environ.get("OKAMA_LIB", "")
if not _lib:
    _lib = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../lib")
OKAMA_LIB_PATH = os.path.abspath(_lib)
sys.path.insert(0, OKAMA_LIB_PATH)

import argparse
import json
import subprocess
import signal
import time
import traceback
import shutil

import okamaos.config as cfg_mod
import okamaos.games as games_mod
import okamaos.manifest as manifest_mod

def _fallback_base(kind: str) -> str:
    if kind == "run":
        base = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
    else:
        base = os.environ.get("XDG_STATE_HOME")
        if not base and os.environ.get("HOME"):
            base = os.path.join(os.environ["HOME"], ".local", "state")
        if not base:
            base = "/tmp"
    return os.path.join(base, "okamaos")


def _writable_dir(path: str) -> bool:
    try:
        os.makedirs(path, exist_ok=True)
    except OSError:
        return False
    return os.access(path, os.W_OK)


def _runtime_file(env_name: str, default: str, fallback_name: str) -> str:
    override = os.environ.get(env_name)
    if override:
        return override
    parent = os.path.dirname(default)
    if _writable_dir(parent):
        return default
    fallback_dir = os.path.join(_fallback_base("run"), "run")
    os.makedirs(fallback_dir, exist_ok=True)
    return os.path.join(fallback_dir, fallback_name)


def _runtime_dir(env_name: str, default: str, fallback_name: str) -> str:
    override = os.environ.get(env_name)
    if override:
        os.makedirs(override, exist_ok=True)
        return override
    if _writable_dir(default):
        return default
    fallback_dir = os.path.join(_fallback_base("state"), fallback_name)
    os.makedirs(fallback_dir, exist_ok=True)
    return fallback_dir


LOCK_FILE = _runtime_file("OKAMA_LOCK_FILE", "/var/run/okama-game.lock", "okama-game.lock")
LOG_DIR = _runtime_dir("OKAMA_LOG_DIR", "/var/okamaos/logs", "logs")
SAVES_DIR = _runtime_dir("OKAMA_SAVES", "/var/okamaos/saves", "saves")

SUSPEND_SERVICES = [
    "S40okama-network",
]


def _path_list(value: str) -> list:
    paths = []
    for part in value.split(os.pathsep):
        if not part:
            continue
        paths.append(part if os.path.isabs(part) else os.path.abspath(part))
    return paths


def _dedupe_paths(paths: list) -> list:
    result = []
    seen = set()
    for path in paths:
        if path and path not in seen:
            seen.add(path)
            result.append(path)
    return result


def _python_executable() -> str:
    for candidate in (sys.executable, shutil.which("python3"), shutil.which("python"), "/usr/bin/python3"):
        if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK):
            return os.path.abspath(candidate)
    return sys.executable or "python3"


def _pygame_lib_dirs() -> list:
    version = f"python{sys.version_info.major}.{sys.version_info.minor}"
    candidates = [
        os.environ.get("OKAMA_PYGAME_LIBS", ""),
        os.path.join(sys.prefix, "lib", version, "site-packages", "pygame.libs"),
        os.path.join("/usr/lib", version, "site-packages", "pygame.libs"),
    ]
    return [path for path in candidates if path and os.path.isdir(path)]


def acquire_lock(game_id: str) -> None:
    if os.path.exists(LOCK_FILE):
        with open(LOCK_FILE) as f:
            current = f.read().strip()
        print(f"ERROR: Game already running: {current}", file=sys.stderr)
        sys.exit(1)
    with open(LOCK_FILE, "w") as f:
        f.write(game_id)


def release_lock() -> None:
    try:
        os.remove(LOCK_FILE)
    except FileNotFoundError:
        pass


def suspend_services() -> None:
    for svc in SUSPEND_SERVICES:
        path = f"/etc/init.d/{svc}"
        if os.path.exists(path):
            subprocess.call([path, "stop"],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def resume_services() -> None:
    for svc in reversed(SUSPEND_SERVICES):
        path = f"/etc/init.d/{svc}"
        if os.path.exists(path):
            subprocess.call([path, "start"],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def build_launch_cmd(manifest: dict, game_dir: str) -> list:
    runtime = manifest["runtime"]
    entry = os.path.join(game_dir, manifest["entry"])

    # Check if game has a venv directory
    venv_python = os.path.join(game_dir, "venv", "bin", "python3")
    if os.path.isfile(venv_python):
        return [venv_python, entry]

    if runtime in ("okama-lite", "okama-python", "okama-sdl2"):
        return [_python_executable(), entry]
    else:
        raise ValueError(f"Unknown runtime: {runtime}")


def save_crash_log(game_id: str, returncode: int, stderr_out: str) -> str:
    os.makedirs(LOG_DIR, exist_ok=True)
    ts = int(time.time())
    path = os.path.join(LOG_DIR, f"crash_{game_id}_{ts}.log")
    with open(path, "w") as f:
        f.write(f"game_id    : {game_id}\n")
        f.write(f"returncode : {returncode}\n")
        f.write(f"timestamp  : {ts}\n\n")
        f.write(stderr_out or "")
    return path


def main():
    p = argparse.ArgumentParser(prog="okama-run")
    p.add_argument("game_id")
    p.add_argument("--no-suspend", action="store_true")
    args = p.parse_args()

    conf = cfg_mod.get()
    dev = conf.is_dev_mode()

    game_id = args.game_id
    game_dir = games_mod.game_dir(game_id)
    manifest_path = os.path.join(game_dir, "manifest.ok.json")

    if not os.path.isdir(game_dir):
        print(f"Game not installed: {game_id}", file=sys.stderr)
        sys.exit(1)

    try:
        manifest = manifest_mod.load_and_validate(manifest_path, dev_mode=dev)
    except manifest_mod.ManifestError as e:
        print(f"Manifest error: {e}", file=sys.stderr)
        sys.exit(1)

    acquire_lock(game_id)

    if not args.no_suspend:
        suspend_services()

    env = os.environ.copy()
    env["OKAMA_GAME_ID"] = game_id
    env["OKAMA_GAME_DIR"] = game_dir
    env.setdefault("OKAMA_SAVES", SAVES_DIR)

    assets_json = os.path.join(SAVES_DIR, "..", "wallet", "assets.json")
    env["OKAMA_ASSETS_PATH"] = os.path.normpath(assets_json)
    try:
        import okamaos.wallet as _w
        env["OKAMA_WALLET_ADDRESS"] = _w.address()
    except Exception:
        pass

    # Self-contained package support: prepend bundled deps so games run
    # without any system-wide pip installs.
    python_path_parts = [OKAMA_LIB_PATH]
    bundled_sp = os.path.join(game_dir, "site-packages")
    if os.path.isdir(bundled_sp):
        python_path_parts.insert(0, bundled_sp)
    python_path_parts.extend(_path_list(env.get("PYTHONPATH", "")))
    env["PYTHONPATH"] = os.pathsep.join(_dedupe_paths(python_path_parts))

    bundled_lib = os.path.join(game_dir, "lib")
    ld_paths = _pygame_lib_dirs() + _path_list(env.get("LD_LIBRARY_PATH", ""))
    if os.path.isdir(bundled_lib):
        ld_paths.insert(0, bundled_lib)
    if ld_paths:
        env["LD_LIBRARY_PATH"] = os.pathsep.join(_dedupe_paths(ld_paths))
    # SDL2's bundled pygame wheel often lacks kmsdrm. Prefer host windows when
    # available, otherwise use offscreen and let okamaos.display copy frames to
    # the framebuffer.
    fb_dev = conf.get("FRAMEBUFFER_DEVICE", "/dev/fb0")
    env["OKAMA_FRAMEBUFFER_DEVICE"] = fb_dev
    if env.get("OKAMA_SDL_VIDEODRIVER"):
        env["SDL_VIDEODRIVER"] = env["OKAMA_SDL_VIDEODRIVER"]
    elif env.get("SDL_VIDEODRIVER"):
        pass
    elif env.get("DISPLAY"):
        env["SDL_VIDEODRIVER"] = "x11"
    elif env.get("WAYLAND_DISPLAY"):
        env["SDL_VIDEODRIVER"] = "wayland"
    elif os.path.exists(fb_dev):
        env["SDL_VIDEODRIVER"] = "offscreen"
        env["SDL_FBDEV"] = fb_dev
    elif os.path.exists("/dev/dri/card0") or os.path.exists("/dev/dri/card1"):
        env["SDL_VIDEODRIVER"] = "kmsdrm"
    env["SDL_AUDIODRIVER"] = "alsa"

    # Performance hardening for games
    # Lock to single CPU to reduce scheduler jitter, enable performance governor
    env["SDL_RENDER_DRIVER"] = "software"
    env["SDL_HIGHDPI_DISABLED"] = "1"
    env["SDL_VIDEO_X11_NODIRECTCOLOR"] = "1"
    env["PYTHONDONTWRITEBYTECODE"] = "1"
    # Reduce input latency hints
    env["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"

    try:
        cmd = build_launch_cmd(manifest, game_dir)
    except ValueError as e:
        print(f"ERROR: {e}", file=sys.stderr)
        release_lock()
        sys.exit(1)

    print(f"Launching: {manifest['name']} ({game_id})")
    log_path = os.path.join(LOG_DIR, f"{game_id}.log")
    os.makedirs(LOG_DIR, exist_ok=True)

    # Set CPU governor to performance if available
    try:
        with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "w") as f:
            f.write("performance\n")
    except Exception:
        pass

    proc = None
    exit_code = 0
    stderr_out = ""
    try:
        with open(log_path, "w") as log_f:
            proc = subprocess.Popen(
                cmd,
                cwd=game_dir,
                env=env,
                stdout=log_f,
                stderr=subprocess.PIPE,
            )
            _, stderr_bytes = proc.communicate()
            exit_code = proc.returncode
            stderr_out = (stderr_bytes or b"").decode(errors="replace")
    except Exception:
        stderr_out = traceback.format_exc()
        exit_code = -1

    if not args.no_suspend:
        resume_services()

    release_lock()

    if exit_code != 0:
        crash_log = save_crash_log(game_id, exit_code, stderr_out)
        print(f"Game exited with code {exit_code}. Crash log: {crash_log}")
        print("Returning to OkamaOS shell…")
    else:
        print(f"Game finished cleanly. Returning to OkamaOS shell…")
        _submit_game_rewards(game_id)


def _submit_game_rewards(game_id: str) -> None:
    """Submit any pending play-to-earn reward claims after game exit."""
    try:
        import okamaos.rewards as rewards_mod
        results = rewards_mod.process_game_rewards(
            game_id, SAVES_DIR,
            passphrase_cb=None,
        )
        if results:
            print(f"Reward: submitted {len(results)} claim(s) for {game_id}.")
    except Exception:
        pass


if __name__ == "__main__":
    main()
