#!/usr/bin/python3
"""Set the Garry session clock after two HTTPS time sources agree."""
import datetime
import email.utils
import json
import os
from pathlib import Path
import ssl
import stat
import subprocess
import sys
import time
import urllib.request

URL = 'https://garryhq.duckdns.org/garry-usb/updates/stable.json'
SOURCES = ((URL, 200), ('https://www.google.com/generate_204', 204))
DATE = Path('/usr/bin/date')


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, request, fp, code, message, headers, newurl):
        raise ValueError('Unexpected time-source redirect; clock unchanged.')


def read_server_time(url, expected_status):
    opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect(),
        urllib.request.HTTPSHandler(context=ssl.create_default_context()))
    request = urllib.request.Request(url, method='HEAD', headers={'Cache-Control': 'no-cache', 'User-Agent': 'Garry-Clock-Repair/1'})
    started = time.monotonic()
    with opener.open(request, timeout=15) as response:
        if response.status != expected_status or response.geturl() != url:
            raise ValueError('Unexpected time-source response; clock unchanged.')
        headers = response.headers.get_all('Date') or []
        if len(headers) != 1:
            raise ValueError('Expected one server date; clock unchanged.')
        value = email.utils.parsedate_to_datetime(headers[0])
    elapsed = time.monotonic() - started
    if value.tzinfo is None or value.utcoffset() != datetime.timedelta(0) or elapsed > 15:
        raise ValueError('Invalid server UTC date or slow response; clock unchanged.')
    stamp = int(value.timestamp() + elapsed / 2)
    if abs(stamp - time.time()) > 7 * 86400:
        raise ValueError('The clock differs by over seven days; review the date manually first.')
    return stamp


def server_time():
    readings = [read_server_time(url, status) for url, status in SOURCES]
    if abs(readings[1] - readings[0]) > 60:
        raise ValueError('The two time sources disagree; clock unchanged.')
    return readings[1]


def apply_clock(stamp):
    if sys.platform != 'linux' or os.geteuid() != 0 or not sys.flags.isolated:
        raise ValueError('Run: sudo python3 -I ~/Downloads/garry-clock-fix.py')
    for path in (DATE, *DATE.parents):
        info = path.lstat()
        expected = stat.S_ISREG if path == DATE else stat.S_ISDIR
        if not expected(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
            raise ValueError('The system clock tool is not a protected system file.')
    subprocess.run([str(DATE), '-u', '--set=@' + str(stamp)], check=True, timeout=10,
        stdin=subprocess.DEVNULL, env={'PATH': '/usr/bin:/bin', 'LC_ALL': 'C'})
    if abs(time.time() - stamp) > 15:
        raise RuntimeError('Clock correction could not be confirmed.')


def main():
    try:
        if '--check' in sys.argv:
            stamp = server_time()
            print(json.dumps({'server_utc': datetime.datetime.fromtimestamp(stamp, datetime.timezone.utc).isoformat(),
                'offset_seconds': round(stamp-time.time()), 'clock_changed': False}))
            return 0
        if len(sys.argv) != 1:
            raise ValueError('Unexpected argument. Use the documented repair command.')
        stamp = server_time()
        apply_clock(stamp)
        print('Garry clock corrected and verified. No phone was erased.')
        print('Now return to Phone & Tablet Reset > Setup & help > Setup tools.')
        print('When setup succeeds, reopen the app and continue with device detection and erase confirmation.')
    except Exception as exc:
        print('CLOCK REPAIR NOT CONFIRMED: ' + ''.join(c for c in str(exc) if c.isprintable())[:500])
        print('Show this message. Do not disable package signature verification.')
        return 1
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
