#!/usr/bin/env python3

import fnmatch
import hashlib
import json
import os
import shutil
import sys
from pathlib import Path


BASE_DIR = Path(
    "/opt/rpgmultiplayer/updates/chaapaai"
)

SOURCE_DIR = BASE_DIR / "source"
VERSIONS_DIR = BASE_DIR / "versions"
LATEST_FILE = BASE_DIR / "latest.json"
EXCLUDE_FILE = SOURCE_DIR / "update_exclude.txt"

EXCLUDED_DIRS = {
    ".git",
    "__pycache__",
}

LOCAL_ONLY_FILES = {
    "manifest.json",
    "update_exclude.txt",
}


def load_exclude_patterns():

    patterns = []

    if not EXCLUDE_FILE.exists():
        return patterns

    with EXCLUDE_FILE.open(
        "r",
        encoding="utf-8-sig"
    ) as f:

        for raw_line in f:

            line = raw_line.strip()

            if not line:
                continue

            if line.startswith("#"):
                continue

            line = line.replace("\\", "/")

            if line.startswith("./"):
                line = line[2:]

            if line:
                patterns.append(line)

    return patterns


def is_excluded(relative_path, patterns):

    path_string = relative_path.as_posix()
    filename = relative_path.name

    for pattern in patterns:

        if fnmatch.fnmatch(
            path_string.lower(),
            pattern.lower()
        ):
            return True

        if fnmatch.fnmatch(
            filename.lower(),
            pattern.lower()
        ):
            return True

    return False


def sha256_file(path):

    digest = hashlib.sha256()

    with path.open("rb") as f:

        while True:

            chunk = f.read(1024 * 1024)

            if not chunk:
                break

            digest.update(chunk)

    return digest.hexdigest()


def sha256_bytes(data):

    return hashlib.sha256(data).hexdigest()


def collect_files(version):

    patterns = load_exclude_patterns()

    files = {}
    excluded = []

    for root, dirs, filenames in os.walk(SOURCE_DIR):

        dirs[:] = [
            d for d in dirs
            if d not in EXCLUDED_DIRS
        ]

        for filename in filenames:

            if filename in LOCAL_ONLY_FILES:
                continue

            full_path = Path(root) / filename

            relative_path = (
                full_path.relative_to(SOURCE_DIR)
            )

            if is_excluded(
                relative_path,
                patterns
            ):

                excluded.append(
                    relative_path.as_posix()
                )

                continue

            relative_string = (
                relative_path.as_posix()
            )

            # version.txt is generated for the release.
            # This lets source/version.txt remain the working version
            # while a published release contains its own exact version.
            if relative_string == "version.txt":

                data = (
                    str(version) +
                    "\n"
                ).encode("utf-8")

                files[relative_string] = {
                    "size": len(data),
                    "sha256": sha256_bytes(data)
                }

                continue

            files[relative_string] = {
                "size": full_path.stat().st_size,
                "sha256": sha256_file(full_path)
            }

    return files, excluded


def copy_release_files(
    release_dir,
    version,
    files
):

    for relative_string in files:

        source = (
            SOURCE_DIR /
            relative_string
        )

        destination = (
            release_dir /
            "files" /
            relative_string
        )

        destination.parent.mkdir(
            parents=True,
            exist_ok=True
        )

        if relative_string == "version.txt":

            destination.write_text(
                str(version) + "\n",
                encoding="utf-8"
            )

        else:

            shutil.copy2(
                source,
                destination
            )


def write_manifest(
    release_dir,
    version,
    files
):

    manifest = {
        "project": "chaapaai",
        "version": version,
        "file_count": len(files),
        "files": files
    }

    manifest_path = (
        release_dir /
        "manifest.json"
    )

    manifest_path.write_text(
        json.dumps(
            manifest,
            indent=2,
            ensure_ascii=True
        ),
        encoding="utf-8"
    )

    return manifest_path


def write_latest(version):

    latest = {
        "project": "chaapaai",
        "version": version,
        "manifest": (
            f"versions/{version}/manifest.json"
        ),
        "files": (
            f"versions/{version}/files/"
        )
    }

    LATEST_FILE.write_text(
        json.dumps(
            latest,
            indent=2,
            ensure_ascii=True
        ),
        encoding="utf-8"
    )


def main():

    if len(sys.argv) != 2:

        print("Usage:")
        print("  python3 publish_release.py 1.2.0")
        sys.exit(1)

    version = sys.argv[1].strip()

    if not version:

        print("ERROR: Version fehlt.")
        sys.exit(1)

    if not SOURCE_DIR.exists():

        print("ERROR: Source directory not found:")
        print(SOURCE_DIR)
        sys.exit(1)

    release_dir = (
        VERSIONS_DIR /
        version
    )

    if release_dir.exists():

        print("ERROR: Release existiert bereits:")
        print(release_dir)
        print("Es wurde nichts veraendert.")
        sys.exit(1)

    print()
    print("==============================================")
    print("Chaapa'ai Release Publisher")
    print("Version: " + version)
    print("==============================================")
    print()

    files, excluded = collect_files(version)

    print(
        "Source-Dateien: " +
        str(len(files))
    )

    print(
        "Ausgeschlossen: " +
        str(len(excluded))
    )

    print()

    release_dir.mkdir(
        parents=True,
        exist_ok=False
    )

    try:

        print("Kopiere Release-Dateien...")

        copy_release_files(
            release_dir,
            version,
            files
        )

        print("Erstelle Manifest...")

        manifest_path = write_manifest(
            release_dir,
            version,
            files
        )

        print("Aktualisiere latest.json...")

        write_latest(
            version
        )

    except Exception:

        print()
        print(
            "FEHLER: Release konnte nicht "
            "vollstaendig erstellt werden."
        )

        print(
            "Entferne unvollstaendiges Release..."
        )

        try:
            shutil.rmtree(release_dir)
        except Exception:
            pass

        raise

    print()
    print("==============================================")
    print("Release erfolgreich erstellt.")
    print("Version: " + version)
    print("Dateien: " + str(len(files)))
    print()
    print("Manifest:")
    print(manifest_path)
    print()
    print("Release:")
    print(release_dir)
    print()
    print(
        "latest.json wurde auf " +
        version +
        " gesetzt."
    )
    print("==============================================")
    print()


if __name__ == "__main__":
    main()
