#!/usr/bin/env python3
"""Reproduce the frozen DA-EX-001 ZIP. Python 3.9+ and Node.js 24+; no pip/npm.

Usage: python reproduce.py published-package.zip output-directory
The output directory must not exist. Does not send inquiries to the website.
This is a same-author reproduction aid, not an independent auditor.
"""
import datetime
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import subprocess
import sys
import zipfile

EXPECTED_ZIP = '4dba2dd9b34e0e39bbc859e5ac7f4cd9bb8b42ab12246736edbdafe919de57da'
EXPECTED_MANIFEST = '797f9a0c63a718c324f25f58b12a70c3441675358321b4d3c505b078c14954c0'
EXPECTED_ARTIFACT = '924bcddb97a58f886a7e4bdd8b3165aae6e05a4ca3b8f91184ce559ab4e9a07b'
sha = lambda data: hashlib.sha256(data).hexdigest()
stamp = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()


def require(condition, message):
    if not condition:
        raise RuntimeError(message)


def observations(value):
    """Compare the claimed observations, not wall time or concurrent winner index."""
    if isinstance(value, list):
        return [observations(item) for item in value]
    if isinstance(value, dict):
        result = {key: observations(item) for key, item in value.items() if key != 'createdAt'}
        if value.get('case') == 'concurrent_promises':
            result['statuses'] = sorted(value['statuses'])
        return result
    return value


def main():
    require(len(sys.argv) == 3, 'Usage: python reproduce.py published-package.zip new-output-directory')
    archive = Path(sys.argv[1]).resolve()
    output = Path(sys.argv[2]).resolve()
    require(not output.exists(), 'Choose a new output directory; previous evidence is never overwritten')
    data = archive.read_bytes()
    require(sha(data) == EXPECTED_ZIP, 'ZIP SHA-256 mismatch; no code executed')
    node = shutil.which('node')
    require(node is not None, 'Node.js 24+ must be on PATH')
    started = stamp()
    output.mkdir(parents=True)
    package = output / 'package'
    package.mkdir()
    with zipfile.ZipFile(archive) as zipped:
        members = zipped.infolist()
        names = [member.filename for member in members]
        require(len(names) == len(set(names)), 'Duplicate ZIP entry')
        for member in members:
            p = PurePosixPath(member.filename)
            require(not p.is_absolute() and '..' not in p.parts and '\\' not in member.filename,
                    'Invalid ZIP path')
            require(member.file_size <= 2_000_000, 'Unexpected ZIP entry size')
        zipped.extractall(package)
    manifest_bytes = (package / 'manifest.json').read_bytes()
    require(sha(manifest_bytes) == EXPECTED_MANIFEST, 'Manifest mismatch')
    manifest = json.loads(manifest_bytes)
    require(manifest['artifact_id'] == EXPECTED_ARTIFACT, 'Artifact ID mismatch')
    before = {}
    for item in manifest['files']:
        path = package / item['path']
        blob = path.read_bytes()
        require(sha(blob) == item['sha256'] and len(blob) == item['bytes'], 'File mismatch: ' + item['path'])
        before[item['path']] = sha(blob)
    before['manifest.json'] = sha(manifest_bytes)
    actual = {path.relative_to(package).as_posix() for path in package.rglob('*') if path.is_file()}
    require(actual == set(before), 'Unexpected package files')
    baseline = json.loads((package / 'report.json').read_bytes())
    environment = dict(os.environ)
    # Avoid inherited Node injection options. Temporary SQLite files stay in this run.
    environment.pop('NODE_OPTIONS', None)
    environment.pop('NODE_PATH', None)
    temp = output / 'temporary-databases'
    temp.mkdir()
    for name in ('TMPDIR', 'TMP', 'TEMP'):
        environment[name] = str(temp)
    version = subprocess.run([node, '--version'], capture_output=True, text=True, env=environment, check=True).stdout.strip()
    require(int(version.lstrip('v').split('.')[0]) >= 24, 'Node.js 24+ is required')
    command = ['node', 'verify.mjs', '--report', '../verification.local.json']
    run = subprocess.run([node, *command[1:]], cwd=package, env=environment, capture_output=True, text=True, timeout=60)
    (output / 'stdout.log').write_text(run.stdout, encoding='utf-8')
    (output / 'stderr.log').write_text(run.stderr, encoding='utf-8')
    require(run.returncode == 0, 'Verifier failed; inspect stdout.log and stderr.log')
    report = json.loads((output / 'verification.local.json').read_bytes())
    checks = {
        'zip_matches_published_sha256': sha(data) == EXPECTED_ZIP,
        'manifest_matches_published_sha256': sha(manifest_bytes) == EXPECTED_MANIFEST,
        'same_artifact': report['artifact_id'] == baseline['artifact_id'] == EXPECTED_ARTIFACT,
        'same_source_files': report['artifacts'] == baseline['artifacts'],
        'successful_process': run.returncode == 0,
        'successful_verifier': report['verdict'] == baseline['verdict'] == 'PASS' and report['failure'] is None,
        'same_request_count': report['handler_requests'] == baseline['handler_requests'] == 347,
        'same_scenario_groups': report['groups'] == baseline['groups'],
        'same_visible_observations': observations(report['trace']) == observations(baseline['trace']),
        'same_full_journal_observations': observations(report['journal']) == observations(baseline['journal']),
        'manifest_checks_accounted_for': report['checks'] == baseline['checks'] + len(manifest['files']),
        'original_files_unchanged': all(sha((package / name).read_bytes()) == digest for name, digest in before.items()),
        'no_extra_package_files': actual == {path.relative_to(package).as_posix() for path in package.rglob('*') if path.is_file()},
    }
    result = {
        'schema': 'DA-EX-001/reproduction/v1',
        'case_id': 'DA-EX-001',
        'verdict': 'PASS' if all(checks.values()) else 'FAIL',
        'started_at': started,
        'finished_at': stamp(),
        'execution_class': 'same_author_clean_directory_reproduction',
        'independent_external_audit': False,
        'executor': 'Codex in the same site-authoring workflow',
        'environment_isolation': 'New extraction and temporary database directories on the same execution host; no separate machine or OS network isolation claimed.',
        'package_sha256': EXPECTED_ZIP,
        'manifest_sha256': EXPECTED_MANIFEST,
        'artifact_id': EXPECTED_ARTIFACT,
        'runner_sha256': sha(Path(__file__).read_bytes()),
        'command': command,
        'exit_code': run.returncode,
        'environment': report['environment'],
        'handler_requests': report['handler_requests'],
        'assertions': report['checks'],
        'initial_assertions': baseline['checks'],
        'additional_manifest_checks': len(manifest['files']),
        'preserved_files': len(before),
        'checks': checks,
        'comparison_exclusions': [
            'Execution start/finish times and receipt createdAt values differ between runs. Receipt consistency within each run is still checked by the unmodified verifier.',
            'The winning request index in the concurrent group is not guaranteed. Compare its HTTP-status multiset and stored-row count. All sequential observations retain their order.',
        ],
        'limits': report['limits'],
    }
    (output / 'reproduction.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
    print(json.dumps(result, indent=2))
    require(result['verdict'] == 'PASS', 'Reproduction comparison failed')


if __name__ == '__main__':
    main()
