"""Read-only manifest generation and verification for an owner-supplied folder.

Usage: python manifest_tool.py make ROOT OUTSIDE_MANIFEST
       python manifest_tool.py verify ROOT OUTSIDE_MANIFEST
Symlinks are refused. Keep the manifest outside ROOT. No files are repaired.
"""
import argparse
import hashlib
import json
from pathlib import Path


def inventory(root):
    root = Path(root)
    if root.is_symlink():
        raise ValueError('Root symlinks require separate explicit scope')
    root = root.resolve()
    if not root.is_dir():
        raise ValueError('Supply an existing directory')
    result = {}
    for path in sorted(root.rglob('*')):
        if path.is_symlink():
            raise ValueError('Symlinks require separate explicit scope')
        if path.is_file():
            with path.open('rb') as stream:
                digest = hashlib.file_digest(stream, 'sha256').hexdigest()
            result[path.relative_to(root).as_posix()] = {
                'bytes': path.stat().st_size, 'sha256': digest}
    return result


def compare(expected, actual):
    return {
        'missing': sorted(expected.keys() - actual.keys()),
        'unexpected': sorted(actual.keys() - expected.keys()),
        'changed': sorted(k for k in expected.keys() & actual.keys()
                          if expected[k] != actual[k]),
    }


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=['make', 'verify'])
    parser.add_argument('root', type=Path)
    parser.add_argument('manifest', type=Path)
    args = parser.parse_args()
    if args.manifest.resolve().is_relative_to(args.root.resolve()):
        parser.error('Manifest must be outside the inventoried folder')
    actual = inventory(args.root)
    if args.action == 'make':
        # Exclusive creation: an existing manifest must not be overwritten.
        with args.manifest.open('x') as out:
            json.dump(actual, out, indent=2)
            out.write('\n')
    else:
        result = compare(json.loads(args.manifest.read_text()), actual)
        print(json.dumps(result, indent=2))
        raise SystemExit(1 if any(result.values()) else 0)
