#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Yuvin's Test Services — Version Updater
=======================================
v.1.0.0

Tells you which Yuvin releases your computer is certified for. It looks at
your operating system, checks it against the official support matrix, and
reports the right Yuvin Terminal, Yuvin Server Console, and Website build —
plus whether you're CURRENT, on a SUPPORTED older build, or FROZEN.

It is an ADVISOR only: it reads your OS version and nothing else. It never
downloads, installs, or changes a single file on your computer.

Works on macOS Terminal, Linux terminals, and Windows Command Prompt.
No installs needed — just Python 3.6+ and this one file.

Run it:            python3 yuvin_updater.py     (macOS / Linux)
                   python yuvin_updater.py      (Windows)
Options:           --all        show every supported OS, then exit
                   --no-color   plain text, no colors
                   --ascii      no emoji / fancy box characters
                   --version    print the version and exit
"""

import argparse
import os
import platform
import shutil
import sys

VERSION = "v.1.0.0"
SITE = "https://yuvin.com.au"

# The newest releases that exist anywhere right now (used to spot gaps).
LATEST_TERMINAL = "v.1.1.0"
LATEST_CONSOLE = "v.1.0.1"
LATEST_WEB_BUILD = "26H2.202.001"
LATEST_WEB_SEMVER = "v.2.3.2"
LATEST_WEB_CODENAME = "Blackheath"

# Status tiers a release can be in for a given OS.
CURRENT = "current"      # equals the newest anywhere
SUPPORTED = "supported"  # a valid, pinned build that isn't the newest
FROZEN = "frozen"        # can't move past its first release

# ---------------------------------------------------------------- terminal setup

def enable_ansi():
    """Turn on ANSI colors. On Windows CMD this needs a console mode flag."""
    if os.name != "nt":
        return True
    try:
        import ctypes
        kernel32 = ctypes.windll.kernel32
        handle = kernel32.GetStdHandle(-11)  # stdout
        mode = ctypes.c_uint32()
        if kernel32.GetConsoleMode(handle, ctypes.byref(mode)) == 0:
            return False
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
        return kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
    except Exception:
        return False


def can_encode(text):
    """Can this terminal actually print these characters?"""
    enc = getattr(sys.stdout, "encoding", None) or "ascii"
    try:
        text.encode(enc)
        return True
    except Exception:
        return False


COLOR = True
UNICODE_OK = True
EMOJI_OK = True


class C:
    """Site palette, translated to ANSI. #2563eb blue is the brand color."""
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    BLUE = "\033[94m"
    CYAN = "\033[96m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    GRAY = "\033[90m"


def col(code, text):
    if not COLOR:
        return text
    return code + text + C.RESET


def sym(emoji, fallback=""):
    """Use the emoji if the terminal can show it, otherwise a plain fallback."""
    if EMOJI_OK:
        return emoji
    return fallback


ASCII_SWAPS = (("—", "-"), ("·", "."), ("×", "x"), ("÷", "/"), ("●", "*"),
               ("─", "-"), ("│", "|"), ("┌", "+"), ("└", "+"), ("╭", "+"),
               ("╰", "+"), ("→", "->"))


def downgrade(text):
    """When the terminal can't do Unicode, swap fancy characters for plain
    ones so print() can never crash with UnicodeEncodeError."""
    if not UNICODE_OK:
        for fancy, plain in ASCII_SWAPS:
            text = text.replace(fancy, plain)
    return text


def emit(text):
    """downgrade() plus a guarantee the result can actually be encoded to
    this stdout — even on Python 3.6, where reconfigure() doesn't exist."""
    text = downgrade(text)
    enc = getattr(sys.stdout, "encoding", None)
    if enc:
        try:
            text.encode(enc)
        except Exception:
            text = text.encode(enc, "replace").decode(enc, "replace")
    return text


def say(text=""):
    print(emit(text))


def width():
    try:
        w = shutil.get_terminal_size((80, 24)).columns
    except Exception:
        w = 80
    return max(56, min(w, 96))


def line(ch=None):
    if ch is None:
        ch = "─" if UNICODE_OK else "-"
    say(col(C.GRAY, ch * width()))


def heading(text):
    say(col(C.BOLD + C.BLUE, text))


def note_line(text):
    """A dim, indented explanation under a version row."""
    say("     " + col(C.GRAY, text))


LOGO = r"""
  __   __ _____  ____
  \ \ / /|_   _|/ ___|
   \ V /   | |  \___ \
    | |    | |   ___) |
    |_|    |_|  |____/
"""


# ---------------------------------------------------------------- support matrix
# Two independent lookups per OS: the SOFTWARE (Terminal + Server Console) and
# the WEBSITE build. Kept explicit — no clever inference — so it matches the
# plan exactly. Dict order below is also the order used by --all.

MATRIX = {
    "win7": {
        "label": "Windows 7",
        "soft": {"terminal": "v.1.0.0", "console": "v.1.0.0", "status": FROZEN,
                 "note": "Frozen at the first release — Windows 7's last supported Python is 3.8."},
        "web": {"build": "26H1.200.000", "semver": "v.1.0.0", "codename": "Armstrong",
                "status": FROZEN, "note": "Frozen at the first stable release (Jan 20 2026)."},
    },
    "win8": {
        "label": "Windows 8 / 8.1",
        "soft": {"terminal": "v.1.0.0", "console": "v.1.0.0", "status": FROZEN,
                 "note": "Frozen at the first release."},
        "web": {"build": "26H1.206.000", "semver": "v.1.13.0", "codename": "",
                "status": SUPPORTED, "note": "The final 26H1 build (Mar 7 2026)."},
    },
    "win10": {
        "label": "Windows 10",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.101.001", "semver": "v.2.2.0", "codename": "",
                "status": SUPPORTED, "note": "The Yuvin Terminal release (Jul 24 2026)."},
    },
    "win11": {
        "label": "Windows 11",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "The current release (Aug 14 2026)."},
    },
    "macos10": {
        "label": "macOS 10 (Catalina)",
        "soft": {"terminal": "v.1.0.0", "console": "v.1.0.0", "status": FROZEN,
                 "note": "Frozen — on Catalina, Python 3 must be installed manually."},
        "web": {"build": "26H1.200.000", "semver": "v.1.0.0", "codename": "Armstrong",
                "status": FROZEN, "note": "Frozen at the first stable release."},
    },
    "macos11": {
        "label": "macOS 11 (Big Sur)",
        "soft": {"terminal": "v.1.0.0", "console": "v.1.0.0", "status": FROZEN,
                 "note": "Frozen at the first release."},
        "web": {"build": "26H1.203.000", "semver": "v.1.0.3", "codename": "",
                "status": SUPPORTED, "note": "The last of the 1.0.x line."},
    },
    "macos12": {
        "label": "macOS 12 (Monterey)",
        "soft": {"terminal": "v.1.0.0", "console": "v.1.0.0", "status": FROZEN,
                 "note": "Frozen at the first release."},
        "web": {"build": "26H1.206.000", "semver": "v.1.13.0", "codename": "",
                "status": SUPPORTED, "note": "The final 26H1 build."},
    },
    "macos13": {
        "label": "macOS 13 (Ventura)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.100.001", "semver": "v.2.1.50", "codename": "",
                "status": SUPPORTED, "note": "The HTTPS security baseline."},
    },
    "macos14": {
        "label": "macOS 14 (Sonoma)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.100.001", "semver": "v.2.1.50", "codename": "",
                "status": SUPPORTED, "note": "The HTTPS security baseline."},
    },
    "macos15": {
        "label": "macOS 15 (Sequoia)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "The current release."},
    },
    "macos26": {
        "label": "macOS 26 (Tahoe)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — gets the latest Terminal and Console."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "The current release."},
    },
    "macos27": {
        "label": "macOS 27 (Golden Gate, Beta)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — rides the newest build."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "Rides the newest build — nothing newer than Blackheath exists yet."},
    },
    "ubuntu": {
        "label": "Ubuntu (24.x)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — all current-generation Linux tracks the latest."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "Tracks the latest."},
    },
    "mint": {
        "label": "Linux Mint (Sena)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — the January 2026 Sena release tracks the latest."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "Tracks the latest."},
    },
    "debian": {
        "label": "Debian (Trixie 13+)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — Debian after January 2026 (incl. Trixie 13) tracks the latest."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "Tracks the latest."},
    },
    "fedora": {
        "label": "Fedora Linux (38+)",
        "soft": {"terminal": "v.1.1.0", "console": "v.1.0.1", "status": CURRENT,
                 "note": "Current — Fedora 38 and later tracks the latest."},
        "web": {"build": "26H2.202.001", "semver": "v.2.3.2", "codename": "Blackheath",
                "status": CURRENT, "note": "Tracks the latest."},
    },
}

# Grouping/order for the --all matrix view.
GROUPS = (
    ("Windows", ("win7", "win8", "win10", "win11")),
    ("macOS", ("macos10", "macos11", "macos12", "macos13", "macos14", "macos15", "macos26", "macos27")),
    ("Linux", ("ubuntu", "mint", "debian", "fedora")),
)


# ---------------------------------------------------------------- OS detection

def read_os_release():
    """Parse /etc/os-release (and fall back to /etc/lsb-release) into a dict."""
    data = {}
    for path in ("/etc/os-release", "/usr/lib/os-release"):
        try:
            with open(path, "r", encoding="utf-8", errors="replace") as fh:
                for raw in fh:
                    raw = raw.strip()
                    if not raw or raw.startswith("#") or "=" not in raw:
                        continue
                    key, _, val = raw.partition("=")
                    data[key.strip()] = val.strip().strip('"').strip("'")
            if data:
                return data
        except Exception:
            continue
    # Older distros: /etc/lsb-release with DISTRIB_ID / DISTRIB_RELEASE
    try:
        with open("/etc/lsb-release", "r", encoding="utf-8", errors="replace") as fh:
            for raw in fh:
                raw = raw.strip()
                if raw.startswith("DISTRIB_ID="):
                    data["ID"] = raw.split("=", 1)[1].strip().strip('"').lower()
                elif raw.startswith("DISTRIB_RELEASE="):
                    data["VERSION_ID"] = raw.split("=", 1)[1].strip().strip('"')
                elif raw.startswith("DISTRIB_DESCRIPTION="):
                    data["PRETTY_NAME"] = raw.split("=", 1)[1].strip().strip('"')
    except Exception:
        pass
    return data


def detect_windows():
    rel = platform.release()  # '7', '8', '8.1', '10', '11' (11 often shows as '10')
    build = 0
    try:
        build = int(platform.version().split(".")[2])
    except Exception:
        build = 0
    if rel in ("10", "11"):
        # Windows 11 shares the 10.0 kernel; build 22000+ is the real tell.
        if rel == "11" or build >= 22000:
            return "win11", "Windows 11"
        return "win10", "Windows 10"
    if rel == "8.1":
        return "win8", "Windows 8.1"
    if rel == "8":
        return "win8", "Windows 8"
    if rel == "7":
        return "win7", "Windows 7"
    return None, "Windows " + rel


MAC_NAMES = {10: "Catalina", 11: "Big Sur", 12: "Monterey", 13: "Ventura",
             14: "Sonoma", 15: "Sequoia", 26: "Tahoe", 27: "Golden Gate"}
MAC_IDS = {10: "macos10", 11: "macos11", 12: "macos12", 13: "macos13",
           14: "macos14", 15: "macos15", 26: "macos26", 27: "macos27"}


def detect_macos():
    raw = platform.mac_ver()[0]
    if not raw:
        return None, "macOS (unknown version)"
    parts = raw.split(".")
    try:
        major = int(parts[0])
        minor = int(parts[1]) if len(parts) > 1 else 0
    except Exception:
        return None, "macOS " + raw
    # Big Sur+ can report as '10.16' when Python is built against an old SDK.
    if major == 10 and minor >= 16:
        major = 11
    if major in MAC_IDS:
        label = "macOS %d (%s)" % (major, MAC_NAMES.get(major, "?"))
        if major == 27:
            label += " Beta"
        return MAC_IDS[major], label
    return None, "macOS " + raw


DISTRO_IDS = {"ubuntu": "ubuntu", "linuxmint": "mint", "debian": "debian", "fedora": "fedora"}


def detect_linux():
    info = read_os_release()
    distro_id = (info.get("ID") or "").lower()
    version_id = info.get("VERSION_ID") or ""
    pretty = info.get("PRETTY_NAME") or ""
    os_id = DISTRO_IDS.get(distro_id)
    if not os_id:
        # Derivatives: fall back to the family they're based on.
        like = (info.get("ID_LIKE") or "").lower()
        for family in ("ubuntu", "debian", "fedora"):
            if family in like:
                os_id = DISTRO_IDS[family]
                break
    label = pretty or (distro_id.capitalize() + " " + version_id).strip() or "Linux"
    return os_id, label


def detect_os():
    """Return (os_id or None, human label, raw-detected string, python version)."""
    system = platform.system()
    py = platform.python_version()
    if system == "Windows":
        os_id, label = detect_windows()
        raw = "Windows release " + platform.release() + " · build " + platform.version()
    elif system == "Darwin":
        os_id, label = detect_macos()
        raw = "Darwin / macOS " + (platform.mac_ver()[0] or "?")
    elif system == "Linux":
        os_id, label = detect_linux()
        info = read_os_release()
        raw = (info.get("PRETTY_NAME") or "Linux") + " · kernel " + platform.release()
    else:
        os_id, label = None, (system or "Unknown OS")
        raw = system or "unknown"
    return os_id, label, raw, py


# ---------------------------------------------------------------- rendering

def status_badge(status):
    if status == CURRENT:
        return col(C.GREEN + C.BOLD, "[CURRENT]")
    if status == SUPPORTED:
        return col(C.YELLOW + C.BOLD, "[SUPPORTED]")
    return col(C.GRAY + C.BOLD, "[FROZEN]")


def web_version_text(web):
    text = web["build"] + " · " + web["semver"]
    if web.get("codename"):
        text += " (" + web["codename"] + ")"
    return text


def version_row(name, version_text, status):
    name_p = name.ljust(22)
    ver_p = version_text.ljust(37)
    say("  " + col(C.BOLD, name_p) + col(C.CYAN, ver_p) + " " + status_badge(status))


def header(subtitle):
    say(col(C.BLUE + C.BOLD, LOGO.rstrip("\n")))
    say(col(C.BOLD, "  Yuvin's Test Services") + col(C.GRAY, "  —  Version Updater " + VERSION))
    say(col(C.GRAY, "  " + subtitle + "      " + SITE))
    line()


def run_hint(os_id):
    return "python" if (os_id or "").startswith("win") else "python3"


def render_report(os_id, label, raw, py, requested_all=False):
    header("Detect · Check · Advise")

    say("  " + sym("🖥️  ", "") + col(C.BOLD, "Your system:") + "  " + col(C.BOLD + C.BLUE, label))
    say("     " + col(C.GRAY, "Detected as " + raw))
    say("     " + col(C.GRAY, "Python " + py))
    say("")

    if os_id is None or os_id not in MATRIX:
        heading("NOT IN THE SUPPORT MATRIX (YET)")
        say("  " + sym("⚠️  ", "!") + "Your operating system isn't one Yuvin certifies yet.")
        say("")
        say("  " + col(C.GRAY, "Newest available anywhere:"))
        say("     Yuvin Terminal        " + col(C.CYAN, LATEST_TERMINAL))
        say("     Yuvin Server Console  " + col(C.CYAN, LATEST_CONSOLE))
        say("     Website build         " + col(C.CYAN, LATEST_WEB_BUILD + " · " + LATEST_WEB_SEMVER +
                                                 " (" + LATEST_WEB_CODENAME + ")"))
        say("")
        say("  " + sym("📦 ", "") + "Downloads:  " + col(C.BLUE, SITE + "/softwares.html"))
        say("  " + sym("🔎 ", "") + "See every supported OS:  " + col(C.CYAN, "python3 yuvin_updater.py --all"))
        line()
        footer()
        return

    entry = MATRIX[os_id]
    soft = entry["soft"]
    web = entry["web"]

    heading("YOUR VERSIONS")
    version_row("Yuvin Terminal", soft["terminal"], soft["status"])
    version_row("Yuvin Server Console", soft["console"], soft["status"])
    note_line(soft["note"])
    version_row("Website build", web_version_text(web), web["status"])
    note_line(web["note"])
    say("")

    # ---- overall verdict
    statuses = {soft["status"], web["status"]}
    if statuses == {CURRENT}:
        say("  " + col(C.GREEN + C.BOLD, sym("✅ ", "") +
            "You're fully up to date for your operating system!"))
    elif FROZEN in statuses:
        say("  " + col(C.GRAY + C.BOLD, sym("🧊 ", "") +
            "Part of your setup is FROZEN — your OS can't move past the versions above."))
    else:
        say("  " + col(C.YELLOW + C.BOLD, sym("⚠️  ", "!") +
            "You're on the newest build your OS is certified for (not the latest overall)."))

    if statuses != {CURRENT}:
        say("     " + col(C.GRAY, "Newest anywhere:  Terminal " + LATEST_TERMINAL +
                          " · Console " + LATEST_CONSOLE +
                          " · Website " + LATEST_WEB_SEMVER + " (" + LATEST_WEB_CODENAME + ")"))
    say("")

    # ---- how to get it
    rp = run_hint(os_id)
    heading("HOW TO GET / UPDATE")
    say("  " + sym("📦 ", "") + "Downloads:  " + col(C.BLUE, SITE + "/softwares.html"))
    say("  " + sym("💻 ", "") + "Terminal:   " + col(C.CYAN, rp + " yuvin.py"))
    say("  " + sym("📡 ", "") + "Console:    " + col(C.CYAN, rp + " yuvin_servers.py"))
    say("  " + sym("🌐 ", "") + "Website:    " + col(C.BLUE, SITE) +
        col(C.GRAY, "   (classic home: " + SITE + "/home)"))
    line()
    if not requested_all:
        say("  " + col(C.GRAY, "Tip: run  ") + col(C.CYAN, "python3 yuvin_updater.py --all") +
            col(C.GRAY, "  to see every supported OS."))
    footer()


def overall_status(entry):
    statuses = {entry["soft"]["status"], entry["web"]["status"]}
    if statuses == {CURRENT}:
        return CURRENT
    if FROZEN in statuses:
        return FROZEN
    return SUPPORTED


def render_all():
    header("Full support matrix")
    heading("EVERY SUPPORTED OPERATING SYSTEM")
    say("")
    for group_name, ids in GROUPS:
        say(col(C.BOLD + C.BLUE, group_name))
        for oid in ids:
            entry = MATRIX[oid]
            soft = entry["soft"]
            web = entry["web"]
            say("  " + col(C.BOLD, entry["label"]) + "  " + status_badge(overall_status(entry)))
            say("     " + col(C.GRAY, "Software  ") + "Terminal " + col(C.CYAN, soft["terminal"]) +
                " · Console " + col(C.CYAN, soft["console"]) + "  " + status_badge(soft["status"]))
            say("     " + col(C.GRAY, "Website   ") + col(C.CYAN, web_version_text(web)) +
                "  " + status_badge(web["status"]))
        say("")
    line()
    say("  " + col(C.GRAY, "Legend:  ") + status_badge(CURRENT) + col(C.GRAY, " newest anywhere   ") +
        status_badge(SUPPORTED) + col(C.GRAY, " valid, older build   ") +
        status_badge(FROZEN) + col(C.GRAY, " can't advance"))
    footer()


def footer():
    say("")
    say(col(C.GRAY, "  Advisor only — this reads your OS version and changes nothing."))
    say(col(C.GRAY, "  (c) 2026 Yuvin's Test Services · Version Updater " + VERSION))
    say("")


# ---------------------------------------------------------------- main

def main():
    global COLOR, UNICODE_OK, EMOJI_OK
    # Even if a fancy character slips past every gate, print() must never
    # crash the whole app over it — replace it with '?' instead.
    for stream in (sys.stdout, sys.stderr):
        try:
            stream.reconfigure(errors="replace")
        except Exception:
            pass

    parser = argparse.ArgumentParser(
        prog="yuvin_updater.py",
        description="Yuvin's Test Services - Version Updater (advisor only)")
    parser.add_argument("--all", action="store_true", help="show every supported OS, then exit")
    parser.add_argument("--no-color", action="store_true", help="disable colors")
    parser.add_argument("--ascii", action="store_true", help="disable emoji and box characters")
    parser.add_argument("--version", action="version",
                        version="Yuvin's Test Services Version Updater " + VERSION)
    args = parser.parse_args()

    ansi_ok = enable_ansi()
    COLOR = ansi_ok and not args.no_color and os.environ.get("NO_COLOR") is None
    # Classic Windows conhost claims UTF-8 (PEP 528) but its fonts can't
    # render emoji — Windows Terminal sets WT_SESSION, conhost doesn't.
    legacy_console = (os.name == "nt" and not os.environ.get("WT_SESSION")
                      and not os.environ.get("TERM_PROGRAM"))
    UNICODE_OK = (not args.ascii) and can_encode("─│┌└×÷—·●╭╰→")
    EMOJI_OK = (not args.ascii) and not legacy_console and can_encode("🖥️✅🧊🌐")

    say("")
    if args.all:
        render_all()
        return

    os_id, label, raw, py = detect_os()
    render_report(os_id, label, raw, py)


if __name__ == "__main__":
    main()
