"""TD version picker + .al association toolkit in one standalone file.

Run interactively (double-click, or `python td_pick.py [project.al]`):

    double-click default : TD_Release_2026.2.210.850
    script default       : TD_Release_2026.2.210.850
    open : demo.al
      [1] TD_Release_2026.2.210.850
      [2] TD_Engineer_2027.1.212.603
      ...
    select TD [1-N, Enter=1, q=quit]: 2
    save <version> as default? [y = double-click + script, Enter = one-shot]: y

Saving rewrites the .al UserChoice with a freshly computed hash (the
kolbi.cz / PS-SFTA algorithm, ported below) and moves that version's bin to
the front of the persistent user PATH.

Subcommands (non-interactive):
    save / apply / status    snapshot and restore the .al association
    verify                   self-test the hash port against the snapshot
    set-progid <ProgId>      point the .al default at any ProgId
    install-picker           make double-clicking .al open this picker

Snapshots are valid only on the same user account and Windows build.
"""

import base64
import ctypes
import ctypes.wintypes as wt
import hashlib
import json
import shutil
import struct
import subprocess
import sys
import time
import winreg
from datetime import datetime
from pathlib import Path

ANLOGIC_ROOT = Path(r"C:\Anlogic")
DEFAULT_INDEX = 0  # plain Enter opens the first listed version
EXTENSION = ".al"
USERCHOICE_KEY = (
    "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"
    f"{EXTENSION}\\UserChoice"
)
SNAPSHOT_PATH = Path(__file__).with_suffix(".json")
PICKER_PROGID = "TDVersionPicker.al"

KEY_QUERY_VALUE = 0x0001
KEY_SET_VALUE = 0x0002
HKEY_CURRENT_USER = 0x80000001
KEY_WRITE_TIME_INFORMATION = 0  # KEY_SET_INFORMATION_CLASS, LARGE_INTEGER buffer
SHCNE_ASSOCCHANGED = 0x08000000
FILETIME_EPOCH_DELTA_S = 11_644_473_600  # seconds between 1601-01-01 and 1970-01-01
HWND_BROADCAST = 0xFFFF
WM_SETTINGCHANGE = 0x001A
SMTO_ABORTIFHUNG = 0x0002

EXPERIENCE_PREFIX = "User Choice set via Windows User Experience"
HARDCODED_EXPERIENCE = (
    "User Choice set via Windows User Experience"
    " {D18B6DD5-6124-4341-9318-804003BAFA0B}"
)

_advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
_ntdll = ctypes.WinDLL("ntdll")
_shell32 = ctypes.WinDLL("shell32")
_user32 = ctypes.WinDLL("user32", use_last_error=True)

_advapi32.RegOpenKeyExW.argtypes = (
    wt.HANDLE, wt.LPCWSTR, wt.DWORD, wt.DWORD, ctypes.POINTER(wt.HKEY)
)
_advapi32.RegOpenKeyExW.restype = wt.LONG
_advapi32.RegQueryInfoKeyW.argtypes = (
    wt.HANDLE,
    ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,  # class, cchClass, reserved
    ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,  # subkey counts/lengths
    ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,  # value counts/lengths
    ctypes.c_void_p,  # security descriptor size
    ctypes.POINTER(wt.FILETIME),
)
_advapi32.RegQueryInfoKeyW.restype = wt.LONG
_advapi32.RegCloseKey.argtypes = (wt.HKEY,)
_ntdll.NtSetInformationKey.argtypes = (
    wt.HANDLE, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_ulong
)
_ntdll.NtSetInformationKey.restype = ctypes.c_long
_shell32.SHChangeNotify.argtypes = (
    ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p
)
_user32.SendMessageTimeoutW.argtypes = (
    wt.HWND, wt.UINT, ctypes.c_size_t, wt.LPCWSTR, wt.UINT, wt.UINT,
    ctypes.POINTER(ctypes.c_size_t),
)
_user32.SendMessageTimeoutW.restype = ctypes.c_ssize_t


# ---------------------------------------------------------------- registry


def _open_raw_key(access: int) -> wt.HKEY:
    hkey = wt.HKEY()
    rc = _advapi32.RegOpenKeyExW(
        wt.HANDLE(HKEY_CURRENT_USER), USERCHOICE_KEY, 0, access, ctypes.byref(hkey)
    )
    if rc != 0:
        raise RuntimeError(f"RegOpenKeyExW({USERCHOICE_KEY}) failed, error {rc}")
    return hkey


def _query_last_write_time() -> int:
    """Read the UserChoice key's exact last-write FILETIME (100 ns ticks)."""
    hkey = _open_raw_key(KEY_QUERY_VALUE)
    try:
        ft = wt.FILETIME()
        rc = _advapi32.RegQueryInfoKeyW(
            hkey, None, None, None, None, None, None, None, None, None, None,
            ctypes.byref(ft),
        )
        if rc != 0:
            raise RuntimeError(f"RegQueryInfoKeyW failed, error {rc}")
        return ft.dwLowDateTime | (ft.dwHighDateTime << 32)
    finally:
        _advapi32.RegCloseKey(hkey)


def _set_last_write_time(filetime: int) -> None:
    """Back-date the key so a restored hash validates against it."""
    hkey = _open_raw_key(KEY_SET_VALUE | KEY_QUERY_VALUE)
    try:
        buffer = ctypes.c_ulonglong(filetime)
        status = _ntdll.NtSetInformationKey(
            hkey, KEY_WRITE_TIME_INFORMATION, ctypes.byref(buffer),
            ctypes.sizeof(buffer),
        )
        if status != 0:
            raise RuntimeError(
                f"NtSetInformationKey failed, NTSTATUS 0x{status & 0xFFFFFFFF:08X}"
            )
    finally:
        _advapi32.RegCloseKey(hkey)


def read_userchoice() -> tuple[str, str, int]:
    """Return (ProgId, Hash, last-write FILETIME) of the .al UserChoice key."""
    try:
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, USERCHOICE_KEY) as key:
            progid = winreg.QueryValueEx(key, "ProgId")[0]
            digest = winreg.QueryValueEx(key, "Hash")[0]
    except OSError as exc:
        raise RuntimeError(f"cannot read UserChoice: {exc}") from exc
    return progid, digest, _query_last_write_time()


def _delete_userchoice_key() -> None:
    try:
        winreg.DeleteKey(winreg.HKEY_CURRENT_USER, USERCHOICE_KEY)
    except OSError as exc:
        raise RuntimeError(
            "cannot delete the protected UserChoice key (ACL or the UCPD"
            f" driver blocked it): {exc}"
        ) from exc


def write_userchoice(progid: str, digest: str, filetime: int) -> None:
    """Write ProgId/Hash, then re-stamp the key time so the hash validates."""
    try:
        with winreg.OpenKey(
            winreg.HKEY_CURRENT_USER, USERCHOICE_KEY,
            access=KEY_SET_VALUE | KEY_QUERY_VALUE,
        ) as key:
            winreg.SetValueEx(key, "Hash", 0, winreg.REG_SZ, digest)
            winreg.SetValueEx(key, "ProgId", 0, winreg.REG_SZ, progid)
    except OSError:
        # Windows protects UserChoice with a "deny SetValue" ACE for the user
        # and/or the UCPD filter driver. Classic workaround: drop the whole
        # key and recreate it; the new key inherits the parent's plain DACL.
        _delete_userchoice_key()
        with winreg.CreateKey(winreg.HKEY_CURRENT_USER, USERCHOICE_KEY) as key:
            winreg.SetValueEx(key, "Hash", 0, winreg.REG_SZ, digest)
            winreg.SetValueEx(key, "ProgId", 0, winreg.REG_SZ, progid)
    _set_last_write_time(filetime)
    _shell32.SHChangeNotify(SHCNE_ASSOCCHANGED, 0, None, None)


def resolve_prog_exe(progid: str) -> str | None:
    """Return the command behind a ProgId, tolerating localized verb names."""
    try:
        shell = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, f"{progid}\\shell")
    except OSError:
        return None
    with shell:
        verbs = ["open"]
        for index in range(winreg.QueryInfoKey(shell)[0]):
            name = winreg.EnumKey(shell, index)
            if name.lower() != "open":
                verbs.append(name)
        for verb in verbs:
            try:
                with winreg.OpenKey(shell, f"{verb}\\command") as cmd:
                    return winreg.QueryValueEx(cmd, "")[0]
            except OSError:
                continue
    return None


# ------------------------------------------------------------- hash port


def _i32(value: int) -> int:
    """Wrap to signed 32-bit, mirroring PowerShell's Convert-Int32."""
    value &= 0xFFFFFFFF
    return value - 0x100000000 if value >= 0x80000000 else value


def _shr(value: int, count: int) -> int:
    """PowerShell Get-ShiftRight: arithmetic shift with sign-bit fixup."""
    if value & 0x80000000:
        return (value >> count) ^ 0xFFFF0000
    return value >> count


def _reglong(data, offset: int) -> int:
    return struct.unpack_from("<i", data, offset)[0]


def _get_sid() -> str:
    output = subprocess.run(
        ["whoami", "/user", "/fo", "csv", "/nh"],
        capture_output=True, text=True, check=True,
    ).stdout
    return output.strip().splitlines()[-1].rsplit(",", 1)[1].strip('"').lower()


def _get_experience() -> str:
    """Pull the hash seed string out of shell32.dll, like PS-SFTA does."""
    try:
        with open(r"C:\Windows\SysWOW64\Shell32.dll", "rb") as handle:
            text = handle.read(5 * 1024 * 1024).decode("utf-16-le", errors="ignore")
        start = text.find(EXPERIENCE_PREFIX)
        end = text.find("}", start)
        if start >= 0 and end > start:
            return text[start : end + 1]
    except OSError:
        pass
    return HARDCODED_EXPERIENCE


def _minute_filetime(filetime: int) -> int:
    """Floor a FILETIME to the start of its local minute (hash granularity)."""
    unix_s = filetime // 10_000_000 - FILETIME_EPOCH_DELTA_S
    parts = time.localtime(unix_s)
    local_s = time.mktime(
        (parts.tm_year, parts.tm_mon, parts.tm_mday,
         parts.tm_hour, parts.tm_min, 0, 0, 0, -1)
    )
    return int(local_s + FILETIME_EPOCH_DELTA_S) * 10_000_000


def compute_userchoice_hash(progid: str, filetime: int) -> str:
    """UserChoice hash, ported from PS-SFTA (kolbi.cz reverse engineering).

    Inputs: extension + user SID + ProgId + minute-floored key write time +
    the shell32 experience string, lowercased, UTF-16LE with a trailing NUL.
    """
    minute_ft = _minute_filetime(filetime)
    dt_hex = f"{minute_ft >> 32:08x}{minute_ft & 0xFFFFFFFF:08x}"
    base_info = f"{EXTENSION}{_get_sid()}{progid}{dt_hex}{_get_experience()}".lower()
    data = base_info.encode("utf-16-le") + b"\x00\x00"
    md5 = hashlib.md5(data).digest()

    length_base = len(base_info) * 2 + 2
    length = ((length_base & 4) <= 1) + (length_base >> 2) - 1
    out = bytearray(16)
    if length <= 1:
        return base64.b64encode(bytes(out)).decode("ascii")

    md51 = _i32((_reglong(md5, 0) | 1) + 0x69FB0000)
    md52 = _i32((_reglong(md5, 4) | 1) + 0x13DB0000)
    pdata = 0
    cache = 0
    outhash1 = 0
    for _ in range(((length - 2) >> 1) + 1):
        r0 = _i32(_reglong(data, pdata) + outhash1)
        r1 = _reglong(data, pdata + 4)
        pdata += 8
        r2 = _i32(_i32(r0 * md51) - _i32(0x10FA9605 * _shr(r0, 16)))
        r2 = _i32(_i32(0x79F8A395 * r2) + _i32(0x689B6B9F * _shr(r2, 16)))
        r3 = _i32(_i32(0xEA970001 * r2) - _i32(0x3C101569 * _shr(r2, 16)))
        r4 = _i32(r3 + r1)
        r5 = _i32(cache + r3)
        r6 = _i32(_i32(r4 * md52) - _i32(0x3CE8EC25 * _shr(r4, 16)))
        r6 = _i32(_i32(0x59C3AF2D * r6) - _i32(0x2232E0F1 * _shr(r6, 16)))
        outhash1 = _i32(_i32(0x1EC90001 * r6) + _i32(0x35BD1EC9 * _shr(r6, 16)))
        outhash2 = _i32(r5 + outhash1)
        cache = outhash2
    out[0:4] = struct.pack("<i", outhash1)
    out[4:8] = struct.pack("<i", outhash2)

    md51 = _i32(_reglong(md5, 0) | 1)
    md52 = _i32(_reglong(md5, 4) | 1)
    pdata = 0
    cache = 0
    outhash1 = 0
    for _ in range(((length - 2) >> 1) + 1):
        r0 = _i32(_reglong(data, pdata) + outhash1)
        pdata += 8
        r1 = _i32(r0 * md51)
        r1 = _i32(_i32(0xB1110000 * r1) - _i32(0x30674EEF * _shr(r1, 16)))
        r2 = _i32(_i32(0x5B9F0000 * r1) - _i32(0x78F7A461 * _shr(r1, 16)))
        r2 = _i32(_i32(0x12CEB96D * _shr(r2, 16)) - _i32(0x46930000 * r2))
        r3 = _i32(_i32(0x1D830000 * r2) + _i32(0x257E1D83 * _shr(r2, 16)))
        r4 = _i32(md52 * _i32(r3 + _reglong(data, pdata - 4)))
        r4 = _i32(_i32(0x16F50000 * r4) - _i32(0x5D8BE90B * _shr(r4, 16)))
        r5 = _i32(_i32(0x96FF0000 * r4) - _i32(0x2C7C6901 * _shr(r4, 16)))
        r5 = _i32(_i32(0x2B890000 * r5) + _i32(0x7C932B89 * _shr(r5, 16)))
        outhash1 = _i32(_i32(0x9F690000 * r5) - _i32(0x405B6097 * _shr(r5, 16)))
        outhash2 = _i32(outhash1 + cache + r3)
        cache = outhash2
    out[8:12] = struct.pack("<i", outhash1)
    out[12:16] = struct.pack("<i", outhash2)

    hv1 = _reglong(out, 8) ^ _reglong(out, 0)
    hv2 = _reglong(out, 12) ^ _reglong(out, 4)
    return base64.b64encode(struct.pack("<ii", hv1, hv2)).decode("ascii")


# ------------------------------------------------------ defaults handling


def _broadcast_env_change() -> None:
    result = ctypes.c_size_t()
    _user32.SendMessageTimeoutW(
        wt.HWND(HWND_BROADCAST), WM_SETTINGCHANGE, 0, "Environment",
        SMTO_ABORTIFHUNG, 5000, ctypes.byref(result),
    )


def set_user_path_first(bin_dir: str) -> None:
    """Move bin_dir to the front of the persistent user PATH."""
    target = bin_dir.rstrip("\\").lower()
    with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as key:
        current, value_type = winreg.QueryValueEx(key, "Path")
    entries = [
        entry for entry in current.split(";")
        if entry and entry.rstrip("\\").lower() != target
    ]
    with winreg.OpenKey(
        winreg.HKEY_CURRENT_USER, "Environment", access=KEY_SET_VALUE
    ) as key:
        winreg.SetValueEx(
            key, "Path", 0, value_type, bin_dir.rstrip("\\") + ";" + ";".join(entries)
        )
    _broadcast_env_change()


def _write_toast(progid: str) -> None:
    toasts = (
        "Software\\Microsoft\\Windows\\CurrentVersion"
        "\\ApplicationAssociationToasts"
    )
    with winreg.CreateKey(winreg.HKEY_CURRENT_USER, toasts) as key:
        winreg.SetValueEx(key, f"{progid}_{EXTENSION}", 0, winreg.REG_DWORD, 0)


def set_userchoice_now(progid: str) -> None:
    """Write UserChoice for progid with a freshly computed valid hash."""
    now_ft = int(time.time() + FILETIME_EPOCH_DELTA_S) * 10_000_000
    digest = compute_userchoice_hash(progid, now_ft)
    write_userchoice(progid, digest, _minute_filetime(now_ft))
    current = read_userchoice()
    if current[0] != progid or current[1] != digest:
        raise RuntimeError("verification mismatch after writing UserChoice")
    _write_toast(progid)


def register_direct_prog(exe: Path) -> str:
    """Register a per-user ProgId that launches this td.exe; return its name."""
    folder = exe.parent.parent.name
    progid = f"TDDirect.{folder}.al"
    root = winreg.HKEY_CURRENT_USER
    base = f"Software\\Classes\\{progid}"
    with winreg.CreateKey(root, base) as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, f"TD {folder}")
    with winreg.CreateKey(root, base + "\\DefaultIcon") as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, f"{exe},0")
    with winreg.CreateKey(root, base + "\\shell\\open\\command") as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, f'"{exe}" "%1"')
    with winreg.CreateKey(
        root, f"Software\\Classes\\{EXTENSION}\\OpenWithProgids"
    ) as key:
        winreg.SetValueEx(key, progid, 0, winreg.REG_NONE, b"")
    return progid


# ------------------------------------------------------ version discovery


def find_td_versions() -> list[Path]:
    """Return existing <TD install>/bin/td.exe paths, folder-name descending.

    Descending name sort happens to put TD_Release_* (current release line)
    first and the old TD_4.6.8 CPLD tree last.
    """
    exes = [
        folder / "bin" / "td.exe"
        for folder in ANLOGIC_ROOT.iterdir()
        if folder.is_dir() and (folder / "bin" / "td.exe").is_file()
    ]
    return sorted(exes, key=lambda exe: exe.parent.parent.name, reverse=True)


def progid_for(exe: Path) -> str:
    """Find or create the ProgId whose command launches this td.exe.

    Candidates come from the Anlogic installer registrations under HKLM
    (each product's Capabilities\\FileAssociations), matched by checking the
    ProgId's shell command against this exe's full path.
    """
    candidates = []
    try:
        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Anlogic"
        ) as products:
            for index in range(winreg.QueryInfoKey(products)[0]):
                product = winreg.EnumKey(products, index)
                try:
                    with winreg.OpenKey(
                        products, product + r"\Capabilities\FileAssociations"
                    ) as assoc:
                        candidates.append(winreg.QueryValueEx(assoc, ".al")[0])
                except OSError:
                    continue
    except OSError:
        pass
    candidates.append("Anlogic." + exe.parent.parent.name.rsplit(".", 2)[0] + ".al")
    for progid in candidates:
        command = resolve_prog_exe(progid) or ""
        if str(exe).lower() in command.lower():
            return progid
    return register_direct_prog(exe)


def double_click_default() -> str:
    """Describe what double-clicking .al currently opens."""
    try:
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, USERCHOICE_KEY) as key:
            progid = winreg.QueryValueEx(key, "ProgId")[0]
    except OSError:
        return "(no association)"
    if progid == PICKER_PROGID:
        return "(this picker)"
    command = resolve_prog_exe(progid) or ""
    first = command.split('"')[1] if '"' in command else command.split()[0]
    if first.lower().endswith("td.exe"):
        return str(Path(first).parent.parent.name)
    return progid


def script_default() -> str:
    """Folder of the td.exe that a bare `td` resolves to right now."""
    found = shutil.which("td")
    if not found:
        return "(not found in PATH)"
    return str(Path(found).parent.parent.name)


# -------------------------------------------------------- interactive UI


def pick_main(project_arg: str | None) -> None:
    project = Path(project_arg).resolve() if project_arg else None
    versions = find_td_versions()
    if not versions:
        print(f"no TD installation found under {ANLOGIC_ROOT}", file=sys.stderr)
        sys.exit(1)

    print(f"double-click default : {double_click_default()}")
    print(f"script default       : {script_default()}")
    print(f"open : {project.name if project else '(no project file)'}")
    for number, exe in enumerate(versions, start=1):
        print(f"  [{number}] {exe.parent.parent.name}")

    raw = input(f"select TD [1-{len(versions)}, Enter=1, q=quit]: ").strip()
    if raw.lower() == "q":
        return
    if raw.isdigit() and 1 <= int(raw) <= len(versions):
        index = int(raw) - 1
    else:
        index = DEFAULT_INDEX
    exe = versions[index]

    answer = input(
        f"save {exe.parent.parent.name} as default?"
        " [y = double-click + script, Enter = this time only]: "
    ).strip().lower()
    if answer == "y":
        set_userchoice_now(progid_for(exe))
        set_user_path_first(str(exe.parent))
        print(f"  double-click default -> {exe.parent.parent.name}")
        print(f"  script default       -> {exe.parent.parent.name} (new terminals)")

    print(f"launching {exe} ...")
    subprocess.Popen([str(exe)] + ([str(project)] if project else []))


# ----------------------------------------------------------- subcommands


def _load_snapshot() -> dict:
    try:
        snapshot = json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8"))
        return {
            "progid": snapshot["progid"],
            "hash": snapshot["hash"],
            "filetime": int(snapshot["last_write_filetime"]),
            "saved_at": snapshot.get("saved_at", "?"),
        }
    except (OSError, KeyError, TypeError, ValueError) as exc:
        raise RuntimeError(f"cannot load snapshot {SNAPSHOT_PATH}: {exc}") from exc


def _filetime_str(filetime: int) -> str:
    unix_s = filetime // 10_000_000 - FILETIME_EPOCH_DELTA_S
    return datetime.fromtimestamp(unix_s).astimezone().strftime("%Y-%m-%d %H:%M:%S")


def cmd_save() -> None:
    progid, digest, filetime = read_userchoice()
    snapshot = {
        "progid": progid,
        "hash": digest,
        "last_write_filetime": str(filetime),
        "exe": resolve_prog_exe(progid),
        "saved_at": datetime.now().astimezone().isoformat(timespec="seconds"),
    }
    SNAPSHOT_PATH.write_text(
        json.dumps(snapshot, indent=2, ensure_ascii=False), encoding="utf-8"
    )
    print(f"saved  : {progid}")
    print(f"  exe      : {snapshot['exe']}")
    print(f"  hash     : {digest}")
    print(f"  key time : {_filetime_str(filetime)}  (filetime {filetime})")
    print(f"  file     : {SNAPSHOT_PATH}")


def cmd_apply() -> None:
    snap = _load_snapshot()
    exe = resolve_prog_exe(snap["progid"])
    if exe is None:
        print(
            f"warning: no command found for {snap['progid']}, that version may"
            " be uninstalled; restoring anyway",
            file=sys.stderr,
        )
    write_userchoice(snap["progid"], snap["hash"], snap["filetime"])
    current = read_userchoice()
    if current != (snap["progid"], snap["hash"], snap["filetime"]):
        raise RuntimeError("verification mismatch after restore, inspect the key")
    print(f"restored: {snap['progid']}")
    print(f"  exe      : {exe}")
    print(f"  key time : {_filetime_str(current[2])}")
    print("double-click an .al file to confirm TD launches from that path")


def cmd_status() -> None:
    progid, digest, filetime = read_userchoice()
    print(f"current : {progid}")
    print(f"  exe            : {resolve_prog_exe(progid)}")
    print(f"  hash           : {digest}")
    print(f"  key time       : {_filetime_str(filetime)}")
    print(f"  script default : {script_default()}")
    if not SNAPSHOT_PATH.exists():
        print("snapshot: none (run 'save' first)")
        return
    snap = _load_snapshot()
    matches = (
        snap["progid"] == progid
        and snap["hash"] == digest
        and snap["filetime"] == filetime
    )
    verdict = "matches current" if matches else "DIFFERS from current"
    print(f"snapshot: {snap['progid']} (saved {snap['saved_at']}) -> {verdict}")


def cmd_verify() -> None:
    """Self-test the hash port against the system-written snapshot."""
    snap = _load_snapshot()
    digest = compute_userchoice_hash(snap["progid"], snap["filetime"])
    print(f"computed : {digest}")
    print(f"snapshot : {snap['hash']}")
    if digest != snap["hash"]:
        print("hash port MISMATCH, do not trust automated writes", file=sys.stderr)
        sys.exit(1)
    print("hash port VERIFIED against the system-written value")


def cmd_set_progid() -> None:
    if len(sys.argv) != 3:
        print("usage: td_pick.py set-progid <ProgId>", file=sys.stderr)
        sys.exit(2)
    progid = sys.argv[2]
    set_userchoice_now(progid)
    print(f"UserChoice -> {progid}")
    print(f"  exe : {resolve_prog_exe(progid)}")


def _register_picker_prog_id() -> None:
    versions = find_td_versions()
    icon = f"{versions[0]},0" if versions else "td.exe,0"
    root = winreg.HKEY_CURRENT_USER
    base = f"Software\\Classes\\{PICKER_PROGID}"
    with winreg.CreateKey(root, base) as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, "TD Version Picker")
    with winreg.CreateKey(root, base + "\\DefaultIcon") as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, icon)
    command = f'"{sys.executable}" "{Path(__file__).resolve()}" "%1"'
    with winreg.CreateKey(root, base + "\\shell\\open\\command") as key:
        winreg.SetValueEx(key, None, 0, winreg.REG_SZ, command)
    with winreg.CreateKey(
        root, f"Software\\Classes\\{EXTENSION}\\OpenWithProgids"
    ) as key:
        winreg.SetValueEx(key, PICKER_PROGID, 0, winreg.REG_NONE, b"")
    _write_toast(PICKER_PROGID)


def cmd_install_picker() -> None:
    _register_picker_prog_id()
    set_userchoice_now(PICKER_PROGID)
    print("installed: double-clicking .al now opens this picker")
    print(f"  ProgId : {PICKER_PROGID}")
    print(f'  command: "{sys.executable}" "{Path(__file__).resolve()}" "%1"')


def main() -> None:
    commands = {
        "save": cmd_save,
        "apply": cmd_apply,
        "status": cmd_status,
        "verify": cmd_verify,
        "set-progid": cmd_set_progid,
        "install-picker": cmd_install_picker,
    }
    args = sys.argv[1:]
    if args and args[0] in commands:
        try:
            commands[args[0]]()
        except RuntimeError as exc:
            print(f"error: {exc}", file=sys.stderr)
            sys.exit(1)
    else:
        pick_main(args[0] if args else None)


if __name__ == "__main__":
    main()
