#!/usr/bin/python3 -I
"""Install the raven-image client on Yggdrasil from GameCult/raven-image main. Merging to main deploys.

Run as root by raven-image-deploy.timer. Reads the forge locally, so it needs no login and no DNS. When main moves,
it unpacks `client/` of that commit into /opt/commons/opt/raven-image.releases/<sha8> (root-owned, no group or other
write bit), points /opt/commons/opt/raven-image at it in one rename, and keeps the three newest releases. The client
in /opt/commons/bin/raven-image is a symlink through that path, so a release swap is atomic for callers.
Rollback: `ln -sfn raven-image.releases/<older sha8> /opt/commons/opt/raven-image.new && mv -T ... raven-image`.
"""
import io, json, os, shutil, sys, tarfile, urllib.request

FORGE = "http://127.0.0.1:3310/api/v1/repos/GameCult/raven-image"
ROOT = "/opt/commons/opt"
LINK = os.path.join(ROOT, "raven-image")
RELEASES = os.path.join(ROOT, "raven-image.releases")
BIN = "/opt/commons/bin/raven-image"
KEEP = 3


def get(url):
    with urllib.request.urlopen(url, timeout=60) as r:
        return r.read()


def main():
    sha = json.loads(get(f"{FORGE}/branches/main"))["commit"]["id"]
    if not (len(sha) == 40 and all(c in "0123456789abcdef" for c in sha)):
        sys.exit(f"install-raven-image: forge gave no plain sha: {sha!r}")
    short = sha[:8]
    target = os.path.join(RELEASES, short)
    if os.path.islink(LINK) and os.readlink(LINK) == os.path.join("raven-image.releases", short):
        print(f"install-raven-image: {short} already live")
        return
    os.makedirs(RELEASES, exist_ok=True)
    if not os.path.isdir(target):
        tmp = target + ".partial"
        shutil.rmtree(tmp, ignore_errors=True)
        os.makedirs(tmp)
        with tarfile.open(fileobj=io.BytesIO(get(f"{FORGE}/archive/{sha}.tar.gz")), mode="r:gz") as tf:
            for m in tf.getmembers():
                parts = m.name.split("/", 2)  # raven-image/client/<path>
                if len(parts) < 3 or parts[1] != "client" or not parts[2]:
                    continue
                if not (m.isfile() or m.isdir()) or ".." in parts[2].split("/"):
                    continue
                dest = os.path.join(tmp, parts[2])
                if m.isdir():
                    os.makedirs(dest, exist_ok=True)
                    continue
                os.makedirs(os.path.dirname(dest), exist_ok=True)
                with open(dest, "wb") as f:
                    f.write(tf.extractfile(m).read())
        if not os.path.isfile(os.path.join(tmp, "raven-image")):
            sys.exit("install-raven-image: commit has no client/raven-image; not installing")
        for d, dirs, files in os.walk(tmp):
            os.chmod(d, 0o755)
            for f in files:
                p = os.path.join(d, f)
                os.chmod(p, 0o755 if p == os.path.join(tmp, "raven-image") else 0o644)
        os.rename(tmp, target)
    if os.path.isdir(LINK) and not os.path.islink(LINK):  # the hand install before this deploy existed
        os.rename(LINK, os.path.join(RELEASES, "hand-installed"))
    new = LINK + ".new"
    if os.path.lexists(new):
        os.remove(new)
    os.symlink(os.path.join("raven-image.releases", short), new)
    os.replace(new, LINK)
    want = os.path.join(LINK, "raven-image")
    if not os.path.islink(BIN) or os.readlink(BIN) != want:
        tmpbin = BIN + ".new"
        if os.path.lexists(tmpbin):
            os.remove(tmpbin)
        os.symlink(want, tmpbin)
        os.replace(tmpbin, BIN)
    kept = sorted((e for e in os.scandir(RELEASES) if e.is_dir() and not e.name.endswith(".partial") and e.name != "hand-installed"),
                  key=lambda e: e.stat().st_mtime, reverse=True)
    for e in kept[KEEP:]:
        if e.name != short:
            shutil.rmtree(e.path)
    print(f"install-raven-image: {short} live")


if __name__ == "__main__":
    main()
