#!/usr/bin/env python3
"""BCITFI55: reuse the measured classification protocol for the released S3 target."""
import datetime
import hashlib
import json
import os
from pathlib import Path
import shutil
import stat
import subprocess
import sys

ROOT = Path('/home/reavus/projects/spt-core/spt-core')
PROOF = Path(__file__).resolve().parent
TREE = ROOT / '.worktrees/consumer-linux-49a08a07'
TARGET = TREE / 'target'
TARGETS = [TARGET]
HISTORY = ROOT / '.spt/preserved/304-handoff/successor-linux-daemon'
ACCEPTED = HISTORY / 'corrected-workspace'

def utc():
    return datetime.datetime.now(datetime.timezone.utc).isoformat()

def digest(path):
    with path.open('rb') as stream:
        return hashlib.file_digest(stream, 'sha256').hexdigest()

def save(name, value):
    (PROOF / name).write_text(json.dumps(value, indent=2) + '\n')

def source_snapshot(tree):
    files = subprocess.check_output(['git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd=tree).split(b'\0')
    values = {}
    for name in files:
        if not name:
            continue
        relative = name.decode()
        path = tree / relative
        info = path.lstat()
        values[relative] = {'mode': info.st_mode, 'content': os.readlink(path) if path.is_symlink() else digest(path) if path.is_file() else None}
    return {'inode': tree.stat().st_ino, 'git_file_sha256': digest(tree / '.git'), 'files': values}

def touches(value):
    return any(value == str(target) or value.startswith(str(target) + '/') for target in TARGETS)

def census():
    active, references, limits = [], [], []
    for proc in Path('/proc').iterdir():
        if not proc.name.isdigit():
            continue
        try:
            name = (proc / 'comm').read_text().strip()
            command = (proc / 'cmdline').read_bytes().replace(b'\0', b' ').decode(errors='replace')
            exe = None
            try:
                exe = os.readlink(proc / 'exe')
            except PermissionError:
                limits.append({'pid': int(proc.name), 'field': 'exe'})
            except FileNotFoundError:
                pass
            if any(word in name for word in ('cargo', 'rustc', 'nextest', 'Runner.Worker', 'xtask')) or exe and '/deps/' in exe:
                active.append({'pid': int(proc.name), 'name': name, 'exe': exe, 'command': command})
            if exe and touches(exe) or any(str(target) in command for target in TARGETS):
                references.append({'pid': int(proc.name), 'kind': 'exe-or-command', 'exe': exe, 'command': command})
            for field in ('cwd', 'root'):
                try:
                    value = os.readlink(proc / field)
                    if touches(value):
                        references.append({'pid': int(proc.name), 'kind': field, 'path': value})
                except PermissionError:
                    limits.append({'pid': int(proc.name), 'field': field})
                except FileNotFoundError:
                    pass
            try:
                for fd in (proc / 'fd').iterdir():
                    try:
                        value = os.readlink(fd)
                        if touches(value):
                            references.append({'pid': int(proc.name), 'kind': 'fd', 'path': value})
                    except FileNotFoundError:
                        pass
            except PermissionError:
                limits.append({'pid': int(proc.name), 'field': 'fd'})
            try:
                maps = (proc / 'maps').read_text()
                if any(str(target) in maps for target in TARGETS):
                    references.append({'pid': int(proc.name), 'kind': 'maps'})
            except PermissionError:
                limits.append({'pid': int(proc.name), 'field': 'maps'})
        except (FileNotFoundError, ProcessLookupError):
            continue
        except PermissionError:
            limits.append({'pid': int(proc.name), 'field': 'process'})
    protected = []
    for pid in sorted({row['pid'] for row in limits}):
        try:
            proc = Path('/proc') / str(pid)
            status = (proc / 'status').read_text()
            uid = int(next(line for line in status.splitlines() if line.startswith('Uid:')).split()[1])
            if uid == os.getuid():
                protected.append({'pid': pid, 'name': (proc / 'comm').read_text().strip(),
                                  'command': (proc / 'cmdline').read_bytes().replace(b'\0', b' ').decode(errors='replace')})
        except FileNotFoundError:
            pass
    assert all(row['name'] in ('(sd-pam)', 'fusermount3', 'ssh-agent') or
               row['name'] == 'sshd' and row['command'].startswith('sshd: reavus@')
               for row in protected), protected
    return {'utc': utc(), 'active': active, 'target_references': references,
            'permission_limits': limits, 'protected_same_uid_services': protected}

def classify():
    info = TARGET.lstat()
    assert stat.S_ISDIR(info.st_mode) and not TARGET.is_symlink() and TARGET.resolve() == TARGET
    owner = json.loads((TARGET / 'POOL-OWNER.json').read_text())
    assert owner == {'owner_tree': str(TREE), 'written_by': 'spt-poolguard'}, owner
    creation = json.loads((PROOF / 'creation.json').read_text())
    assert creation['target_existed'] is False
    assert any(str(TREE) in row['command'] and row['exit'] == 0 for row in creation['results'])
    old_hashes = json.loads((PROOF / 'prior-hash-verification.json').read_text())['remote_sha256']
    for relative, expected in old_hashes.items():
        assert digest(ACCEPTED / relative) == expected, ('prior proof changed', relative)
    proof_hashes = {str(path.relative_to(HISTORY)): digest(path) for path in HISTORY.rglob('*') if path.is_file()}
    receipt = json.loads((ACCEPTED / 'receipt.json').read_text())
    release = json.loads((ACCEPTED / 'pool-release-receipt.json').read_text())
    claim = json.loads((ACCEPTED / 'pool-claim-receipt.json').read_text())
    assert receipt['sha'] == '6c89e8f7545db54772ea5686b4d59718573d1f08'
    assert all(row['exit'] == 0 for row in receipt['phases'])
    assert release['exit'] == claim['exit'] == 0 and str(TARGET) in claim['command']
    for label in ('phase-daemon', 'phase-daemon-inventory', 'pool-claim', 'pool-release', 'capture-player-build'):
        native = json.loads((ACCEPTED / (label + '-receipt.json')).read_text())
        assert native['exit'] == 0 and digest(ACCEPTED / (label + '.log')) == native['log_sha256']
    source = source_snapshot(TREE)
    inbound, errors = [], []
    for directory, dirs, files in os.walk('/home/reavus', followlinks=False, onerror=lambda e: errors.append(str(e))):
        for name in dirs + files:
            path = Path(directory) / name
            if not path.is_symlink() or path.is_relative_to(TARGET):
                continue
            resolved = os.path.realpath(path)
            if resolved == str(TARGET) or resolved.startswith(str(TARGET) + '/'):
                inbound.append({'path': str(path), 'resolved': resolved})
    state = census()
    report = {'authority': 'BCITFI55', 'utc': utc(), 'target': str(TARGET), 'outbound': 'real-directory',
              'inode': info.st_ino, 'device': info.st_dev, 'owner': owner, 'source': source,
              'creation': creation, 'creation_sha256': digest(PROOF / 'creation.json'),
              'prior_manifest_files_verified': len(old_hashes), 'proof_root': str(HISTORY), 'proof_hashes': proof_hashes,
              'release': release, 'inbound_scope': '/home/reavus', 'inbound': inbound, 'scan_errors': errors,
              'census': state, 'allocated_bytes': int(subprocess.check_output(['du', '-s', '-B1', str(TARGET)], text=True).split()[0]),
              'free_bytes': shutil.disk_usage(ROOT).free}
    save('classification-final.json' if '--apply' in sys.argv else 'classification.json', report)
    print(json.dumps({k: v for k, v in report.items() if k not in ('source', 'proof_hashes', 'creation', 'release', 'census')}), flush=True)
    print(json.dumps({'active': state['active'], 'target_references': state['target_references'], 'proof_files': len(proof_hashes)}), flush=True)
    assert not inbound and not errors and not state['active'] and not state['target_references']
    return report

report = classify()
if '--apply' in sys.argv:
    assert not (PROOF / 'reclaim.json').exists()
    prior = json.loads((PROOF / 'classification.json').read_text())
    assert all(prior[key] == report[key] for key in ('target', 'inode', 'device', 'owner', 'source', 'proof_hashes'))
    assert TARGET == ROOT / '.worktrees/consumer-linux-49a08a07/target'
    assert TARGET.resolve() == TARGET and not TARGET.is_symlink() and TARGET.lstat().st_ino == report['inode']
    assert json.loads((TARGET / 'POOL-OWNER.json').read_text()) == report['owner']
    current = census()
    assert not current['active'] and not current['target_references'], current
    result = {'authority': 'BCITFI55', 'target': str(TARGET), 'start_utc': utc(),
              'allocated_bytes_before': report['allocated_bytes'], 'free_bytes_before': shutil.disk_usage(ROOT).free,
              'classification_sha256': digest(PROOF / 'classification-final.json')}
    save('reclaim-before.json', result)
    shutil.rmtree(TARGET)
    os.sync()
    assert not TARGET.exists() and source_snapshot(TREE) == report['source']
    assert all(digest(HISTORY / name) == value for name, value in report['proof_hashes'].items())
    final = census()
    assert not final['active'] and not final['target_references'], final
    result.update(end_utc=utc(), free_bytes_after=shutil.disk_usage(ROOT).free, target_exists=False,
                  source_unchanged=True, proofs_unchanged=True, final_census=final)
    result['free_delta'] = result['free_bytes_after'] - result['free_bytes_before']
    save('reclaim.json', result)
    print('RECLAIM_DONE ' + json.dumps({k: v for k, v in result.items() if k != 'final_census'}), flush=True)
