import json
import os
from pathlib import Path
import shutil
import subprocess
import time
import psutil

ROOT = Path(r'C:\Users\decid\Documents\projects\spt-core')
PROOF = ROOT / '.spt/preserved/304-handoff/target-reclaim-20260914'
NAMES = ['300-input-acceptance', '304-product', '304-w2-bootstrap-tcp', '49-267-obs', '299-instr']
TARGETS = [ROOT / '.worktrees' / name / 'target' for name in NAMES]
PROTECTED = ROOT / '.worktrees/asm-304-v3/target'
scan = json.loads((PROOF / 'inbound-and-sizes.json').read_text())
assert not scan['errors']
assert all(not row['inbound'] and not row['internal_reparse'] for row in scan['targets'].values())

def norm(path):
    return os.path.normcase(os.path.abspath(path))

def inside(path, root):
    return path == root or path.startswith(root + os.sep)

def tree_measure(path):
    result = {'bytes': 0, 'files': 0, 'directories': 0}
    stack = [path]
    while stack:
        directory = stack.pop()
        st = os.lstat(directory)
        if st.st_file_attributes & 0x400:
            raise RuntimeError('REPARSE_REFUSED ' + str(directory))
        result['directories'] += 1
        with os.scandir(directory) as entries:
            for entry in entries:
                st = entry.stat(follow_symlinks=False)
                if st.st_file_attributes & 0x400:
                    raise RuntimeError('REPARSE_REFUSED ' + entry.path)
                if entry.is_dir(follow_symlinks=False):
                    stack.append(entry.path)
                else:
                    result['bytes'] += st.st_size
                    result['files'] += 1
    return result

def active_users():
    found = []
    for proc in psutil.process_iter(['pid', 'name', 'exe', 'cmdline', 'create_time'], ad_value=None):
        if proc.pid == os.getpid():
            continue
        try:
            info = proc.info
            texts = [info['exe'] or '', ' '.join(info['cmdline'] or [])]
            if (info['name'] or '').lower() in ('cargo.exe', 'rustc.exe', 'cargo-nextest.exe', 'rust-analyzer.exe'):
                cwd = proc.cwd()
                texts.append(cwd)
                target_env = proc.environ().get('CARGO_TARGET_DIR')
                if target_env:
                    texts.append(os.path.abspath(os.path.join(cwd, target_env)))
            if any(str(ROOT / '.worktrees' / name).lower() in text.lower() for name in NAMES for text in texts):
                found.append(info)
        except psutil.NoSuchProcess:
            continue
        except psutil.AccessDenied as error:
            if (proc.info['name'] or '').lower() in ('cargo.exe', 'rustc.exe', 'cargo-nextest.exe', 'rust-analyzer.exe'):
                raise RuntimeError('PRODUCER_UNPROVEN ' + str(proc.pid)) from error
    return found

report = {'authority': 'doyle V3FOQWW3/QYXIAIA7', 'started_at': time.time(),
          'initial_free_bytes': shutil.disk_usage(ROOT).free, 'targets': [],
          'protected': str(PROTECTED), 'protected_inode_before': PROTECTED.stat().st_ino,
          'inbound_sweep_scope': scan['scan_root'],
          'attribution_limit': 'Volume free-space deltas include concurrent activity, including doyle asm-304 cleanup; logical target bytes are measured separately.'}

def save():
    (PROOF / 'reap-receipt.json').write_text(json.dumps(report, indent=2))

save()
for name, target in zip(NAMES, TARGETS):
    assert target.parent.parent == ROOT / '.worktrees'
    assert target.name == 'target' and target != PROTECTED
    git_file = target.parent / '.git'
    git_before = git_file.read_bytes()
    source_inode = target.parent.stat().st_ino
    row = {'lane': name, 'target': str(target), 'started_at': time.time()}
    report['targets'].append(row)
    try:
        env = dict(os.environ, RECLAIM_TARGET=str(target))
        checked = subprocess.run(['pwsh', '-NoProfile', '-Command',
            "$ErrorActionPreference='Stop'; $x=Get-Item -Force -LiteralPath $env:RECLAIM_TARGET; if(($x.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0){throw 'OUTBOUND_REPARSE_REFUSED'}; $x | Select-Object FullName,Attributes,LinkType,Target | ConvertTo-Json"],
            env=env, capture_output=True, text=True, check=True)
        row['outbound_fresh'] = json.loads(checked.stdout)
        row['before'] = tree_measure(target)
        users = active_users()
        row['active_users_before'] = users
        if users:
            raise RuntimeError('ACTIVE_USERS_REFUSED')
        row['free_bytes_before'] = shutil.disk_usage(ROOT).free
        save()
        shutil.rmtree(target)
        row['free_bytes_after'] = shutil.disk_usage(ROOT).free
        row['free_delta_bytes'] = row['free_bytes_after'] - row['free_bytes_before']
        row['target_absent'] = not os.path.lexists(target)
        row['source_preserved'] = target.parent.stat().st_ino == source_inode and git_file.read_bytes() == git_before
        assert row['target_absent'] and row['source_preserved']
        row['status'] = 'REMOVED'
    except Exception as error:
        row['status'] = 'REFUSED_OR_PARTIAL'
        row['error'] = repr(error)
        row['free_bytes_after'] = shutil.disk_usage(ROOT).free
        row['target_absent'] = not os.path.lexists(target)
    row['finished_at'] = time.time()
    save()
    print(json.dumps(row), flush=True)
report['final_free_bytes'] = shutil.disk_usage(ROOT).free
report['volume_free_delta_bytes'] = report['final_free_bytes'] - report['initial_free_bytes']
report['removed_logical_bytes'] = sum(row['before']['bytes'] for row in report['targets'] if row['status'] == 'REMOVED')
report['protected_inode_after'] = PROTECTED.stat().st_ino
report['protected_release_exists'] = (PROTECTED / 'release/spt.exe').is_file()
report['finished_at'] = time.time()
assert report['protected_inode_before'] == report['protected_inode_after']
save()
print('FINAL ' + json.dumps(report), flush=True)
raise SystemExit(0 if all(row['status'] == 'REMOVED' for row in report['targets']) else 1)
