#!/usr/bin/env python3
"""
Nástroj na overenie integrity exportu z CHDÚ — FR SR Certifikácia v1.9, bod 3.B.13.

Samostatný offline nástroj (výhradne štandardná knižnica Pythonu, bez prístupu
k databáze a sieti). Vstupom je exportný ZIP archív z CHDÚ SORP
(`{kód pokladnice} {YYMMDD-hhmmss}.zip`), výstupom výsledok overenia:

1. Overenie súborov .512 — pre každý súbor transakcie sa prepočíta SHA-512
   kontrolný súčet a porovná s obsahom prislúchajúceho .512 súboru
   (formát: SHA512 hex malým písmom + dve medzery + názov súboru + LF).
2. Overenie súborov .SYNC — prepočíta sa reťazený synchronizačný kontrolný
   kód od 128-bajtového nemenného reťazca PPPEK cez všetky súbory všetkých
   transakcií v chronologickom poradí (vrátane súborov .512 a .SYNC)
   a porovná s obsahom každého .SYNC súboru. Ako posledná transakcia sa
   overí audit.csv + audit.512 + audit.SYNC v roote archívu.

Pri narušení integrity nástroj vypíše PRESNÚ lokalizáciu — názov súboru,
pri ktorom kontrola zlyhala. Návratový kód: 0 = OK, 1 = narušenie/chyba.

Použitie:
    chdu_export_verify <export.zip>
    python3 chdu_export_verify.py <export.zip>

Algoritmus reťazenia (zhodný s app/chdu/export_format.py):
    state = SHA512(seed).hexdigest()
    pre každý súbor F v poradí (číslo transakcie, poradie súboru):
        state = SHA512(state + SHA512(F).hexdigest())
        state = SHA512(state + SHA512(F.512).hexdigest())
        F.SYNC musí obsahovať aktuálny state
        state = SHA512(state + SHA512(F.SYNC).hexdigest())
"""

import hashlib
import re
import sys
import zipfile
import zlib

# 128-bajtový nemenný reťazec PPPEK (obfuskovaný XOR poliami) — totožný
# s app/chdu/sync_seed.py; schvaľuje sa počas konania o certifikácii.
_MASK = bytes.fromhex(
    "49dca14fc34abd2f566e13864c6522a9718e303d4cbcb9366905db249aceedfa"
    "2b108694d6b5fbf507bdc9539bd94adf2d5810ff128cfd230bbde780daf92cbf"
    "75f5a64f01a8aca65f4ace02040f9e846a6780ac6fd0cdb393a07a00ef7b40a2"
    "4382c1936f7c5d02ecd058db5bc56df59d78d7e13a7ff12616574618f3ab1635"
)
_XORED = bytes.fromhex(
    "ee2b863534640ddde75a6aab19f19906c280eb7566c2266c67ff8618a5bc9d92"
    "68e8b15d185ce7c87a8c1da3b7abd11ae417ce6a548b1221210665f8731a67d9"
    "5b0f3edb53a3da4de3c86a954200fc6eb8cd4b72bc2418986070d16d3a30a111"
    "0b6c796cedd4449d36fb5d46adce5fd176b6f8f48a295fcd64abf9e20eec85f6"
)

# YY/MM/YYMMDD-IIII-IIII-IIII/YYMMDD-IIII-IIII-IIII-XXXX.EXT
_FILE_RE = re.compile(
    r"^(?P<yy>\d{2})/(?P<mm>\d{2})/"
    r"(?P<dir>\d{6}-\d{4}-\d{4}-\d{4})/"
    r"(?P=dir)-(?P<fileseq>\d{4})\.(?P<ext>JSON|XML|PRN|BIN|AUTH|IDENT|512|SYNC)$"
)


def _seed() -> bytes:
    return bytes(a ^ b for a, b in zip(_MASK, _XORED))


def _sha512(data: bytes) -> str:
    return hashlib.sha512(data).hexdigest()


def _chain(state: str, file_hash: str) -> str:
    return hashlib.sha512((state + file_hash).encode("ascii")).hexdigest()


def verify_export(zip_path: str) -> tuple[bool, list[str]]:
    """Over integritu exportného ZIP. Vracia (ok, zoznam hlásení)."""
    messages: list[str] = []

    try:
        zf = zipfile.ZipFile(zip_path)
    except (OSError, zipfile.BadZipFile) as e:
        return False, [f"CHYBA: archív sa nedá otvoriť: {e}"]

    # Čítanie po súboroch — bajt zmenený priamo v archíve (hexeditorom) sa
    # prejaví CRC chybou pri čítaní člena; musí viesť na presnú lokalizáciu
    # narušenia, nie na pád nástroja.
    contents: dict[str, bytes] = {}
    with zf:
        for info in zf.infolist():
            if info.is_dir():
                continue
            try:
                contents[info.filename] = zf.read(info.filename)
            except (zipfile.BadZipFile, zlib.error, OSError, RuntimeError) as e:
                return False, [
                    f"NARUŠENIE: poškodený obsah súboru {info.filename} v archíve ({e})"
                ]

    # Rozparsuj transakčné súbory: {(txn_seq, file_seq): {ext/512/SYNC: (name, data)}}
    entries: dict[tuple[int, int], dict[str, tuple[str, bytes]]] = {}
    root_files: dict[str, bytes] = {}
    unknown: list[str] = []

    for path, data in contents.items():
        if path in ("audit.csv", "audit.512", "audit.SYNC"):
            root_files[path] = data
            continue
        m = _FILE_RE.match(path)
        if not m:
            unknown.append(path)
            continue
        txn_seq = int(m.group("dir")[7:].replace("-", ""))
        file_seq = int(m.group("fileseq"))
        ext = m.group("ext")
        slot = entries.setdefault((txn_seq, file_seq), {})
        key = ext if ext in ("512", "SYNC") else "data"
        slot[key] = (path, data)

    ok = True
    for path in sorted(unknown):
        ok = False
        messages.append(f"NARUŠENIE: neočakávaný súbor v archíve: {path}")

    if "audit.csv" not in root_files:
        return False, messages + ["NARUŠENIE: v roote archívu chýba audit.csv"]
    for required in ("audit.512", "audit.SYNC"):
        if required not in root_files:
            return False, messages + [f"NARUŠENIE: v roote archívu chýba {required}"]

    state = _sha512(_seed())
    checked = 0

    for (txn_seq, file_seq) in sorted(entries):
        slot = entries[(txn_seq, file_seq)]
        if "data" not in slot:
            return False, messages + [
                f"NARUŠENIE: chýba dátový súbor transakcie {txn_seq:012d}, poradie {file_seq:04d}"
            ]
        name = slot["data"][0].rsplit("/", 1)[-1]
        data = slot["data"][1]

        # 1) .512 — kontrolný súčet
        content_hash = _sha512(data)
        state = _chain(state, content_hash)

        if "512" not in slot:
            return False, messages + [f"NARUŠENIE: chýba súbor .512 pre {name}"]
        expected_512 = f"{content_hash}  {name}\n".encode("ascii")
        actual_512 = slot["512"][1]
        if actual_512 != expected_512:
            return False, messages + [
                f"NARUŠENIE: kontrolný súčet nesúhlasí pre {name} "
                f"(súbor {slot['512'][0]})"
            ]
        state = _chain(state, _sha512(actual_512))

        # 2) .SYNC — synchronizačný kontrolný kód
        if "SYNC" not in slot:
            return False, messages + [f"NARUŠENIE: chýba súbor .SYNC pre {name}"]
        expected_sync = (state + "\n").encode("ascii")
        actual_sync = slot["SYNC"][1]
        if actual_sync != expected_sync:
            return False, messages + [
                f"NARUŠENIE: synchronizačný kontrolný kód nesúhlasí — reťaz "
                f"integrity je prerušená pri súbore {slot['SYNC'][0]}"
            ]
        state = _chain(state, _sha512(actual_sync))
        checked += 1

    # audit.csv ako posledná transakcia
    audit_csv = root_files["audit.csv"]
    audit_hash = _sha512(audit_csv)
    state = _chain(state, audit_hash)

    expected_audit_512 = f"{audit_hash}  audit.csv\n".encode("ascii")
    if root_files["audit.512"] != expected_audit_512:
        return False, messages + ["NARUŠENIE: kontrolný súčet nesúhlasí pre audit.csv (audit.512)"]
    state = _chain(state, _sha512(root_files["audit.512"]))

    if root_files["audit.SYNC"] != (state + "\n").encode("ascii"):
        return False, messages + [
            "NARUŠENIE: synchronizačný kontrolný kód nesúhlasí pre audit.csv (audit.SYNC)"
        ]

    if ok:
        messages.append(f"OK: overených {checked} súborov transakcií + audit.csv — integrita neporušená")
    return ok, messages


def main() -> int:
    # Windows konzola môže bežať v cp1252, ktorá nepozná slovenské znaky —
    # bez tohto by výpis (ť, š, á…) skončil UnicodeEncodeError.
    for stream in (sys.stdout, sys.stderr):
        if hasattr(stream, "reconfigure"):
            stream.reconfigure(encoding="utf-8", errors="replace")
    if len(sys.argv) != 2:
        print("Použitie: chdu_export_verify <export.zip>", file=sys.stderr)
        return 1
    ok, messages = verify_export(sys.argv[1])
    for msg in messages:
        print(msg)
    print("VÝSLEDOK: INTEGRITA OK" if ok else "VÝSLEDOK: INTEGRITA NARUŠENÁ")
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
