#!/usr/bin/env bash
# CraftamaValheim for Linux: installs / updates the Craftama Valheim modpack into a native (Linux) Valheim
# and starts the game. Same logic as the Windows CraftamaValheim.exe (craftama-valheim.ps1).
#
#   ./craftama-valheim.sh                 first run: install (and ask whether to start); later: update + start
#   ./craftama-valheim.sh --play          update (quietly) and start the game
#   ./craftama-valheim.sh --install       install / repair, then ask whether to start
#   ./craftama-valheim.sh [...] /path/to/Valheim     use this game folder (the one with valheim.x86_64)
# Options: --edition free|paid|auto (remember BepInEx choice), --base-url URL,
#          --yes (use suggested choice and start without asking), --no-launch (tests).
# Env: CRAFTAMA_STATE_DIR (default ~/.config/craftama), CRAFTAMA_STEAM_ROOTS (colon-separated Steam roots).
#
# On Linux BepInEx is loaded by the pack's start_game_bepinex.sh, so Steam must start the game through it:
# set once in Steam -> Valheim -> Properties -> Launch options:   ./start_game_bepinex.sh %command%
# The script checks Steam's localconfig.vdf and prints that line if it is not set.
# Needs bash + python3 (no jq, no unzip).
set -euo pipefail

MODE=""; BASE_URL="https://valheim.infra.craftama.eu/valheim/modpack/craftama"; GAME_DIR=""; YES=0; NO_LAUNCH=0; GAME_EDITION=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --play|/play) MODE=play ;;
    --install|/install) MODE=install ;;
    --base-url) BASE_URL="$2"; shift ;;
    --edition) GAME_EDITION="${2:?Chybí free, paid nebo auto za --edition}"; shift ;;
    --yes|-y) YES=1 ;;
    --no-launch) NO_LAUNCH=1 ;;
    -h|--help) sed -n '2,17p' "$0"; exit 0 ;;
    -*) echo "neznámý přepínač: $1" >&2; exit 2 ;;
    *) GAME_DIR="$1" ;;
  esac
  shift
done
case "$GAME_EDITION" in
  ""|auto|free|paid) ;;
  *) echo "--edition musí být free, paid nebo auto" >&2; exit 2 ;;
esac
INTERACTIVE=0
[[ -t 0 && $YES -eq 0 ]] && INTERACTIVE=1

STATE_DIR="${CRAFTAMA_STATE_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/craftama}"
STEAM_ROOTS="${CRAFTAMA_STEAM_ROOTS:-$HOME/.steam/steam:$HOME/.local/share/Steam:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam}"
mkdir -p "$STATE_DIR"
[[ -n "$MODE" ]] || { [[ -f "$STATE_DIR/valheim.json" ]] && MODE=play || MODE=install; }
command -v python3 >/dev/null || { echo "Chybí python3 (sudo apt install python3)." >&2; exit 1; }

# The python part finds the game, installs/updates, and prints three lines on stdout:
#   <game dir>  <ok|fail>  <steam|direct>  <launch-option: set|missing|unknown>
set +e
OUT="$(STATE_DIR="$STATE_DIR" STEAM_ROOTS="$STEAM_ROOTS" BASE_URL="$BASE_URL" GAME_DIR="$GAME_DIR" MODE="$MODE" GAME_EDITION="$GAME_EDITION" INTERACTIVE="$INTERACTIVE" python3 - <<'PY'
import datetime, json, os, re, shutil, sys, tempfile, urllib.request, zipfile
from pathlib import Path

STATE = Path(os.environ["STATE_DIR"]); BASE = os.environ["BASE_URL"].rstrip("/")
LOG = STATE / "valheim-update.log"; CONFIG = STATE / "valheim.json"
ROOTS = [Path(p) for p in os.environ["STEAM_ROOTS"].split(":") if p]
EXE = "valheim.x86_64"; APP = "892970"

def log(msg):
    line = f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {msg}"
    print(line, file=sys.stderr)
    with open(LOG, "a", encoding="utf-8") as f:
        f.write(line + "\n")

def read_json(p):
    try:
        return json.loads(Path(p).read_text(encoding="utf-8"))
    except Exception:
        return None

def vdf(text):
    """Minimal Valve KeyValues parser -> nested dicts."""
    toks = re.findall(r'"((?:[^"\\]|\\.)*)"|([{}])', text)
    def block(i):
        d = {}
        while i < len(toks):
            s, br = toks[i]
            if br == "}":
                return d, i + 1
            key = s.replace("\\\\", "\\"); i += 1
            s2, br2 = toks[i]
            if br2 == "{":
                d[key], i = block(i + 1)
            else:
                d[key] = s2.replace("\\\\", "\\"); i += 1
        return d, i
    return block(0)[0]

def libraries():
    libs = []
    for r in ROOTS:
        if not r.is_dir():
            continue
        libs.append(r.resolve())
        f = r / "steamapps" / "libraryfolders.vdf"
        if f.is_file():
            for m in re.finditer(r'"path"\s+"([^"]+)"', f.read_text(errors="replace")):
                libs.append(Path(m.group(1)))
    out = []
    for l in libs:
        if l not in out:
            out.append(l)
    return out

def is_game(d):
    return bool(d) and (Path(d) / EXE).is_file()

def find_game(arg):
    if arg:
        if is_game(arg):
            return Path(arg).resolve()
        raise SystemExit(f"Ve složce {arg} není {EXE}.")
    cfg = read_json(CONFIG)
    if cfg and is_game(cfg.get("game_dir")):
        return Path(cfg["game_dir"])
    for lib in libraries():
        d = lib / "steamapps" / "common" / "Valheim"
        if is_game(d):
            return d.resolve()
    raise SystemExit(f"Valheim nenalezen. Spusť skript s cestou ke složce hry (tam, kde je {EXE}).")

def under_steam(game):
    g = str(Path(game).resolve())
    return any(g.startswith(str(l.resolve()) + "/") for l in libraries() if l.exists())

def launch_option():
    found = None
    for r in ROOTS:
        for f in (r / "userdata").glob("*/config/localconfig.vdf") if (r / "userdata").is_dir() else []:
            def walk(d):
                for k, v in d.items():
                    if not isinstance(v, dict):
                        continue
                    if k == APP:
                        lo = next((x for kk, x in v.items() if kk.lower() == "launchoptions"), "")
                        if isinstance(lo, str):
                            yield lo
                    yield from walk(v)
            try:
                for lo in walk(vdf(f.read_text(errors="replace"))):
                    if "start_game_bepinex.sh" in lo:
                        return "set"
                    found = "missing"
            except Exception as e:
                log(f"cannot parse {f}: {e}")
    return found or "missing" if any((r / "userdata").is_dir() for r in ROOTS) else "unknown"

def download(url, out, timeout=300):
    last = None
    for i in range(1, 4):
        try:
            log(f"GET {url} (try {i})")
            req = urllib.request.Request(url, headers={"User-Agent": "CraftamaValheim"})
            with urllib.request.urlopen(req, timeout=timeout) as r, open(out, "wb") as f:
                shutil.copyfileobj(r, f)
            return
        except Exception as e:
            last = e; log(f"  failed: {e}")
    raise RuntimeError(f"Stahování selhalo: {url} ({last})")

def valid_folder(f):
    return isinstance(f, str) and f not in ("", ".", "..") and "/" not in f and "\\" not in f

def safe_extract(zip_path, dest, mod=False):
    """mod=True: Thunderstore mod layout, same rules as plugin_files() in scripts/build_modpack.py."""
    dest = Path(dest); dest.mkdir(parents=True, exist_ok=True); root = dest.resolve()
    with zipfile.ZipFile(zip_path) as z:
        for info in z.infolist():
            if info.is_dir():
                continue
            path = info.filename.replace("\\", "/"); low = path.lower()
            if mod:
                if low in ("manifest.json", "readme.md", "changelog.md", "icon.png", "license", "license.md"):
                    continue
                for prefix in ("bepinex/plugins/", "plugins/"):
                    if low.startswith(prefix):
                        path = path[len(prefix):]; break
                if low.startswith("bepinex/config/") or low.startswith("config/"):
                    continue
            target = (dest / path).resolve()
            if not str(target).startswith(str(root) + "/"):
                raise RuntimeError(f"neplatná cesta v zipu: {path}")
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_bytes(z.read(info))

def have_bepinex(game):
    return (game / "BepInEx/core/BepInEx.Preloader.dll").is_file() and (game / "start_game_bepinex.sh").is_file()

def have_free_support(game):
    return all((game / p).is_file() for p in ("BepInEx/core/BepInEx.dll", "start_game_bepinex.sh",
                                            "doorstop_libs/libdoorstop_x64.so"))

def select_edition(game):
    explicit = os.environ["GAME_EDITION"]
    if explicit in ("free", "paid"):
        return explicit
    cfg = read_json(CONFIG) or {}
    saved = cfg.get("game_edition") if cfg.get("game_dir") and Path(cfg["game_dir"]).resolve() == game.resolve() else None
    if saved not in ("free", "paid"):
        saved = None
    if saved and not explicit and os.environ["MODE"] != "install":
        return saved
    found = (game / "BepInEx/core/BepInEx.Preloader.dll").is_file()
    choice = saved if saved and not explicit else ("free" if found else "paid")
    if os.environ["INTERACTIVE"] == "1":
        print("\nVyber verzi hry (volba se uloží):\n"
              "  free — FreeVersion: zachovat Preloader, doplnit ostatní soubory\n"
              "  paid — Paid Version: doplnit / opravit chybějící BepInEx", file=sys.stderr)
        print("Našel jsem Preloader; lze ho zachovat. Samotný soubor neurčuje verzi hry."
              if found else "Preloader jsem nenašel; Paid Version ho doplní.", file=sys.stderr)
        with open("/dev/tty") as tty:
            while True:
                print(f"Volba [{choice}] (z = zrušit): ", end="", file=sys.stderr, flush=True)
                answer = tty.readline()
                if not answer or answer.strip().lower() == "z":
                    raise SystemExit("Instalace zrušena.")
                answer = answer.strip().lower() or choice
                if answer in ("free", "paid"):
                    return answer
    return choice

def needs_bepinex(game):
    return edition == "paid" and not have_bepinex(game)

def pack_state(game):
    return read_json(game / "BepInEx" / "craftama-pack.json")

def up_to_date(game, m):
    st = pack_state(game)
    return (bool(st) and st.get("pack_version") == m["pack_version"] and not needs_bepinex(game)
            and (edition != "free" or (st.get("loader_policy") == 2 and st.get("game_edition") == "free" and have_free_support(game)))
            and all((game / "BepInEx/plugins" / x["folder"]).is_dir() for x in m["mods"] if not x.get("bepinex")))

def install(game, m, tmp):
    plugins = game / "BepInEx" / "plugins"
    install_bep = needs_bepinex(game)
    x = tmp / "pack"
    if edition == "free":
        log("Stahuji FreeVersion — bez Preloaderu…"); download(f"{BASE}/free.zip", tmp / "free.zip"); safe_extract(tmp / "free.zip", x)
    elif not install_bep:
        log("Stahuji módy…"); download(f"{BASE}/plugins.zip", tmp / "plugins.zip"); safe_extract(tmp / "plugins.zip", x)
    else:
        log("Stahuji BepInEx a módy…"); download(f"{BASE}/craftama.zip", tmp / "full.zip"); safe_extract(tmp / "full.zip", x)
    sources = []
    for mod in m["mods"]:
        if mod.get("bepinex"):
            continue
        f = mod["folder"]
        if not valid_folder(f):
            raise RuntimeError(f"neplatná složka v manifestu: {f}")
        if mod.get("download"):
            log(f"Stahuji {mod.get('name', f)} z Thunderstore…")
            z = tmp / f"ext-{f}.zip"; download(mod["download"], z)
            src = tmp / "ext" / f; safe_extract(z, src, mod=True)
        else:
            src = x / "BepInEx" / "plugins" / f
            if not src.is_dir():
                raise RuntimeError(f"v balíčku chybí {f}")
        sources.append((f, src))
    log("Instaluji…")
    if install_bep or edition == "free":
        for p in sorted(x.rglob("*")):
            if p.is_dir():
                continue
            rel = p.relative_to(x)
            # Keep the player's Preloader even if the published free ZIP accidentally includes one.
            if edition == "free" and rel.as_posix().lower() == "bepinex/core/bepinex.preloader.dll":
                continue
            if rel.parts[:2] == ("BepInEx", "plugins"):
                continue
            t = game / rel
            if rel.parts[:2] == ("BepInEx", "config") and t.exists():
                continue
            t.parent.mkdir(parents=True, exist_ok=True); shutil.copyfile(p, t)
        log(f"BepInEx support installed ({edition})")
        for sh in ("start_game_bepinex.sh", "start_server_bepinex.sh"):
            if (game / sh).is_file():
                (game / sh).chmod(0o755)
    plugins.mkdir(parents=True, exist_ok=True)
    folders = []
    for f, src in sources:
        d = plugins / f
        if d.exists():
            shutil.rmtree(d)
        shutil.copytree(src, d); folders.append(f); log(f"  plugin {f}")
    old = pack_state(game) or {}
    for f in old.get("folders") or []:
        if f not in folders and valid_folder(f) and (plugins / f).exists():
            shutil.rmtree(plugins / f); log(f"  removed dropped {f}")
    (game / "BepInEx" / "craftama-pack.json").write_text(json.dumps(
        {"pack_version": m["pack_version"], "folders": folders, "loader_policy": 2, "game_edition": edition,
         "installed_at": datetime.datetime.now().astimezone().isoformat()}, indent=2))
    log("Hotovo.")

def update(game):
    with tempfile.TemporaryDirectory(prefix="craftama-") as t:
        tmp = Path(t)
        try:
            download(f"{BASE}/manifest.json", tmp / "manifest.json", timeout=30)
            m = json.loads((tmp / "manifest.json").read_text(encoding="utf-8"))
            if not m.get("pack_version") or not m.get("mods"):
                raise RuntimeError("manifest.json nemá pack_version / mods")
        except Exception as e:
            log(f"manifest unavailable: {e}")
            if pack_state(game):
                log("Portál je nedostupný, spouštím hru s tím, co už máš."); return True
            log(f"CHYBA: Nepodařilo se spojit s portálem Craftama ({BASE}/manifest.json). Zkontroluj připojení a zkus to znovu.")
            return False
        if up_to_date(game, m):
            log(f"Modpack je aktuální ({m['pack_version']})."); return True
        try:
            install(game, m, tmp); return True
        except Exception as e:
            log(f"install failed: {e}")
            if pack_state(game):
                log(f"CHYBA: Aktualizace se nepovedla ({e}), hra se spustí se stávajícími módy."); return True
            log(f"CHYBA: Instalace modpacku se nepovedla: {e}"); return False

log(f"---- CraftamaValheim (linux) mode={os.environ['MODE']} base={BASE}")
try:
    game = find_game(os.environ.get("GAME_DIR"))
    edition = select_edition(game)
except SystemExit as e:
    log(f"CHYBA: {e}"); sys.exit(3)
log(f"game: {game}")
log(f"game edition={edition} (free=preserve Preloader only, paid=install/repair missing loader)")
CONFIG.write_text(json.dumps({"game_dir": str(game), "game_edition": edition}, indent=2))
ok = update(game)
print(game); print("ok" if ok else "fail"); print("steam" if under_steam(game) else "direct")
print(launch_option() if under_steam(game) else "n/a")
PY
)"
RC=$?
set -e
[[ $RC -eq 3 ]] && exit 1
[[ $RC -eq 0 ]] || { echo "CHYBA: updater selhal (log: $STATE_DIR/valheim-update.log)" >&2; exit 1; }
{ read -r GAME; read -r STATUS; read -r HOW; read -r LOPT; } <<<"$OUT"

if [[ "$HOW" == steam && "$LOPT" != set && -f "$GAME/start_game_bepinex.sh" ]]; then
  cat >&2 <<EOF

Na Linuxu BepInEx nahrává módy přes start_game_bepinex.sh. Nastav jednou ve Steamu:
  Knihovna -> Valheim -> Vlastnosti -> Obecné -> Možnosti spuštění:
    ./start_game_bepinex.sh %command%
EOF
  [[ "$LOPT" == unknown ]] && echo "(nepodařilo se ověřit, jestli už je nastavené)" >&2
  echo >&2
fi

start_game() {
  if [[ "$HOW" == steam ]]; then
    if [[ $NO_LAUNCH -eq 1 ]]; then echo "LAUNCH steam steam://rungameid/892970"; return; fi
    nohup steam steam://rungameid/892970 >/dev/null 2>&1 &
  else
    if [[ -f "$GAME/start_game_bepinex.sh" ]]; then
      if [[ $NO_LAUNCH -eq 1 ]]; then echo "LAUNCH $GAME/start_game_bepinex.sh $GAME/valheim.x86_64"; return; fi
      (cd "$GAME" && nohup ./start_game_bepinex.sh ./valheim.x86_64 >/dev/null 2>&1 &)
    else
      if [[ $NO_LAUNCH -eq 1 ]]; then echo "LAUNCH $GAME/valheim.x86_64"; return; fi
      (cd "$GAME" && nohup ./valheim.x86_64 >/dev/null 2>&1 &)
    fi
  fi
}

if [[ "$MODE" == install ]]; then
  [[ "$STATUS" == ok ]] || exit 1
  echo "Modpack Craftama je nainstalovaný ve $GAME." >&2
  if [[ $YES -eq 1 ]]; then start_game
  elif [[ -t 0 ]]; then
    read -r -p "Spustit hru teď? [a/N] " a
    [[ "$a" =~ ^[aAyY] ]] && start_game
  fi
else
  start_game   # never block playing: start even when the update failed
  [[ "$STATUS" == ok ]] || exit 1
fi
exit 0
