#!/usr/bin/env python3
"""Read-only Garry update diagnosis. No apply, mounts, repairs or media writes."""
import hashlib
import importlib.machinery
import importlib.util
from pathlib import Path
import stat
import sys
import tempfile

CORE = Path('/usr/local/bin/garry-usb-update-core')
MEDIA = Path('/run/live/medium')


def load_core():
    for path in (CORE, *CORE.parents):
        item = path.lstat()
        if stat.S_ISLNK(item.st_mode) or item.st_uid != 0 or item.st_mode & 0o022:
            raise ValueError('The installed updater is not protected by normal system ownership.')
    if not CORE.is_file():
        raise ValueError('Garry Updates must be installed first.')
    loader = importlib.machinery.SourceFileLoader('garry_readonly_core', str(CORE))
    spec = importlib.util.spec_from_loader(loader.name, loader)
    core = importlib.util.module_from_spec(spec)
    loader.exec_module(core)
    return core


def clean(value):
    return ''.join(char if char.isprintable() else ' ' for char in str(value))[:700]


def describe_boot_files():
    print('\nBOOT FILES (read-only):')
    for name in ('.disk/info', 'sha256sum.txt', 'sha256sum', 'boot/grub/grub.cfg',
                 'isolinux/live.cfg', 'syslinux/live.cfg', 'live/filesystem.squashfs'):
        path = MEDIA / name
        if path.is_symlink():
            print(name + ': symbolic link (not read)')
        elif not path.is_file():
            print(name + ': missing')
        else:
            print(name + ': present, ' + str(path.stat().st_size) + ' bytes')
    for name in ('isolinux/live.cfg', 'syslinux/live.cfg', 'boot/grub/grub.cfg'):
        path = MEDIA / name
        if path.is_file() and not path.is_symlink() and path.stat().st_size < 65536:
            print(name + ' SHA256: ' + hashlib.sha256(path.read_bytes()).hexdigest())


def main():
    print('GARRY UPDATE CHECK — READ ONLY', flush=True)
    print('This does not install, repair, format or restart. Boot and personal files stay unchanged.', flush=True)
    try:
        core = load_core()
        print('Verifying the signed update download...', flush=True)
        manifest, raw, signature = core.metadata()
        payload = core.fetch(manifest['payload']['url'], manifest['payload']['bytes'])
        core.verify_payload(payload, manifest)
        print('Signature and payload verified. Release: ' + manifest['release'], flush=True)
        with tempfile.TemporaryDirectory(prefix='garry-readonly-check-') as folder:
            folder = Path(folder)
            core.safe_zip(payload, folder)
            try:
                media = core.inspect_media()
                print('USB/persistence checks: PASS (' + media['fstype'] + ')', flush=True)
            except Exception as exc:
                print('MEDIA CHECK: ' + clean(type(exc).__name__ + ': ' + str(exc)), flush=True)
            installer = core.load_installer(folder)
            print('Checking boot files and image. This can take a minute...', flush=True)
            try:
                plan = installer.prepare(MEDIA, folder)
                print('INSTALLER PREFLIGHT: PASS. Nothing installed.', flush=True)
                print('Files proposed for a future update: ' + ', '.join(plan['after']), flush=True)
            except Exception as exc:
                print('EXACT PREFLIGHT ERROR: ' + clean(type(exc).__name__ + ': ' + str(exc)), flush=True)
            describe_boot_files()
    except Exception as exc:
        print('CHECK COULD NOT FINISH: ' + clean(type(exc).__name__ + ': ' + str(exc)), flush=True)
        return 1
    finally:
        print('\nCHECK FINISHED — no update applied; boot and personal files unchanged.', flush=True)
    return 0


if __name__ == '__main__':
    sys.exit(main())
