import hashlib
import json
import os
from pathlib import Path
import shutil
import stat
import subprocess
import sys
import time

ROOT = Path(__file__).resolve().parent.parent
CACHES = [Path('/root/.vscode-server/data/CachedExtensionVSIXs'), Path('/root/.npm/_cacache')]
GIB = 1024**3


def write(name, value):
    path = ROOT / 'reports' / name
    with path.open('x') as stream:
        os.chmod(path, 0o600)
        json.dump(value, stream, indent=2, ensure_ascii=False)


def digest(path):
    h = hashlib.sha256()
    with Path(path).open('rb') as stream:
        for block in iter(lambda: stream.read(1024**2), b''):
            h.update(block)
    return h.hexdigest()


def command(args):
    return subprocess.check_output(args, timeout=60)


def snapshot():
    ids = command(['docker', 'ps', '-aq']).decode().split()
    containers = json.loads(command(['docker', 'inspect', *ids]))
    definition = Path('/etc/contextra-maintenance/server.json')
    d = json.loads(definition.read_bytes())
    state = Path(d['paths']['runtime']) / 'state'
    files = [definition, Path('/etc/docker/daemon.json'), Path('/etc/fstab')]
    files += [state / n for n in ('active.json', 'site.json', 'protected-bindings.json', 'input-baseline.json')]
    files += [Path('/workspace/contextra-runtime-config/web-start-candidate-20260918/medium.json'),
              Path('/workspace/contextra-runtime-config/configs/streams.yaml'),
              Path('/data/contextra/runtime-state/streams_runtime.yaml')]
    return dict(at=time.time(), free={p: shutil.disk_usage(p).free for p in ('/', '/data')},
                files={str(p): digest(p) for p in files if p.is_file()},
                transaction=json.loads((state/'active.json').read_bytes()).get('transaction'),
                containers=[dict(id=c['Id'], name=c['Name'].lstrip('/'), image=c['Image'],
                                 running=c['State']['Running'], started=c['State']['StartedAt'],
                                 health=c['State'].get('Health', {}).get('Status'),
                                 config_sha256=hashlib.sha256(json.dumps(
                                     [c['Config'], c['HostConfig'], sorted(c['Mounts'], key=lambda m: m['Destination'])], sort_keys=True).encode()).hexdigest())
                            for c in containers])


def active_users():
    users = []
    for process in Path('/proc').iterdir():
        if not process.name.isdigit() or int(process.name) == os.getpid():
            continue
        try:
            argv = (process / 'cmdline').read_bytes().split(b'\0')
            busy = any(b'npm-cli.js' in a or a in (b'npm', b'npx', b'--install-extension') for a in argv)
            for fd in (process/'fd').iterdir():
                try:
                    target = os.readlink(fd)
                except OSError:
                    continue
                if any(target == str(p) or target.startswith(str(p) + '/') for p in CACHES):
                    busy = True
            if busy:
                users.append(dict(pid=int(process.name), name=(process/'comm').read_text().strip()))
        except (FileNotFoundError, PermissionError, ProcessLookupError):
            continue
    return users


def inventory(root):
    assert root.is_dir() and not any(p.is_symlink() for p in [root, *root.parents])
    result = []
    for p in sorted(root.rglob('*')):
        s = p.lstat()
        assert not p.is_symlink() and (stat.S_ISREG(s.st_mode) or stat.S_ISDIR(s.st_mode)), 'Special cache path'
        if not p.is_file():
            continue
        assert time.time() - max(s.st_mtime, s.st_ctime) > 3600, 'Recently changed cache; preserve'
        result.append(dict(path=str(p), size=s.st_size, blocks=s.st_blocks, inode=s.st_ino,
                           device=s.st_dev, mtime_ns=s.st_mtime_ns, ctime_ns=s.st_ctime_ns,
                           sha256=digest(p)))
    return result


def main():
    action = sys.argv[1]
    if action == 'inspect':
        before = snapshot()
        write('before.json', before)
        plan = dict(at=time.time(), active_users=active_users(), caches={str(p): inventory(p) for p in CACHES})
        write('cleanup-plan.json', plan)
        print(json.dumps(dict(root_free_gib=before['free']['/']/GIB, active_users=plan['active_users'],
                              candidates_gib={p: sum(x['blocks']*512 for x in rows)/GIB for p, rows in plan['caches'].items()})))
    elif action == 'cleanup':
        plan = json.loads((ROOT/'reports/cleanup-plan.json').read_bytes())
        assert not plan['active_users'] and not active_users(), 'Cache installer/open file present'
        assert time.time() - plan['at'] < 1800, 'Refresh stale plan'
        # Recheck all files before the first deletion; no installed extension directory is touched.
        for root in CACHES:
            assert inventory(root) == plan['caches'][str(root)], 'Cache changed since inspection'
        before = shutil.disk_usage('/').free
        removed = []
        for root in CACHES:
            assert not active_users(), 'New cache user; stop before this directory'
            for row in plan['caches'][str(root)]:
                p = Path(row['path']); s = p.lstat()
                assert (s.st_ino, s.st_size, s.st_mtime_ns, s.st_ctime_ns) == (
                    row['inode'], row['size'], row['mtime_ns'], row['ctime_ns'])
                p.unlink()
                removed.append(row['path'])
            if shutil.disk_usage('/').free >= int(34.18 * GIB):
                break
        after = snapshot()
        write('cleanup-result.json', dict(removed=removed, before_free=before, after_free=after['free']['/'],
                                          reclaimed=after['free']['/']-before, unchanged_services=
                                          after['containers']==json.loads((ROOT/'reports/before.json').read_bytes())['containers']))
        print(json.dumps(dict(removed_files=len(removed), reclaimed_gib=(after['free']['/']-before)/GIB,
                              root_free_gib=after['free']['/']/GIB)))
    elif action == 'snapshot':
        result = snapshot(); write(sys.argv[2], result)
        print(json.dumps(dict(containers=len(result['containers']), root_free_gib=result['free']['/']/GIB)))
    else:
        raise ValueError('Unsupported action')


if __name__ == '__main__':
    main()
