#!/usr/bin/env python3
"""Installiert Bastelpingu Skinbilder sicher in eine bestehende Kodi-Repo."""

from __future__ import annotations

import argparse
import hashlib
import io
import os
import shutil
import sys
import zipfile
from datetime import datetime
from pathlib import Path
import xml.etree.ElementTree as ET


ADDON_ID = "script.bastelpingu.skinbilder"
VERSION = "1.3.0"
ZIP_NAME = f"{ADDON_ID}-{VERSION}.zip"
EXPECTED_SHA256 = "2b547398da1b3aadbed19ec89a54c6a4cb27bac9e67d2cbf4b39e5c10927d90e"


def abort(message: str) -> None:
    print(f"FEHLER: {message}", file=sys.stderr)
    raise SystemExit(1)


def digest(path: Path, algorithm: str) -> str:
    hasher = hashlib.new(algorithm)
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            hasher.update(block)
    return hasher.hexdigest()


def serialized_xml(tree: ET.ElementTree) -> bytes:
    ET.indent(tree, space="    ")
    buffer = io.BytesIO()
    tree.write(buffer, encoding="UTF-8", xml_declaration=True)
    return buffer.getvalue() + b"\n"


def atomic_write(path: Path, data: bytes) -> None:
    temporary = path.with_name(f".{path.name}.tmp-{os.getpid()}")
    try:
        temporary.write_bytes(data)
        os.replace(temporary, path)
    finally:
        if temporary.exists():
            temporary.unlink()


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Nimmt Bastelpingu Skinbilder 1.3.0 in eine bestehende Kodi-Repo auf."
    )
    parser.add_argument(
        "--repo",
        default="/var/www/repoKodi",
        help="Pfad zur bestehenden Kodi-Repo (Standard: /var/www/repoKodi)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Nur prüfen; noch keine Dateien verändern",
    )
    args = parser.parse_args()

    repo = Path(args.repo).expanduser().resolve()
    package_root = Path(__file__).resolve().parent
    payload = package_root / ADDON_ID
    source_zip = payload / ZIP_NAME
    addons_xml = repo / "addons.xml"
    addons_md5 = repo / "addons.xml.md5"

    if repo in {Path("/"), Path("/var"), Path("/var/www")}:
        abort(f"Unsicherer Repo-Pfad: {repo}")
    if not repo.is_dir():
        abort(f"Repo-Ordner existiert nicht: {repo}")
    if not addons_xml.is_file():
        abort(f"Vorhandene addons.xml fehlt: {addons_xml}")

    required = {
        source_zip: "Add-on-ZIP",
        payload / "icon.png": "Repo-Icon",
        payload / "fanart.jpg": "Repo-Fanart",
        payload / f"changelog-{VERSION}.txt": "Changelog",
    }
    for path, label in required.items():
        if not path.is_file():
            abort(f"{label} fehlt im Paket: {path}")

    actual_sha256 = digest(source_zip, "sha256")
    if actual_sha256 != EXPECTED_SHA256:
        abort(
            "Prüfsumme der Add-on-ZIP stimmt nicht. "
            f"Erwartet {EXPECTED_SHA256}, erhalten {actual_sha256}."
        )

    member_name = f"{ADDON_ID}/addon.xml"
    try:
        with zipfile.ZipFile(source_zip) as archive:
            bad_member = archive.testzip()
            if bad_member:
                abort(f"Beschädigte Datei in der ZIP: {bad_member}")
            names = archive.namelist()
            if member_name not in names:
                abort(f"{member_name} fehlt in der Add-on-ZIP")
            if any(
                name and not name.startswith(f"{ADDON_ID}/")
                for name in names
            ):
                abort("Die Add-on-ZIP enthält Dateien außerhalb ihres Add-on-Ordners")
            addon_element = ET.fromstring(archive.read(member_name))
    except (zipfile.BadZipFile, ET.ParseError) as error:
        abort(f"Add-on-ZIP kann nicht gelesen werden: {error}")

    if addon_element.tag != "addon":
        abort("addon.xml besitzt kein <addon>-Wurzelelement")
    if addon_element.get("id") != ADDON_ID:
        abort(f"Unerwartete Add-on-ID: {addon_element.get('id')}")
    if addon_element.get("version") != VERSION:
        abort(f"Unerwartete Add-on-Version: {addon_element.get('version')}")

    try:
        xml_parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
        tree = ET.parse(addons_xml, parser=xml_parser)
    except ET.ParseError as error:
        abort(f"Bestehende addons.xml ist ungültig: {error}")

    root = tree.getroot()
    if root.tag != "addons":
        abort(f"Unerwartetes Wurzelelement in addons.xml: <{root.tag}>")

    old_entries = [
        child
        for child in list(root)
        if child.tag == "addon" and child.get("id") == ADDON_ID
    ]
    for child in old_entries:
        root.remove(child)
    root.append(addon_element)

    new_xml = serialized_xml(tree)
    new_md5 = hashlib.md5(new_xml).hexdigest().encode("ascii") + b"\n"

    action = "ersetzen" if old_entries else "neu hinzufügen"
    print(f"Prüfung erfolgreich: {ADDON_ID} {VERSION} ({action})")
    print(f"ZIP SHA-256: {actual_sha256}")

    if args.dry_run:
        print("Dry-Run abgeschlossen – es wurde nichts verändert.")
        return 0

    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    xml_backup = repo / f"addons.xml.bak-{timestamp}"
    shutil.copy2(addons_xml, xml_backup)
    if addons_md5.exists():
        shutil.copy2(addons_md5, repo / f"addons.xml.md5.bak-{timestamp}")

    destination = repo / ADDON_ID
    destination.mkdir(parents=True, exist_ok=True)
    for source in required:
        shutil.copy2(source, destination / source.name)

    zip_md5 = digest(destination / ZIP_NAME, "md5") + "\n"
    atomic_write(destination / f"{ZIP_NAME}.md5", zip_md5.encode("ascii"))
    atomic_write(addons_xml, new_xml)
    atomic_write(addons_md5, new_md5)

    print(f"Sicherung: {xml_backup}")
    print(f"Repo-Ordner: {destination}")
    print(f"Neue addons.xml.md5: {new_md5.decode().strip()}")
    print("Fertig – das Add-on ist vollständig in der Kodi-Repo eingetragen.")
    return 0


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