#!/usr/bin/env python3
"""Set the system clock from an HTTPS Date: header.

Used when NTP (UDP/123) is blocked. Stdlib only. Safe to run every 12h:
skips when NTP is already synchronized, or when the skew is under 1s.
"""
from __future__ import annotations

import argparse
import os
import ssl
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path

URLS = (
    "https://www.google.com",
    "https://www.cloudflare.com",
    "https://www.microsoft.com",
)
USER_AGENT = "DyzineFrame-http-date-sync/1"
SKEW_SKIP_SEC = 1.0


def log(msg: str) -> None:
    line = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ ") + msg
    print(line, flush=True)
    for candidate in (
        Path.home() / "DyzineFrame" / "log" / "http_date_sync.log",
        Path(r"C:\DyzineFrame\log\http_date_sync.log"),
    ):
        try:
            candidate.parent.mkdir(parents=True, exist_ok=True)
            with candidate.open("a", encoding="utf-8") as fh:
                fh.write(line + "\n")
            break
        except OSError:
            continue


def ntp_synchronized_linux() -> bool:
    try:
        out = subprocess.run(
            ["timedatectl", "show", "-p", "NTPSynchronized", "--value"],
            capture_output=True,
            text=True,
            timeout=5,
            check=False,
        )
    except (OSError, subprocess.TimeoutExpired):
        return False
    return (out.stdout or "").strip().lower() == "yes"


def ntp_synchronized_windows() -> bool:
    try:
        out = subprocess.run(
            ["w32tm", "/query", "/status"],
            capture_output=True,
            text=True,
            timeout=8,
            check=False,
        )
    except (OSError, subprocess.TimeoutExpired):
        return False
    text = (out.stdout or "") + (out.stderr or "")
    lower = text.lower()
    if "local cmos clock" in lower or "free-running" in lower:
        return False
    return "source:" in lower and "ntp" in lower


def fetch_http_date() -> datetime:
    ctx = ssl.create_default_context()
    last_err: Exception | None = None
    for url in URLS:
        for method in ("HEAD", "GET"):
            req = urllib.request.Request(
                url,
                method=method,
                headers={"User-Agent": USER_AGENT},
            )
            try:
                with urllib.request.urlopen(req, timeout=10, context=ctx) as resp:
                    raw = resp.headers.get("Date") or resp.headers.get("date")
                    if not raw:
                        last_err = RuntimeError(f"no Date header from {url}")
                        continue
                    dt = parsedate_to_datetime(raw)
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=timezone.utc)
                    return dt.astimezone(timezone.utc)
            except (urllib.error.URLError, ssl.SSLError, TimeoutError, OSError, ValueError) as exc:
                last_err = exc
                continue
    raise RuntimeError(f"HTTPS Date fetch failed: {last_err}")


def set_linux(dt: datetime) -> None:
    stamp = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")

    def timedatectl(*args: str) -> None:
        cmd = ["timedatectl", *args]
        if os.geteuid() != 0:
            cmd = ["sudo", "-n", *cmd]
        subprocess.run(cmd, check=True, timeout=15)

    try:
        timedatectl("set-ntp", "false")
        timedatectl("set-time", stamp)
    finally:
        try:
            timedatectl("set-ntp", "true")
        except subprocess.CalledProcessError as exc:
            log(f"WARN re-enable NTP failed: {exc}")
    save = ["fake-hwclock", "save"]
    if os.geteuid() != 0:
        save = ["sudo", "-n", *save]
    subprocess.run(save, check=False, timeout=10)


def set_windows(dt: datetime) -> None:
    import ctypes
    from ctypes import wintypes

    class SYSTEMTIME(ctypes.Structure):
        _fields_ = [
            ("wYear", wintypes.WORD),
            ("wMonth", wintypes.WORD),
            ("wDayOfWeek", wintypes.WORD),
            ("wDay", wintypes.WORD),
            ("wHour", wintypes.WORD),
            ("wMinute", wintypes.WORD),
            ("wSecond", wintypes.WORD),
            ("wMilliseconds", wintypes.WORD),
        ]

    utc = dt.astimezone(timezone.utc)
    st = SYSTEMTIME(
        utc.year,
        utc.month,
        utc.weekday(),
        utc.day,
        utc.hour,
        utc.minute,
        utc.second,
        utc.microsecond // 1000,
    )
    if not ctypes.windll.kernel32.SetSystemTime(ctypes.byref(st)):
        raise OSError("SetSystemTime failed (need Administrator / SYSTEM)")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--apply", action="store_true", help="set the system clock")
    parser.add_argument("--dry-run", action="store_true", help="print only (default if not --apply)")
    args = parser.parse_args()
    apply = bool(args.apply) and not args.dry_run

    if sys.platform.startswith("linux") and ntp_synchronized_linux():
        log("skip: NTP already synchronized")
        return 0
    if sys.platform == "win32" and ntp_synchronized_windows():
        log("skip: Windows Time already NTP-synced")
        return 0

    try:
        http_dt = fetch_http_date()
    except Exception as exc:
        log(f"FAIL fetch: {exc}")
        return 1

    now = datetime.now(timezone.utc)
    skew = (http_dt - now).total_seconds()
    log(f"http_date={http_dt.strftime('%Y-%m-%dT%H:%M:%SZ')} local_utc={now.strftime('%Y-%m-%dT%H:%M:%SZ')} skew_s={skew:.1f}")

    if abs(skew) < SKEW_SKIP_SEC:
        log("skip: already within 1s")
        return 0
    if not apply:
        log("dry-run: pass --apply to set the clock")
        return 0

    try:
        if sys.platform == "win32":
            set_windows(http_dt)
        elif sys.platform.startswith("linux"):
            set_linux(http_dt)
        else:
            log(f"FAIL unsupported platform {sys.platform}")
            return 1
    except Exception as exc:
        log(f"FAIL set clock: {exc}")
        return 1

    log("applied")
    return 0


if __name__ == "__main__":
    sys.exit(main())
