#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Einmal-Installation von der Website: Release herunterladen,
nach ~/.local/share/windows-in-linux/ entpacken, Menüeintrag anlegen.

Standard: öffentliche Metadaten unter /release-info.php.
Fallback ohne release-info.php: Manifest ``api/version.php`` (ebenfalls ohne Key).

Ist die lokale Version **neuer** als der Server, wird abgebrochen (kein Downgrade).

Standard: **gleiche** Server-Version wird trotzdem neu heruntergeladen und die App-Dateien
überschrieben (wie früher mit --force). Ohne erneuten Download nur bei gleicher Version:
``--skip-if-same`` oder ``WIL_INSTALL_SKIP_IF_SAME=1``.

Aufruf:

  curl -fsSL https://windows-in-linux.reffinet.com/install_from_web.py -o /tmp/wil-install.py
  python3 /tmp/wil-install.py

Weitere Optionen:
  python3 install_from_web.py --skip-if-same
  PUBLIC_RELEASE_URL=https://…/release-info.php python3 install_from_web.py
  MANIFEST_URL=https://…/api/version.php python3 install_from_web.py
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.error
import urllib.request
from pathlib import Path

DEFAULT_MANIFEST = "https://windows-in-linux.reffinet.com/api/version.php"
DEFAULT_PUBLIC_RELEASE = "https://windows-in-linux.reffinet.com/release-info.php"

BUNDLE_REQUIRED = (
    "wine_einfach_installer.py",
    "updater.py",
    "VERSION",
    "run.sh",
    "windows-in-linux.desktop.in",
)
# Ältere Release-Tarballs (z. B. v1.1.4) enthalten install.sh noch nicht.
BUNDLE_OPTIONAL = ("install.sh",)


def xdg_data_home() -> Path:
    return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share"))


def install_root() -> Path:
    return xdg_data_home() / "windows-in-linux"


def apps_dir() -> Path:
    return xdg_data_home() / "applications"


def fetch_public_release_info(url: str) -> dict:
    req = urllib.request.Request(url, headers={"User-Agent": "windows-in-linux-installer/1.0"}, method="GET")
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def fetch_manifest(url: str) -> dict:
    req = urllib.request.Request(
        url,
        headers={"User-Agent": "windows-in-linux-installer/1.0"},
        method="GET",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for block in iter(lambda: f.read(1 << 20), b""):
            h.update(block)
    return h.hexdigest()


def find_bundle_root(extracted: Path) -> Path:
    if (extracted / "wine_einfach_installer.py").is_file():
        return extracted
    dirs = [p for p in extracted.iterdir() if p.is_dir()]
    if len(dirs) == 1 and (dirs[0] / "wine_einfach_installer.py").is_file():
        return dirs[0]
    raise RuntimeError("Archiv enthält nicht die erwarteten Dateien.")


def extract_bundle(archive: Path, dest: Path) -> None:
    dest.mkdir(parents=True, exist_ok=True)
    tmp = Path(tempfile.mkdtemp(prefix="wil-extract-"))
    try:
        with tarfile.open(archive, "r:gz") as tf:
            tf.extractall(tmp)
        root = find_bundle_root(tmp)
        for name in BUNDLE_REQUIRED:
            src = root / name
            if not src.is_file():
                raise RuntimeError(f"Im Archiv fehlt: {name}")
            shutil.copy2(src, dest / name)
        for name in BUNDLE_OPTIONAL:
            src = root / name
            if src.is_file():
                shutil.copy2(src, dest / name)
        (dest / "run.sh").chmod(0o755)
        ins = dest / "install.sh"
        if ins.is_file():
            ins.chmod(0o755)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def write_desktop(dest: Path) -> None:
    tpl = dest / "windows-in-linux.desktop.in"
    if not tpl.is_file():
        raise RuntimeError("windows-in-linux.desktop.in fehlt in der Installation.")
    text = tpl.read_text(encoding="utf-8").replace("@INSTALL_DIR@", str(dest))
    ad = apps_dir()
    ad.mkdir(parents=True, exist_ok=True)
    (ad / "windows-in-linux.desktop").write_text(text, encoding="utf-8")
    try:
        subprocess.run(
            ["update-desktop-database", str(ad)],
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except OSError:
        pass


def read_local_version(dest: Path) -> str:
    vf = dest / "VERSION"
    if vf.is_file():
        return vf.read_text(encoding="utf-8").strip()
    return ""


def _version_tuple(ver: str) -> tuple[int, int, int, int]:
    v = str(ver).strip()
    if len(v) >= 1 and v[0] in "vV":
        v = v[1:].strip()
    parts: list[int] = []
    for seg in v.split(".")[:4]:
        n = 0
        for c in seg:
            if c.isdigit():
                n = n * 10 + (ord(c) - 48)
            else:
                break
        parts.append(n)
    while len(parts) < 4:
        parts.append(0)
    return (parts[0], parts[1], parts[2], parts[3])


def try_public_release(url: str) -> dict | None:
    if not (url or "").strip():
        return None
    try:
        d = fetch_public_release_info(url.strip())
        if str(d.get("version", "")).strip() and str(d.get("download_url", "")).strip():
            return d
    except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError, json.JSONDecodeError, ValueError):
        pass
    return None


def main() -> int:
    p = argparse.ArgumentParser(description="Windows in Linux – Installation von der Website")
    p.add_argument(
        "--manifest",
        default=os.environ.get("MANIFEST_URL", DEFAULT_MANIFEST),
        help="URL zu api/version.php (JSON, Fallback wenn release-info fehlt)",
    )
    p.add_argument(
        "--public-release",
        default=os.environ.get("PUBLIC_RELEASE_URL", DEFAULT_PUBLIC_RELEASE).strip(),
        help="URL zu release-info.php (JSON; Standard)",
    )
    p.add_argument(
        "--skip-if-same",
        action="store_true",
        help="Kein Download, wenn lokal und Server dieselbe Versionsnummer haben",
    )
    p.add_argument(
        "--force",
        action="store_true",
        help="(ohne Wirkung) Früher nötig; Neuinstallation bei gleicher Version ist jetzt Standard",
    )
    args = p.parse_args()

    dest = install_root()
    data: dict | None = None

    if args.public_release:
        data = try_public_release(args.public_release)

    if data is None:
        try:
            data = fetch_manifest(args.manifest)
        except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError, json.JSONDecodeError) as e:
            print(
                f"Abbruch: Weder öffentliche release-info erreichbar noch Manifest ({args.manifest}): {e}",
                file=sys.stderr,
            )
            return 1

    remote = str(data.get("version", "")).strip()
    url = str(data.get("download_url", "")).strip()
    expect_hash = str(data.get("sha256", "")).strip()

    if not remote or not url:
        print("Release-Metadaten ohne version oder download_url.", file=sys.stderr)
        return 1

    skip_if_same = args.skip_if_same or os.environ.get("WIL_INSTALL_SKIP_IF_SAME", "").strip().lower() in (
        "1",
        "true",
        "yes",
    )
    local = read_local_version(dest)
    if local.strip():
        rt = _version_tuple(remote)
        lt = _version_tuple(local)
        if rt < lt:
            print(f"Lokal v{local} ist neuer als Server v{remote} – Abbruch.", file=sys.stderr)
            return 1
        if skip_if_same and rt == lt:
            print(
                f"Bereits v{local} installiert – übersprungen (--skip-if-same / WIL_INSTALL_SKIP_IF_SAME).",
                file=sys.stderr,
            )
            return 0

    print(f"Lade Release v{remote} (lokal bisher {local or '—'}) …", file=sys.stderr)
    dl = Path(tempfile.mkstemp(prefix="wil-dl-", suffix=".tar.gz")[1])
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "windows-in-linux-installer/1.0"})
        with urllib.request.urlopen(req, timeout=180) as resp, dl.open("wb") as out:
            shutil.copyfileobj(resp, out)

        if expect_hash:
            got = sha256_file(dl)
            if got.lower() != expect_hash.lower():
                print(f"SHA256 ungültig (erwartet {expect_hash}, erhalten {got}).", file=sys.stderr)
                return 1

        extract_bundle(dl, dest)
        write_desktop(dest)
        dest.mkdir(parents=True, exist_ok=True)
    finally:
        dl.unlink(missing_ok=True)

    print("")
    print(f"Fertig. Installation: {dest}")
    print(f"Start: Menü „Windows in Linux“ oder: {dest}/run.sh")
    print("")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
