"""
VoxelRealm License Server
Generates personalized license PDF + installer on demand.
"""
import os, sys, shutil, subprocess, tempfile, json, zipfile, fnmatch
from pathlib import Path
from flask import Flask, request, send_file, jsonify, after_this_request, send_from_directory
from generate_license import generate_license_pdf

app = Flask(__name__)

@app.after_request
def cors(response):
    response.headers["Access-Control-Allow-Origin"]  = "*"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    return response

@app.route("/", defaults={"path": "index.html"})
@app.route("/<path:path>")
def website(path):
    web_dir = str(BASE / "website")
    return send_from_directory(web_dir, path)

BASE     = Path(__file__).parent.parent          # minecraft/
INST_DIR = BASE / "installer"
MAKENSIS     = os.environ.get("MAKENSIS",     "/tmp/nsis-bin/bin/makensis")
NSISDIR      = os.environ.get("NSISDIR_PATH", "/tmp/nsis-bin/share/nsis")
APPIMAGETOOL = os.environ.get("APPIMAGETOOL", "/tmp/appimagetool")
DOWNLOAD_DIR = BASE / "website" / "downloads"

# ── Cyrillic → Latin transliteration (for in-game ASCII font) ───────────────
_TRANSLIT = {
    'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','є':'ye','ж':'zh',
    'з':'z','и':'i','і':'i','й':'y','к':'k','л':'l','м':'m','н':'n',
    'о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh',
    'ц':'ts','ч':'ch','ш':'sh','щ':'sch','ь':'','ю':'yu','я':'ya',
    'А':'A','Б':'B','В':'V','Г':'G','Д':'D','Е':'E','Є':'Ye','Ж':'Zh',
    'З':'Z','И':'I','І':'I','Й':'Y','К':'K','Л':'L','М':'M','Н':'N',
    'О':'O','П':'P','Р':'R','С':'S','Т':'T','У':'U','Ф':'F','Х':'Kh',
    'Ц':'Ts','Ч':'Ch','Ш':'Sh','Щ':'Sch','Ь':'','Ю':'Yu','Я':'Ya',
    'ё':'yo','Ё':'Yo','ъ':'','Ъ':'','ы':'y','Ы':'Y','э':'e','Э':'E',
    'ї':'yi','Ї':'Yi','ґ':'g','Ґ':'G',
}

def _transliterate(s: str) -> str:
    """Convert Cyrillic characters to Latin equivalents for ASCII font rendering."""
    return ''.join(_TRANSLIT.get(c, c) for c in s)

# ── Validate nickname ───────────────────────────────────────────────────────
def _validate(nickname: str) -> str | None:
    nickname = nickname.strip()
    if not nickname:
        return "Нікнейм не може бути порожнім"
    if len(nickname) < 2:
        return "Нікнейм занадто короткий (мін. 2 символи)"
    if len(nickname) > 24:
        return "Нікнейм занадто довгий (макс. 24 символи)"
    allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
                  "0123456789_-"
                  "\u0400-\u04ff")  # Cyrillic range
    for ch in nickname:
        if ch not in allowed and not ('\u0400' <= ch <= '\u04ff'):
            return f"Заборонений символ: '{ch}'. Дозволено: A-Z, a-z, 0-9, _, -, кирилиця"
    return None

# ── Build Windows installer ─────────────────────────────────────────────────
def _build_windows_installer(nickname: str, pdf_path: str, tmp_dir: str) -> str:
    out_exe = os.path.join(tmp_dir, "VoxelRealm-Setup.exe")
    # Patch the NSIS script to include license.pdf and player.cfg
    nsi_src  = (INST_DIR / "VoxelRealm.nsi").read_text()
    # Inject extra files into install section
    extra = f'''
    ; Personalized files
    File "{pdf_path}"
    SetOutPath "$INSTDIR"
    File "{os.path.join(tmp_dir, "player.cfg")}"
'''
    nsi_patched = nsi_src.replace(
        '; Personalized files (injected)',
        extra
    )
    nsi_file = os.path.join(tmp_dir, "VoxelRealm_personal.nsi")
    # Fix OutFile to write to tmp_dir
    nsi_patched = nsi_patched.replace(
        'OutFile             "..\\website\\downloads\\VoxelRealm-Setup.exe"',
        f'OutFile             "{out_exe}"'
    )
    # Make asset paths absolute (NSI relative paths are relative to script location = tmp_dir)
    assets_abs = str(INST_DIR / "assets").replace('\\', '/')
    game_files_abs = str(INST_DIR / "game_files").replace('\\', '/')
    nsi_patched = nsi_patched.replace('"assets\\', f'"{assets_abs}\\')
    nsi_patched = nsi_patched.replace('"game_files\\', f'"{game_files_abs}\\')
    nsi_patched = nsi_patched.replace('"assets/', f'"{assets_abs}/')
    nsi_patched = nsi_patched.replace('"game_files/', f'"{game_files_abs}/')
    with open(nsi_file, 'w') as f:
        f.write(nsi_patched)

    env = os.environ.copy()
    env["NSISDIR"] = NSISDIR
    result = subprocess.run(
        [MAKENSIS, nsi_file],
        capture_output=True, text=True, env=env,
        cwd=str(BASE)
    )
    if result.returncode != 0:
        raise RuntimeError(f"makensis failed:\n{result.stderr}")
    return out_exe

# ── Build Linux AppImage ──────────────────────────────────────────────────────
def _build_linux_package(nickname: str, pdf_path: str, tmp_dir: str) -> str:
    from PIL import Image

    # Read personalized info from player.cfg
    with open(os.path.join(tmp_dir, "player.cfg")) as f:
        cfg = json.load(f)
    lic_id   = cfg["license_id"]
    game_nick = cfg["nickname"]   # already transliterated

    # ── AppDir structure ──────────────────────────────────────────────────────
    appdir      = os.path.join(tmp_dir, "VoxelRealm.AppDir")
    usr_bin     = os.path.join(appdir, "usr", "bin")
    usr_lib     = os.path.join(appdir, "usr", "lib")
    usr_share   = os.path.join(appdir, "usr", "share", "VoxelRealm")
    shaders_dir = os.path.join(usr_share, "shaders")

    for d in [usr_bin, usr_lib, shaders_dir]:
        os.makedirs(d)

    # Game binary
    shutil.copy(str(BASE / "build" / "minecraft"), os.path.join(usr_bin, "VoxelRealm"))
    os.chmod(os.path.join(usr_bin, "VoxelRealm"), 0o755)

    # Shaders
    shutil.copy(str(BASE / "shaders" / "triangle.vert.spv"), shaders_dir)
    shutil.copy(str(BASE / "shaders" / "triangle.frag.spv"), shaders_dir)

    # Textures
    shutil.copytree(str(BASE / "textures"), os.path.join(usr_share, "textures"))

    # Personalized files
    shutil.copy(pdf_path, os.path.join(usr_share, "license.pdf"))
    shutil.copy(os.path.join(tmp_dir, "player.cfg"), usr_share)

    # Bundle libraries (skip system/vulkan libs that must match GPU driver)
    skip = {"libvulkan.so.1", "libc.so.6", "libm.so.6", "libdl.so.2",
            "libpthread.so.0", "librt.so.1"}
    ldd = subprocess.run(["ldd", os.path.join(usr_bin, "VoxelRealm")],
                         capture_output=True, text=True)
    for line in ldd.stdout.splitlines():
        parts = line.strip().split()
        if "=>" in parts:
            idx = parts.index("=>")
            if idx + 1 < len(parts) and parts[idx+1].startswith("/"):
                lib = parts[idx+1]
                name = os.path.basename(lib)
                if name not in skip and not name.startswith("ld-linux"):
                    shutil.copy(lib, os.path.join(usr_lib, name))

    # ── Icon (dirt texture scaled to 256×256) ─────────────────────────────────
    dirt = str(BASE / "textures" / "blocks" / "dirt.png")
    icon = Image.open(dirt).resize((256, 256), Image.NEAREST)
    icon_path = os.path.join(appdir, "VoxelRealm.png")
    icon.save(icon_path)
    shutil.copy(icon_path, os.path.join(appdir, ".DirIcon"))

    # ── AppRun launcher ───────────────────────────────────────────────────────
    apprun_path = os.path.join(appdir, "AppRun")
    with open(apprun_path, "w") as f:
        f.write(f"""\
#!/bin/bash
APPDIR="$(dirname "$(readlink -f "${{0}}")")"
export LD_LIBRARY_PATH="$APPDIR/usr/lib:${{LD_LIBRARY_PATH}}"

RUNTIME="$HOME/.local/share/VoxelRealm"
mkdir -p "$RUNTIME"

# Copy game data on first run
for dir in shaders textures; do
    [ -d "$RUNTIME/$dir" ] || cp -r "$APPDIR/usr/share/VoxelRealm/$dir" "$RUNTIME/$dir"
done

# Always overwrite personalized files (unique per download)
cp "$APPDIR/usr/share/VoxelRealm/player.cfg" "$RUNTIME/player.cfg"
cp "$APPDIR/usr/share/VoxelRealm/license.pdf" "$RUNTIME/license.pdf"

# Show license dialog on first run of this license
MARKER="$RUNTIME/.license_shown_{lic_id}"
if [ ! -f "$MARKER" ]; then
    touch "$MARKER"
    # Open PDF in background
    xdg-open "$RUNTIME/license.pdf" 2>/dev/null &
    # Welcome + license info dialog
    zenity --info \\
        --title="VoxelRealm — Ліцензія отримана!" \\
        --width=460 --height=220 \\
        --ok-label="Запустити гру ›" \\
        --text="<big><b>Ласкаво просимо, {game_nick}!</b></big>

<b>Ліцензія №:</b>  {lic_id}
<b>Нікнейм:</b>     {game_nick}

Ваша персональна ліцензія (<b>license.pdf</b>) відкрита у переглядачі.
Зберігайте її — вона підписана ключем RSA-8192 і підтверджує вашу копію гри.

Файл також збережено у:
<small>$RUNTIME/license.pdf</small>" 2>/dev/null
fi

cd "$RUNTIME"
exec "$APPDIR/usr/bin/VoxelRealm" "$@"
""")
    os.chmod(apprun_path, 0o755)

    # ── .desktop inside AppDir ────────────────────────────────────────────────
    with open(os.path.join(appdir, "VoxelRealm.desktop"), "w") as f:
        f.write("[Desktop Entry]\nName=VoxelRealm\n"
                "Comment=Open Voxel World — Vulkan · C++20\n"
                "Exec=VoxelRealm\nIcon=VoxelRealm\n"
                "Terminal=false\nType=Application\nCategories=Game;\n")

    # ── Build AppImage ────────────────────────────────────────────────────────
    out_appimage = os.path.join(tmp_dir, f"VoxelRealm-{nickname}-x86_64.AppImage")
    env = os.environ.copy()
    env["ARCH"] = "x86_64"
    result = subprocess.run(
        [APPIMAGETOOL, "--no-appstream", appdir, out_appimage],
        capture_output=True, text=True, env=env
    )
    if result.returncode != 0:
        raise RuntimeError(f"appimagetool failed:\n{result.stderr}\n{result.stdout}")
    os.chmod(out_appimage, 0o755)
    return out_appimage

# ── Endpoints ───────────────────────────────────────────────────────────────
@app.route("/api/generate", methods=["POST"])
def generate():
    data     = request.get_json(force=True)
    nickname = data.get("nickname", "").strip()
    platform = data.get("platform", "windows")  # "windows" or "linux"

    err = _validate(nickname)
    if err:
        return jsonify({"error": err}), 400

    tmp_dir = tempfile.mkdtemp(prefix="voxelrealm_")
    try:
        # 1. Generate PDF license
        pdf_path = os.path.join(tmp_dir, "license.pdf")
        meta = generate_license_pdf(nickname, pdf_path)

        # 2. Write player config (game reads this)
        # Transliterate Cyrillic → Latin: game uses ASCII bitmap font
        game_nickname = _transliterate(nickname)
        cfg = {"nickname": game_nickname, "license_id": meta["license_id"]}
        with open(os.path.join(tmp_dir, "player.cfg"), "w") as f:
            json.dump(cfg, f)

        # 3. Build installer/package
        if platform == "linux":
            out_file = _build_linux_package(nickname, pdf_path, tmp_dir)
            mime = "application/x-executable"
            dl_name = f"VoxelRealm-{nickname}-x86_64.AppImage"
        else:
            out_file = _build_windows_installer(nickname, pdf_path, tmp_dir)
            mime = "application/octet-stream"
            dl_name = f"VoxelRealm-{nickname}-Setup.exe"

        @after_this_request
        def cleanup(response):
            shutil.rmtree(tmp_dir, ignore_errors=True)
            return response

        return send_file(
            out_file,
            mimetype=mime,
            as_attachment=True,
            download_name=dl_name
        )

    except Exception as e:
        shutil.rmtree(tmp_dir, ignore_errors=True)
        return jsonify({"error": str(e)}), 500

@app.route("/api/check", methods=["GET"])
def check():
    return jsonify({"status": "ok", "version": "1.0"})

# ── Source code download ─────────────────────────────────────────────────────
_SOURCE_ZIP_PATH = DOWNLOAD_DIR / "VoxelRealm-source.zip"
_SOURCE_EXCLUDE_DIRS = {
    ".claude", ".git", "__pycache__", "build", "build-windows", "build-native",
    "node_modules", "downloads", "keys",
}
_SOURCE_EXCLUDE_PATTERNS = [
    "*.pyc", "*.pyo", "license_counter.json", "voxelrealm_*",
    "*.o", "*.obj", "*.exe", "*.dll", "*.a", "*.so", "*.dylib",
    ".DS_Store", "Thumbs.db", "*.log",
]

def _should_skip(name: str, is_dir: bool) -> bool:
    if is_dir and name in _SOURCE_EXCLUDE_DIRS:
        return True
    for pat in _SOURCE_EXCLUDE_PATTERNS:
        if fnmatch.fnmatch(name, pat):
            return True
    return False

def _build_source_zip():
    """Generate the source code zip (excludes .claude, build artifacts, secrets)."""
    DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
    tmp = _SOURCE_ZIP_PATH.with_suffix(".zip.tmp")
    with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
        for root, dirs, files in os.walk(BASE):
            dirs[:] = [d for d in dirs if not _should_skip(d, True)]
            for f in files:
                if _should_skip(f, False):
                    continue
                abs_path = Path(root) / f
                rel = abs_path.relative_to(BASE)
                # Never ship private key material
                if "keys" in rel.parts and f.endswith(".pem") and "private" in f.lower():
                    continue
                if rel.parts and rel.parts[0] == "server" and "keys" in rel.parts:
                    continue
                zf.write(abs_path, Path("VoxelRealm") / rel)
    tmp.replace(_SOURCE_ZIP_PATH)

@app.route("/api/source", methods=["GET"])
def source_code():
    """Serve the source code zip. Rebuilds if missing or older than 1 hour."""
    import time
    need_build = (
        not _SOURCE_ZIP_PATH.exists()
        or (time.time() - _SOURCE_ZIP_PATH.stat().st_mtime) > 3600
    )
    if need_build:
        _build_source_zip()
    return send_file(
        str(_SOURCE_ZIP_PATH),
        mimetype="application/zip",
        as_attachment=True,
        download_name="VoxelRealm-source.zip",
    )

@app.route("/api/public-key", methods=["GET"])
def public_key():
    """Return the RSA-8192 public key for license verification."""
    key_path = BASE / "server" / "keys" / "public.pem"
    return send_file(str(key_path), mimetype="application/x-pem-file",
                     as_attachment=True, download_name="VoxelRealm-public.pem")

@app.route("/api/verify", methods=["POST"])
def verify():
    """Verify a license payload against its signature."""
    from cryptography.hazmat.primitives.asymmetric import padding as _padding
    from cryptography.hazmat.primitives import hashes as _hashes, serialization as _ser
    import base64 as _b64
    data = request.get_json(force=True)
    try:
        payload   = data["payload"]   # "LICENSE:VR-000001|NICKNAME:...|DATE:..."
        signature = data["signature"] # base64-encoded
        pub = _ser.load_pem_public_key((BASE / "server" / "keys" / "public.pem").read_bytes())
        pub.verify(_b64.b64decode(signature), payload.encode(),
                   _padding.PKCS1v15(), _hashes.SHA512())
        return jsonify({"valid": True})
    except Exception as e:
        return jsonify({"valid": False, "reason": str(e)}), 400

if __name__ == "__main__":
    host = os.environ.get("HOST", "127.0.0.1")
    port = int(os.environ.get("PORT", 5000))
    print(f"VoxelRealm License Server running on http://{host}:{port}")
    app.run(host=host, port=port, debug=False)
