"""Prepare/verify P06 image batches from explicit local originals; Pillow==12.3.0.
Hashes and decoded transforms are verified; licensing and independent provenance
remain human review. No network access and no automatic evaluation or training.
"""
import argparse
import hashlib
import importlib.util
import io
import json
from pathlib import Path
import PIL
from PIL import Image

spec = importlib.util.spec_from_file_location('evaluation', Path(__file__).with_name('perception-evaluation.py'))
e = importlib.util.module_from_spec(spec)
spec.loader.exec_module(e)
RECIPE = 'Pillow 12.3.0; RGB; aspect-preserving longest side 48 with round; BOX RGB, NEAREST trimap; no EXIF orientation transform'


def source_bytes(root, name):
    e.text(name, 240)
    relative = Path(name)
    if relative.is_absolute() or '..' in relative.parts:
        raise ValueError('Use explicit relative source paths without parent traversal.')
    path = (root/relative).resolve()
    if not path.is_relative_to(root.resolve()) or not path.is_file():
        raise ValueError('Source must be a regular file within the manifest directory.')
    with path.open('rb') as stream:
        raw = stream.read(4000001)
    if not raw or len(raw) > 4000000:
        raise ValueError('Source file must be nonempty and at most 4 MB.')
    return raw


def decode(raw, mask=False):
    with Image.open(io.BytesIO(raw)) as image:
        if image.format not in ('JPEG', 'PNG') or getattr(image, 'n_frames', 1) != 1:
            raise ValueError('Use one-frame JPEG/PNG sources.')
        if not 1 <= image.width*image.height <= 4000000:
            raise ValueError('Decoded source exceeds four million pixels.')
        if image.getexif().get(274, 1) != 1:
            raise ValueError('Resolve image/annotation orientation together before preparation.')
        if mask:
            if image.mode not in ('L', 'P'):
                raise ValueError('Trimap must contain integer class IDs in L or P mode.')
            image.load()
            if any(v not in (1, 2, 3) for v in image.get_flattened_data()):
                raise ValueError('Source trimap contains labels outside 1/2/3.')
            return image.copy()
        if image.mode not in ('RGB', 'L'):
            raise ValueError('Use RGB or grayscale without an implicit alpha/color-profile conversion.')
        return image.convert('RGB')


def prepare(manifest_path):
    if PIL.__version__ != '12.3.0':
        raise ValueError('Use Pillow==12.3.0 to reproduce the pinned preparation recipe.')
    manifest_path = Path(manifest_path)
    value = e.read(manifest_path, 32000)
    if value.get('version') != 'p06-source-manifest-v1':
        raise ValueError('Unknown source manifest.')
    header = {k: e.text(value.get(k)) for k in ('attribution', 'license', 'source', 'sourceGroupingReview')}
    sources = value.get('records')
    if not isinstance(sources, list) or not 1 <= len(sources) <= 16:
        raise ValueError('Use 1–16 original source pairs.')
    records = []
    seen_ids, seen_hashes = set(), set()
    for source in sources:
        identity = e.text(source.get('id'), 160)
        if source.get('sourceGroup') != identity or identity in seen_ids:
            raise ValueError('Use one full image per unique source group/id.')
        image_raw = source_bytes(manifest_path.parent, source.get('image'))
        mask_raw = source_bytes(manifest_path.parent, source.get('trimap'))
        digest = hashlib.sha256(image_raw).hexdigest()
        if digest in seen_hashes:
            raise ValueError('Duplicate source bytes.')
        seen_ids.add(identity); seen_hashes.add(digest)
        image, mask = decode(image_raw), decode(mask_raw, mask=True)
        if image.size != mask.size:
            raise ValueError('Original image and trimap dimensions disagree.')
        width, height = image.size
        ratio = 48/max(width, height)
        size = (max(1, round(width*ratio)), max(1, round(height*ratio)))
        rgb = image.resize(size, Image.Resampling.BOX)
        trimap = mask.resize(size, Image.Resampling.NEAREST)
        records.append({'id': identity, 'sourceGroup': identity, 'sourceHash': digest,
                        'sourceTrimapHash': hashlib.sha256(mask_raw).hexdigest(),
                        'originalWidth': width, 'originalHeight': height,
                        'width': size[0], 'height': size[1],
                        'rgb': [v for pixel in rgb.get_flattened_data() for v in pixel], 'trimap': list(trimap.get_flattened_data())})
    result = {'version': 'p06-prepared-evaluation-v1', **header, 'preparation': RECIPE,
              'sourceManifestDigest': e.digest(value), 'records': records}
    # Reuse structural checks without declaring any sources unknown to a model.
    e.validate_batch(result, {'excluded': []})
    return result


def verify(manifest_path, prepared):
    actual = prepare(manifest_path)
    if actual != prepared:
        raise ValueError('Prepared data/metadata differ from decoded original source files.')
    return {'status': 'original bytes and preparation replay verified',
            'sourceManifestDigest': actual['sourceManifestDigest'], 'batchDigest': e.digest(actual),
            'recipe': RECIPE, 'provenanceReview': 'pending; byte/transform agreement does not authenticate license, origin or prior exposure'}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=('prepare', 'verify'))
    parser.add_argument('--manifest', type=Path, required=True)
    parser.add_argument('--batch', type=Path)
    parser.add_argument('--out', type=Path, required=True)
    args = parser.parse_args()
    if args.out.exists(): parser.error('Output already exists.')
    if args.action == 'verify':
        if args.batch is None: parser.error('--batch is required for verification.')
        result = verify(args.manifest, e.read(args.batch))
    else:
        result = prepare(args.manifest)
    raw = json.dumps(result, separators=(',', ':'), allow_nan=False)
    if len(raw.encode()) > 1000000: raise ValueError('Prepared output exceeds 1 MB.')
    with args.out.open('x') as stream: stream.write(raw+'\n')
    print('Wrote', args.out, '— licensing and independent source review remain pending.')


if __name__ == '__main__': main()
