#!/usr/bin/env python3
"""Per-user Linux installation and automatic updates; Python standard library only."""
import fcntl
import hashlib
import io
import json
import os
from pathlib import Path
import platform
import subprocess
import sys
import tempfile
import urllib.parse
import urllib.request
import zipfile

CHANNEL = "__DOWNLOAD_ORIGIN__"


def fetch(url, limit):
    with urllib.request.urlopen(urllib.request.Request(url, headers={"Cache-Control": "no-cache"}), timeout=15) as response:
        data = response.read(limit + 1)
    if len(data) > limit:
        raise ValueError("Download exceeds size limit")
    return data


def atomic_write(path, data, mode=0o600):
    with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as out:
        temporary = Path(out.name)
        try:
            out.write(data)
            out.flush()
            os.fchmod(out.fileno(), mode)
            os.fsync(out.fileno())
            os.replace(temporary, path)
        finally:
            temporary.unlink(missing_ok=True)


def update(root, channel):
    manifest = json.loads(fetch(channel + "/latest.json", 65536))
    version = manifest["version"]
    binary = root / "game"
    if binary.exists() and (root / "version").exists():
        if (root / "version").read_text() == version:
            return
    artifact = manifest["linux_x86_64"]
    filename = artifact["file"]
    if not isinstance(filename, str) or Path(filename).name != filename or ":" in filename:
        raise ValueError("Invalid update filename")
    archive = fetch(channel + "/" + urllib.parse.quote(filename), 256 * 1024 * 1024)
    if hashlib.sha256(archive).hexdigest() != artifact["sha256"]:
        raise ValueError("Update checksum mismatch")
    with zipfile.ZipFile(io.BytesIO(archive)) as package:
        member = package.getinfo("Crowns and Ashes/crowns-and-ashes")
        if member.file_size > 256 * 1024 * 1024:
            raise ValueError("Game exceeds size limit")
        executable = package.read(member)
        launcher_info = package.getinfo("Crowns and Ashes/launcher.py")
        if launcher_info.file_size > 1024 * 1024:
            raise ValueError("Launcher exceeds size limit")
        launcher = package.read(launcher_info).decode().replace(
            '__DOWNLOAD_' + 'ORIGIN__', json.dumps(channel)[1:-1])
        compile(launcher, "launcher.py", "exec")
    if not executable.startswith(b"\x7fELF"):
        raise ValueError("Update is not a native Linux executable")
    atomic_write(binary, executable, 0o755)
    atomic_write(root / "launcher.py", launcher.encode(), 0o755)
    atomic_write(root / "version", version.encode())


def main():
    if platform.system() != "Linux" or platform.machine() not in ("x86_64", "AMD64"):
        raise SystemExit("Crowns and Ashes requires Linux x86_64.")
    channel = CHANNEL.rstrip("/")
    parsed = urllib.parse.urlsplit(channel)
    if parsed.scheme != "https" or not parsed.netloc or parsed.query or parsed.fragment:
        raise SystemExit("Download the installer from the Crowns and Ashes download page.")
    identity = hashlib.sha256(channel.encode()).hexdigest()[:12]
    data = Path(os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local/share")))
    root = data / "crowns-and-ashes" / identity
    root.mkdir(parents=True, exist_ok=True)
    with (root / "update.lock").open("w") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        try:
            update(root, channel)
        except Exception as error:
            print(f"Could not update Crowns and Ashes: {error}", file=sys.stderr)
            if not (root / "game").exists():
                raise SystemExit("Installation failed. Run the installer again when connected.")
            print("Starting the installed version.", file=sys.stderr)
        launcher = root / "launcher.py"
        if not launcher.exists():
            atomic_write(launcher, Path(__file__).read_bytes(), 0o755)
        applications = data / "applications"
        applications.mkdir(parents=True, exist_ok=True)
        # Desktop Exec uses double quotes, with these characters escaped.
        def quote(value):
            return '"' + str(value).replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`').replace('$', '\\$').replace('%', '%%') + '"'
        entry = ("[Desktop Entry]\nType=Application\n"
                 f"Name=Crowns and Ashes ({parsed.hostname})\n"
                 f"Exec={quote(sys.executable)} {quote(launcher)}\n"
                 "Terminal=true\nCategories=Game;StrategyGame;\n")
        atomic_write(applications / f"crowns-and-ashes-{identity}.desktop", entry.encode())
    return subprocess.call([str(root / "game"), *sys.argv[1:]])


if __name__ == "__main__":
    sys.exit(main())
