#!/usr/bin/python3
"""Keep a complete local Drive tree, then expose it at ~/GoogleDrive."""
import datetime
import fcntl
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import uuid

USER_HOME = Path.home()
LOCAL = USER_HOME / '.local/share/gdrive-local'
PUBLIC = USER_HOME / 'GoogleDrive'
STATE = USER_HOME / '.local/state/gdrive-sync'
BACKUPS = USER_HOME / '.local/share/gdrive-sync-backups'
READY = STATE / 'baseline-ready'
INITIALIZING = STATE / 'initializing'
CHECK_NAME = '.gdrive-sync-access'
FILTERS = USER_HOME / '.config/rclone/gdrive-sync-filters.conf'
BASE = ['/usr/bin/rclone', '--config', str(USER_HOME / '.config/rclone/rclone.conf')]
COMMON = ['--fast-list', '--create-empty-src-dirs', '--transfers', '8', '--checkers', '16',
          '--stats', '30s', '--stats-one-line', '--stats-log-level', 'NOTICE',
          '--log-level', 'INFO', '--drive-use-trash=true']
BISYNC = ['--filters-file', str(FILTERS), '--check-access', '--check-filename', CHECK_NAME, '--max-delete', '10',
          '--resilient', '--recover', '--max-lock', '5m',
          '--conflict-resolve', 'none', '--conflict-loser', 'num',
          '--compare', 'size,modtime', '--workdir', str(STATE / 'bisync')]

def run(args, **kwargs):
    return subprocess.run([str(a) for a in args], check=True, **kwargs)

def status(phase, **extra):
    path = STATE / 'status.json'
    previous = json.loads(path.read_text()) if path.exists() else {}
    previous.update(phase=phase, updated=datetime.datetime.now().astimezone().isoformat(), **extra)
    tmp = path.with_suffix('.tmp')
    tmp.write_text(json.dumps(previous, indent=2))
    tmp.replace(path)
    print('Google Drive:', phase, flush=True)

def expose_local():
    if PUBLIC.is_symlink() and PUBLIC.resolve() == LOCAL:
        return
    if PUBLIC.exists() or PUBLIC.is_symlink():
        raise RuntimeError('GoogleDrive already exists; refusing to replace it.')
    PUBLIC.symlink_to(LOCAL, target_is_directory=True)

def sync():
    if not LOCAL.is_dir() or LOCAL.is_symlink():
        raise RuntimeError('The local sync directory is missing or unexpectedly linked.')
    if shutil.disk_usage(LOCAL).free < 15 * 1024**3:
        raise RuntimeError('Less than 15 GiB free; sync paused to protect local disk space.')
    stamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f')
    backup = BACKUPS / stamp
    command = BASE + ['bisync', 'gdrive:', str(LOCAL)] + COMMON + BISYNC + ['--backup-dir2', str(backup)]
    if not READY.exists():
        if PUBLIC.exists() or PUBLIC.is_symlink():
            raise RuntimeError('GoogleDrive exists without a baseline; inspect before recovery.')
        if not INITIALIZING.exists():
            if any(LOCAL.iterdir()):
                raise RuntimeError('First setup requires an empty local sync directory.')
            INITIALIZING.write_text('Initial download in progress. Do not edit the staging folder.\n')
        status('initial-download')
        # systemd joins an already-running initial download; after reboot it resumes
        # by copying only missing/changed files. This job never modifies Drive.
        run(['systemctl', '--user', 'start', 'gdrive-initial-download.service'])
        check = STATE / CHECK_NAME
        if not check.exists():
            check.write_text(str(uuid.uuid4()) + '\n')
        run(BASE + ['copyto', str(check), 'gdrive:' + CHECK_NAME, '--ignore-existing'])
        # The staging folder is not exposed yet. Refresh its remote snapshot and
        # archive any stale staged files rather than upload them during resync.
        status('initial-verification')
        run(BASE + ['sync', 'gdrive:', str(LOCAL), '--backup-dir', str(backup), '--filter-from', str(FILTERS)] + COMMON)
        with (STATE / 'initial-dry-run.log').open('w') as log:
            run(command + ['--resync', '--dry-run'], stdout=log, stderr=subprocess.STDOUT)
        status('initial-baseline')
        run(command + ['--resync'])
        READY.write_text(datetime.datetime.now().astimezone().isoformat() + '\n')
        INITIALIZING.unlink(missing_ok=True)
    else:
        if not (LOCAL / CHECK_NAME).is_file():
            raise RuntimeError('Local access-check file is missing; refusing to sync.')
        status('syncing')
        run(command)
    status('activating-local-folder')
    expose_local()
    status('up-to-date', last_success=datetime.datetime.now().astimezone().isoformat(), error=None)

def main():
    os.umask(0o077)
    STATE.mkdir(parents=True, exist_ok=True, mode=0o700)
    with (STATE / 'runner.lock').open('w') as lock:
        try:
            fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            return
        try:
            sync()
        except Exception as error:
            status('error', error=str(error))
            print(str(error), file=sys.stderr, flush=True)
            raise SystemExit(1)

if __name__ == '__main__':
    main()
