"""Byte-level manifest for a quiet local dataset directory. Python 3.11+."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import sys


def scan(root):
    def fail(error):
        raise error

    files = {}
    for folder, dirs, names in os.walk(root, onerror=fail, followlinks=False):
        for name in dirs + names:
            path = Path(folder) / name
            mode = path.lstat().st_mode
            if stat.S_ISLNK(mode):
                raise ValueError(f"Symlink is not supported: {path}")
            if stat.S_ISDIR(mode):
                continue
            if not stat.S_ISREG(mode):
                raise ValueError(f"Not a regular file: {path}")
            with path.open("rb") as handle:
                digest = hashlib.file_digest(handle, "sha256").hexdigest()
            files[path.relative_to(root).as_posix()] = digest
    return files


def read_manifest(path):
    data = json.loads(path.read_text(encoding="utf-8"))
    if (not isinstance(data, dict) or data.get("version") != 1
            or data.get("algorithm") != "sha256"
            or not isinstance(data.get("files"), dict)):
        raise ValueError("Unsupported manifest format")
    for name, digest in data["files"].items():
        if (not name or not isinstance(digest, str)
                or re.fullmatch(r"[0-9a-f]{64}", digest) is None):
            raise ValueError("Invalid manifest entry")
    return data["files"]


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("action", choices=["create", "verify"])
    parser.add_argument("root", type=Path)
    parser.add_argument("manifest", type=Path)
    args = parser.parse_args()
    try:
        if args.root.is_symlink():
            raise ValueError("Dataset root must not be a symlink")
        root = args.root.resolve(strict=True)
        if not root.is_dir():
            raise ValueError("Dataset root must be a directory")
        manifest = args.manifest.resolve()
        if manifest.is_relative_to(root):
            raise ValueError("Keep the manifest outside the dataset directory")
        expected = read_manifest(manifest) if args.action == "verify" else None
        current = scan(root)
        if args.action == "create":
            data = {"version": 1, "algorithm": "sha256", "files": current}
            with manifest.open("x", encoding="utf-8", newline="\n") as handle:
                handle.write(json.dumps(data, sort_keys=True, indent=2) + "\n")
            print(f"CREATED: {len(current)} files")
            return 0
        missing = sorted(expected.keys() - current.keys())
        added = sorted(current.keys() - expected.keys())
        changed = sorted(name for name in expected.keys() & current.keys()
                         if expected[name] != current[name])
        for label, names in [("MISSING", missing), ("ADDED", added),
                             ("CHANGED", changed)]:
            for name in names:
                print(f"{label}: {json.dumps(name, ensure_ascii=False)}")
        if missing or added or changed:
            return 1
        print(f"OK: {len(current)} files match")
        return 0
    except (OSError, ValueError) as error:
        print(f"ERROR: {error}", file=sys.stderr)
        return 2


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