#!/usr/bin/env python3
"""
chk_feature_V1R1.py - Python port of chk_feature_V1R1.ps1
Diagnostic script: reports OS, Java/keytool, runmqakm, gcc/MinGW, Python,
Nuitka and chkcert build/runtime environment.

Unlike the PowerShell/.cmd ports, subprocess.run() in Python calls
CreateProcess directly regardless of file extension, so there is no
ShellExecute/PATHEXT association problem to work around when running a
".lnk"-named plain-copy fallback binary - the Invoke-ExeCapture workaround
from the PowerShell version has no equivalent need here.
"""

# python -c "import socket; print('hostname :', socket.gethostname()); print('fqdn     :', socket.getfqdn())"
import argparse
import ctypes
import datetime
import glob
import json
import os
import platform
import re
import shutil
import socket
import struct
import subprocess
import sys
import time
from pathlib import Path

# ── Colour helpers - both console and the output file itself, using raw
#    ANSI escapes so the .log carries colour the same way the shell/
#    PowerShell versions do. ──
ESC = "\033"
ANSI_RED = f"{ESC}[91m"
ANSI_GREEN = f"{ESC}[92m"
ANSI_YELLOW = f"{ESC}[93m"
ANSI_CYAN = f"{ESC}[96m"
ANSI_MAGENTA = f"{ESC}[95m"
ANSI_RESET = f"{ESC}[0m"

FILE01 = None  # set in main() once the hostname/date-stamped filename is known

# The PATH-bootstrap directories (WinPython, JDK, IBM MQ Client, GSKit,
# mingw64) are Windows-specific by design - see the PATH setup section in
# main() - so this flag guards that whole section rather than letting it
# print a wall of "not found" warnings for paths that can never exist on
# Unix/macOS.
IS_WINDOWS = os.name == "nt"


def _append_file(line: str) -> None:
    if FILE01:
        with open(FILE01, "a", encoding="utf-8") as f:
            f.write(line + "\n")


def write_info(text: str) -> None:
    print(f"{ANSI_CYAN}# Info       : {text}{ANSI_RESET}")
    _append_file(f"{ANSI_CYAN}# Info       : {text}{ANSI_RESET}")


def write_warn(text: str) -> None:
    print(f"{ANSI_YELLOW}# Warning    : {text}{ANSI_RESET}")
    _append_file(f"{ANSI_YELLOW}# Warning    : {text}{ANSI_RESET}")


def write_err(text: str) -> None:
    print(f"{ANSI_RED}# Error      : {text}{ANSI_RESET}")
    _append_file(f"{ANSI_RED}# Error      : {text}{ANSI_RESET}")


def write_cmd(text: str) -> None:
    print(f"{ANSI_MAGENTA}# Command    : {text}{ANSI_RESET}")
    _append_file(f"{ANSI_MAGENTA}# Command    : {text}{ANSI_RESET}")


def write_ok(text: str) -> None:
    print(f"{ANSI_GREEN}# OK         : {text}{ANSI_RESET}")
    _append_file(f"{ANSI_GREEN}# OK         : {text}{ANSI_RESET}")


def run_capture(cmd: list, timeout: float = None) -> list:
    """Run a command, return combined stdout+stderr as a list of text lines.
    Never raises on a non-zero exit or a missing executable - callers check
    run_capture_rc() alongside this, or simply treat an empty list as
    failure, matching the PowerShell version's tolerant behavior.
    On timeout (only enforced when the `timeout` argument is given, in
    seconds), the child process is killed and an empty list is returned -
    same tolerant contract as a missing executable, so existing callers
    that don't pass a timeout see no behavior change."""
    try:
        proc = subprocess.run(
            cmd, capture_output=True, text=True, encoding="utf-8",
            errors="replace", timeout=timeout
        )
    except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
        return []
    combined = (proc.stdout or "") + (proc.stderr or "")
    return combined.splitlines()


def run_capture_rc(cmd: list) -> tuple:
    """Same as run_capture() but also returns the process return code."""
    try:
        proc = subprocess.run(
            cmd, capture_output=True, text=True, encoding="utf-8", errors="replace"
        )
    except (FileNotFoundError, OSError):
        return [], 127
    combined = (proc.stdout or "") + (proc.stderr or "")
    return combined.splitlines(), proc.returncode


def format_chkcert_version(lines: list) -> list:
    """Trim chkcert's own --version banner down to just the name/version and
    copyright lines, dropping blank lines and the fixed legal paragraph that
    would otherwise repeat in full every time --version is captured."""
    skip_prefixes = (
        "This software", "Licensed for", "resale, reverse",
        "is strictly", "Use of this", "provided by",
    )
    return [
        ln for ln in lines
        if ln.strip() != "" and not ln.startswith(skip_prefixes)
    ]


def remove_blank_lines(lines: list) -> list:
    """Strip only the blank lines, keeping every text line (including the
    legal paragraph) - used where the full banner text is wanted."""
    return [ln for ln in lines if ln.strip() != ""]


def find_embedded_string(path: str, pattern: str, chunk_size: int = 4 * 1024 * 1024):
    """Equivalent of Unix 'strings' restricted to one pattern, streamed in
    fixed-size chunks so a large binary is never held fully in memory."""
    regex = re.compile(pattern)
    carry = ""
    with open(path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            chunk_text = chunk.decode("latin-1")
            combined = carry + chunk_text
            m = regex.search(combined)
            if m:
                return m.group(0)
            tail_len = min(256, len(combined))
            carry = combined[len(combined) - tail_len:]
    return None


_PRINTABLE_RUN_RE = re.compile(r"[\x20-\x7E]{4,}")


def find_string_with_prefix(path: str, prefix: str, chunk_size: int = 4 * 1024 * 1024):
    """
    Equivalent of `strings PATH | grep "^PREFIX"` — unlike find_embedded_string(),
    this only considers genuine printable runs delimited by non-printable
    bytes (exactly what the `strings` utility extracts), so a match can
    never straddle two unrelated runs of bytes that merely happen to look
    printable side by side. find_embedded_string() greps the raw file
    directly instead, which was observed in practice to return corrupted
    matches (e.g. "GCCCCC") when the real string boundary fell in the
    middle of the pattern. Chunk boundaries are handled the same
    carry-over way as find_embedded_string() so a run split across two
    reads is still found whole.
    """
    carry = ""
    with open(path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            chunk_text = chunk.decode("latin-1")
            combined = carry + chunk_text
            for run in _PRINTABLE_RUN_RE.findall(combined):
                if run.startswith(prefix):
                    return run
            tail_len = min(256, len(combined))
            carry = combined[len(combined) - tail_len:]
    return None


def get_file_description(path: str):
    """Read the FileDescription field from a PE binary's VersionInfo, via
    ctypes calls into version.dll (no pywin32 dependency needed). Returns
    None on any failure or when not running on Windows."""
    if os.name != "nt":
        return None
    try:
        version_dll = ctypes.windll.version  # type: ignore[attr-defined]
    except AttributeError:
        return None
    size = version_dll.GetFileVersionInfoSizeW(path, None)
    if not size:
        return None
    res = ctypes.create_string_buffer(size)
    if not version_dll.GetFileVersionInfoW(path, 0, size, res):
        return None
    lptr = ctypes.c_void_p()
    lsize = ctypes.c_uint()
    if not version_dll.VerQueryValueW(res, "\\VarFileInfo\\Translation", ctypes.byref(lptr), ctypes.byref(lsize)):
        return None
    translations = ctypes.cast(lptr, ctypes.POINTER(ctypes.c_ushort * (lsize.value // 2))).contents
    lang, codepage = translations[0], translations[1]
    subblock = f"\\StringFileInfo\\{lang:04x}{codepage:04x}\\FileDescription"
    if not version_dll.VerQueryValueW(res, subblock, ctypes.byref(lptr), ctypes.byref(lsize)):
        return None
    return ctypes.wstring_at(lptr, lsize.value - 1)


def which(name: str):
    return shutil.which(name)


def has_internet(host: str = "pypi.org", port: int = 443, timeout: float = 2.0) -> bool:
    """Fast TCP-connect check, used to skip pip's own PyPI lookups outright
    when there is no network path at all, instead of letting pip fail once
    per installed package (each failed DNS resolution is fast, so it never
    trips pip's own --timeout/--retries or the outer subprocess timeout -
    without this check the caller would misread a wall of connection
    errors as "N packages are outdated")."""
    try:
        socket.create_connection((host, port), timeout=timeout).close()
        return True
    except OSError:
        return False


def test_pip_package(name: str, pip_list_lines: list, is_mandatory: bool) -> None:
    """Required/optional Python package check, parsed from 'pip list -v'.
    Case-insensitive match: pip lists package names using their PyPI
    metadata casing (e.g. "Nuitka", not "nuitka"), and PowerShell's -match
    operator (used in the original script) is case-insensitive by default -
    matching case-sensitively here would silently miss installed packages."""
    pattern = re.compile(rf"^{re.escape(name)}\s", re.IGNORECASE)
    line = next((ln for ln in pip_list_lines if pattern.match(ln)), None)
    if line:
        ver = line.split()[1]
        write_info(f"{name} : {ver}")
    elif is_mandatory:
        write_err(f"{name} not installed (mandatory) !")
        _append_file(f"pip install {name}")
    else:
        write_warn(f"{name} not installed (optional)")


def _detect_platform_label() -> str:
    """
    Build the "<Uname>[_<OsDistro>_<Version>]" prefix for the output
    filename, mirroring chk_feature_V1R1.sh: Uname is `uname` (Linux/
    Darwin/Windows), and on Linux the distro name + version are appended
    when detectable, e.g. "Linux_AlmaLinux_10.2" or "Linux_Ubuntu_26.04".
    Checked in the same order as the shell script: /etc/system-release
    first (RHEL family: AlmaLinux, RHEL - "NAME release VERSION (CODENAME)"),
    then /etc/os-release (Debian family: Ubuntu, Debian - NAME=/VERSION_ID=
    key-value pairs). Falls back to just Uname when neither file yields a
    usable distro/version (non-Linux, or an unrecognised Linux layout).
    """
    uname = platform.system()  # 'Linux', 'Darwin', 'Windows', 'CYGWIN_NT-10.0-26200'...
    if uname != "Linux":
        # Cygwin's platform.system() includes the full Windows kernel
        # build (e.g. "CYGWIN_NT-10.0-26200") - too long/detailed for a
        # filename that already carries the OS build separately via
        # java/keytool etc. in the report body. Keep just "CYGWIN_NT-10".
        m = re.match(r"(CYGWIN_NT-\d+)", uname)
        if m:
            return m.group(1)
        return uname

    os_linux = None
    version_l = None
    try:
        if os.path.isfile("/etc/system-release"):
            with open("/etc/system-release", encoding="utf-8") as f:
                line = f.readline().strip()
            # e.g. "AlmaLinux release 10.2 (Lavender Lion)"
            m = re.match(r"^(.*?)\s+release\s+(\S+)", line)
            if m:
                os_linux = m.group(1).replace(" ", "")
                version_l = m.group(2).strip('"')
        elif os.path.isfile("/etc/os-release"):
            info = {}
            with open("/etc/os-release", encoding="utf-8") as f:
                for raw_line in f:
                    if "=" in raw_line:
                        k, _, v = raw_line.strip().partition("=")
                        info[k] = v.strip('"')
            os_linux = info.get("NAME", "").replace(" ", "")
            version_l = info.get("VERSION_ID")
    except OSError:
        pass

    if os_linux and version_l:
        return f"{uname}_{os_linux}_{version_l}"
    return uname


def _get_windows_version_info():
    """
    Return (build_number, product_type) for the real Windows OS.

    sys.getwindowsversion() only exists on a genuine win32-targeted Python
    build. If this script is launched via a Python interpreter that
    reports itself as POSIX even while running on a Windows host -- e.g.
    an MSYS2/Git-Bash/Cygwin "python" picked up from PATH -- sys.
    getwindowsversion simply doesn't exist and raises AttributeError, even
    though the OS underneath is really Windows.

    In that case, fall back to calling ntdll's RtlGetVersion() directly
    through ctypes. That's a raw Win32 API call, independent of which
    Python build is making it, and (unlike GetVersionEx) is not subject
    to the application-manifest "version lying" Windows applies to
    unmanifested processes. Kept identical to nuitka_compile_V1R1M1.py's
    helper of the same name, so both scripts detect the same OS tag
    regardless of which "python" ends up running them.

    Returns (None, None) if neither approach works (genuinely not
    running on Windows at all, or ctypes/ntdll unavailable).
    """
    try:
        wv = sys.getwindowsversion()  # type: ignore[attr-defined]
        return wv.build, wv.product_type
    except Exception:
        pass

    try:
        import ctypes

        class _OSVERSIONINFOEXW(ctypes.Structure):
            _fields_ = [
                ("dwOSVersionInfoSize", ctypes.c_ulong),
                ("dwMajorVersion", ctypes.c_ulong),
                ("dwMinorVersion", ctypes.c_ulong),
                ("dwBuildNumber", ctypes.c_ulong),
                ("dwPlatformId", ctypes.c_ulong),
                ("szCSDVersion", ctypes.c_wchar * 128),
                ("wServicePackMajor", ctypes.c_ushort),
                ("wServicePackMinor", ctypes.c_ushort),
                ("wSuiteMask", ctypes.c_ushort),
                ("wProductType", ctypes.c_byte),
                ("wReserved", ctypes.c_byte),
            ]

        info = _OSVERSIONINFOEXW()
        info.dwOSVersionInfoSize = ctypes.sizeof(_OSVERSIONINFOEXW)
        ntdll = ctypes.WinDLL("ntdll")  # type: ignore[attr-defined]
        if ntdll.RtlGetVersion(ctypes.byref(info)) == 0:
            return info.dwBuildNumber, info.wProductType
    except Exception:
        pass

    return None, None


def _detect_windows_os_tag():
    """
    Detect the short, locale-independent Windows OS tag used both in the
    report filename and in the report body - same logic as
    nuitka_compile_windows_V1R1M1.py, so the tag reported here always
    matches the naming convention actually used by the compiled binary
    ("<OsShortTag>_<SR_BASE_NAME>.exe", e.g. W11_chkcert.exe on Windows 11,
    W10_chkcert.exe on Windows 10, W2019_chkcert.exe on Server 2019).

    sys.getwindowsversion() is a Windows-only stdlib function whose
    .product_type matches WMI's ProductType exactly (1=workstation,
    2=domain controller, 3=server) - no extra dependency needed.
    _get_windows_version_info() (above) falls back to a ctypes call into
    RtlGetVersion() when sys.getwindowsversion() is unavailable (e.g. a
    non-win32 "python" from PATH, such as MSYS2/Git-Bash, running on a
    genuine Windows host). A server build number can fall under 22000 too
    (e.g. Server 2019 = 17763, Server 2022 = 20348), so build number alone
    would misclassify those as Windows 10/W10. Server editions get their
    own W20xx tag:
      26100 = Server 2025, 20348 = Server 2022, 17763 = Server 2019,
      14393 = Server 2016.
    Anything older, or undetectable (e.g. not running on Windows at all),
    falls back to "other" rather than a guess that could be wrong.

    Returns (os_caption, os_tag, os_short_tag, versionw).
    """
    os_build = None
    is_workstation = False
    os_caption = platform.platform()
    os_build, product_type = _get_windows_version_info()
    is_workstation = product_type == 1
    versionw = f"10.0.{os_build}" if os_build is not None else "unknown"

    if is_workstation and os_build is not None and os_build >= 22000:
        os_tag = "Windows_11"
        os_short_tag = "W11"
    elif is_workstation and os_build is not None:
        os_tag = "Windows_10"
        os_short_tag = "W10"
    elif os_build is not None and os_build >= 26100:
        os_tag = os_caption
        os_short_tag = "W2025"
    elif os_build is not None and os_build >= 20348:
        os_tag = os_caption
        os_short_tag = "W2022"
    elif os_build is not None and os_build >= 17763:
        os_tag = os_caption
        os_short_tag = "W2019"
    elif os_build is not None and os_build >= 14393:
        os_tag = os_caption
        os_short_tag = "W2016"
    else:
        os_tag = os_caption
        os_short_tag = "other"

    return os_caption, os_tag, os_short_tag, versionw


def main() -> int:
    global FILE01

    parser = argparse.ArgumentParser(
        prog="chk_feature_V1R1.py",
        description=(
            "Diagnostic script: reports OS, Java/keytool, runmqakm, gcc/MinGW, "
            "Python, Nuitka and chkcert build/runtime environment. Writes a "
            "timestamped report file "
            "(<W10|W11|W2019|...>_<hostname>_features_py_version_<date>_<time>.log "
            "on Windows, <Linux>_<distro>_<version>_<hostname>_features_py_version_"
            "<date>_<time>.log on Unix) in the current directory, in "
            "addition to printing to the console."
        ),
        epilog=(
            "Run this script from the directory containing chkcert_V*R*M*.py "
            "(and its compiled binary / properties folder, if present) to get "
            "a full report."
        ),
    )
    parser.parse_args()

    os.system("cls" if os.name == "nt" else "clear")

    lchkexe = "chkcert.exe"

    # ── Output filename ──────────────────────────────────────────────────
    # Mirrors chk_feature_V1R1.sh's File01/Filege construction:
    #   Date=$(date +"%Y%m%d_%HH%M")
    #   File01="${Uname}_$(hostname -f)_features_sh_version_${Date}.log"
    # extended with the distro/version detail (_detect_platform_label)
    # baked into the prefix, e.g.
    # "Linux_AlmaLinux_10.2_check-certificat.com_features_py_version_20260720_17H42.log".
    # "%HH%M" is not a typo: %H is the hour specifier, followed by a
    # literal "H", then %M for minutes - matching the shell script exactly.
    #
    # On Windows, the "Linux_AlmaLinux_10.2"-style distro/version prefix
    # has no equivalent, so the short OS tag (W10/W11/W2019/W2022/W2025/...)
    # is used instead - e.g. "W11_chkcert_V1R1M1_features_py_version_20260720_19H20.log".
    # The "_py"/"_sh"/"_ps1" tag right before "_version" identifies which
    # port of this script produced the report, since the three ports can
    # otherwise report slightly different environment details (e.g. PATH
    # bootstrap, subprocess quoting) for the same machine.
    #
    # The filename identifies which chkcert_VxRxMx.py source version was
    # analysed (not the hostname) - a fresh look-up here, since the real
    # chkcert_file detection further below only runs after os.chdir() has
    # potentially moved into python_ws. Falls back to the hostname if no
    # chkcert_V*R*M*.py is found yet (e.g. script run from the wrong
    # directory) so the report still gets a usable, unique name.
    platform_label = _detect_platform_label()
    os_caption, os_tag, os_short_tag, versionw = _detect_windows_os_tag()
    date_stamp = time.strftime("%Y%m%d_%HH%M")
    try:
        # host_f = socket.getfqdn()
        host_f = socket.gethostname()
    except OSError:
        host_f = os.environ.get("COMPUTERNAME", "unknown-host")

    python_ws = r"C:\Tools\python" if IS_WINDOWS else "."
    _early_candidates = sorted(glob.glob(os.path.join(python_ws, "chkcert_V*R*M*.py")), reverse=True)
    if _early_candidates:
        chkcert_version_tag = os.path.splitext(os.path.basename(_early_candidates[0]))[0]
    else:
        chkcert_version_tag = host_f

    file_prefix = os_short_tag if IS_WINDOWS else platform_label
    file01_name = f"{file_prefix}_{chkcert_version_tag}_features_py_version_{date_stamp}.log"
    filege_pattern_name = f"{file_prefix}_{chkcert_version_tag}_features_py_version_*.log"

    # ── Log directory: log/ under the current working directory (not the
    #    script's own location) - created on first run if missing. Falls
    #    back to the current directory itself if it cannot be created (e.g.
    #    read-only parent), so the report is never silently lost. ──
    log_dir = os.path.normpath(os.path.join(os.getcwd(), "log"))
    try:
        os.makedirs(log_dir, exist_ok=True)
    except OSError as e:
        print(f"{ANSI_YELLOW}# Warning    : could not create log directory {log_dir} ({e}) - "
              f"falling back to {os.getcwd()}{ANSI_RESET}")
        log_dir = os.getcwd()

    FILE01 = os.path.join(log_dir, file01_name)
    filege_pattern = os.path.join(log_dir, filege_pattern_name)

    # ── PATH setup (ported from Python_V312.cmd) ────────────────────────
    # WinPython, JDK, IBM MQ Client, GSKit and mingw64 under C:\Tools\ are
    # Windows-specific tool locations by design (this bootstraps a Windows
    # chkcert build environment) - on Unix/macOS none of these paths can
    # ever exist, so the whole section is skipped there rather than
    # printing one "not found" warning per hardcoded Windows path on every
    # run. Each directory is checked for existence before being added to
    # PATH: a missing one is reported as a warning but does not stop the
    # script, and is simply left out of PATH. A directory already present
    # in PATH is not added again, to avoid PATH growing unbounded across
    # repeated runs of this script within the same session.
    if not IS_WINDOWS:
        write_info("Skipping Windows-specific PATH bootstrap (WinPython/JDK/MQ Client/GSKit/mingw64) - not applicable on this platform")
    else:
        python_ws = r"C:\Tools\python"
        python_wi = r"C:\Tools\WPy64-312101\python"
        pip_wi = r"C:\Tools\WPy64-312101\python\Scripts"
        jdk_wi = r"C:\Tools\jdk-25.0.3+9\bin"
        mq_client_wi = r"C:\Tools\IBM-MQC_9.4.5.1\bin64"
        ming_wi = r"C:\Tools\mingw64\bin"
        depend_wi = r"C:\Tools\Dependencies_x64_Release"
        gskit_wi_bin = r"C:\Tools\IBM-MQC_9.4.5.1\gskit8\bin"
        gskit_wi_lib64 = r"C:\Tools\IBM-MQC_9.4.5.1\gskit8\lib64"

        path_candidates = [
            python_wi, pip_wi, jdk_wi, mq_client_wi,
            gskit_wi_bin, gskit_wi_lib64, ming_wi, depend_wi,
        ]

        # os.chdir happens before the directory checks below (which call
        # write_warn, itself appending to FILE01) so every report line lands
        # in the same directory - doing it after would split the report
        # across two different physical locations depending on whether a
        # warning fired before or after the location change.
        if os.path.isdir(python_ws):
            os.chdir(python_ws)
        else:
            write_warn(f"Working directory not found, staying in current location: {python_ws}")

        existing_path_entries = os.environ.get("PATH", "").split(os.pathsep)
        for d in path_candidates:
            if not os.path.isdir(d):
                write_warn(f"Directory not found, not added to PATH: {d}")
                continue
            if d in existing_path_entries:
                continue
            os.environ["PATH"] = d + os.pathsep + os.environ.get("PATH", "")
            existing_path_entries.append(d)

    for old in glob.glob(filege_pattern):
        try:
            os.remove(old)
        except OSError:
            pass

    write_info(f"file output:{FILE01}")
    write_info("wait few second ...")

    _append_file("")
    _append_file(f"date:{datetime.datetime.now()}")
    _append_file("hostname")
    _append_file(host_f)

    # ── OS detection ─────────────────────────────────────────────────────
    # os_caption/os_tag/os_short_tag/versionw were already computed by
    # _detect_windows_os_tag() above (needed early to build FILE01) - no
    # need to detect them again here, just report them.
    _append_file("")
    _append_file(os_caption)
    _append_file(f"{os_tag} {versionw}")

    # ── Disk space check ─────────────────────────────────────────────────
    # Nuitka --onefile can use several hundred MB of scratch space before
    # cleanup - a nearly-full disk can cause a compile to fail with an
    # unclear error partway through.
    try:
        usage = shutil.disk_usage(".")
        free_gb = round(usage.free / (1024 ** 3), 1)
        drive = os.path.splitdrive(os.path.abspath("."))[0] or "."
        _append_file(f"Free disk space on {drive}: {free_gb} GB")
        if free_gb < 2:
            write_warn(
                f"Low disk space ({free_gb} GB free) - Nuitka --onefile may fail "
                f"with an unclear error if it runs out of scratch space during compilation"
            )
    except OSError:
        pass

    # ── Write permission check ───────────────────────────────────────────
    test_file = os.path.join(os.getcwd(), ".chk_feature_write_test.tmp")
    try:
        with open(test_file, "w", encoding="utf-8") as f:
            f.write("test")
        os.remove(test_file)
        _append_file("Write permission in current directory: OK")
    except OSError:
        write_err(f"No write permission in current directory ({os.getcwd()}) - compilation and cleanup will fail")

    # ── Java / keytool ───────────────────────────────────────────────────
    _append_file("")
    _append_file("java --version")

    if which("java"):
        for ln in run_capture(["java", "--version"]):
            _append_file(ln)

        if which("keytool"):
            _append_file("keytool -version")
            kt_lines, kt_rc = run_capture_rc(["keytool", "-version"])
            for ln in kt_lines:
                _append_file(ln)
            if kt_rc != 0:
                write_warn(
                    "keytool -version is not accepted by this JDK build (exit code "
                    f"{kt_rc}) - falling back to keytool --help to confirm it still runs; "
                    "see 'java --version' above for the actual JDK version"
                )
                _append_file("keytool --help")
                for ln in run_capture(["keytool", "--help"]):
                    _append_file(ln)
            write_info(
                "You can use keytool to analysing java keystore(p12 pfx pkcs12 kdb "
                "p7b jks jck jceks) and certificat (cer cert crt pem arm der) and request csr)"
            )
        else:
            write_err(
                "You can't use keytool to analysing java keystore(p12 pfx pkcs12 kdb "
                "p7b jks jck jceks) and certificat (cer cert crt pem arm der) and request csr) !"
            )
    else:
        write_err("java not installed or not in PATH !")
        write_err(
            "You can't use keytool to analysing java keystore(p12 pfx pkcs12 kdb "
            "p7b jks jck jceks) and certificat (cer cert crt pem arm der) and request csr) !"
        )
        write_cmd(
            "Urls to download : https://adoptium.net/temurin/releases/ or "
            "https://jdk.java.net/ or https://developers.redhat.com/products/openjdk/download"
        )
        write_cmd(
            "Download the Windows x64 JDK 25 zip, extract it to C:\\Tools\\jdk-25.0.3+9 "
            "and add C:\\Tools\\jdk-25.0.3+9\\bin to PATH"
        )

    # ── runmqakm ──────────────────────────────────────────────────────────
    _append_file("")
    _append_file("runmqakm -version")

    if which("runmqakm"):
        raw_output, runmqakm_exit = run_capture_rc(["runmqakm", "-version"])
        for ln in raw_output:
            _append_file(ln)

        # Select-String is case-insensitive by default in PowerShell
        ver_line = next((ln for ln in raw_output if "productversion" in ln.lower()), None)
        if ver_line:
            ver_value = ver_line.split("Version:")[-1].strip()
            _append_file(f"runmqakm Version:{ver_value}")
            write_info("You can use runmqakm to analysing keystore cms/kdb from MQSeries")
        else:
            # No "ProductVersion" line - runmqakm.exe is present in PATH but
            # either produced no output, or printed a partial banner before
            # crashing mid-run. The most common cause is the Visual C++ 2013
            # Runtime (msvcr120.dll/msvcp120.dll) being missing.
            msg1 = (
                f"runmqakm -version did not report its ProductVersion "
                f"(exit code: {runmqakm_exit}) - it is likely failing to start "
                f"because the Visual C++ 2013 Runtime is missing (msvcr120.dll / msvcp120.dll)"
            )
            write_warn(msg1)
            _append_file(msg1)
            write_info("Install: Visual C++ Redistributable for Visual Studio 2013 (x64) - vc_redist.x64.exe")
            write_info("URL: https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist (section 'Visual Studio 2013')")
            write_warn("kdb/cms files cannot be processed until this is resolved")
    else:
        write_warn("runmqakm from MQseries (server/client) not installed !")
        write_warn("You can't use runmqakm to analysing keystore cms/kdb from MQSeries !")

    # ── Locate chkcert_VxRxMx.py (plain descending name sort) ───────────
    candidates = sorted(glob.glob("chkcert_V*R*M*.py"), reverse=True)
    chkcert_file = candidates[0] if candidates else None
    if not chkcert_file:
        write_err("chkcert_V*R*M*.py not found !")

    # ── gcc (MinGW64 / winlibs) ───────────────────────────────────────────
    _append_file("")
    _append_file("gcc --version")

    version_gc_current = None
    if which("gcc"):
        gcc_lines = run_capture(["gcc", "--version"])
        gcc_line = gcc_lines[0] if gcc_lines else ""
        _append_file(gcc_line)
        m = re.search(r"(\d+\.\d+\.\d+)", gcc_line)
        if m:
            version_gc_current = m.group(1)
        write_info(f"Current version GCC {gcc_line}")
        write_info(
            "Tip: exclude the build directory from Windows Defender/antivirus "
            "real-time scanning - scanning freshly-created binaries mid-write is "
            "a known cause of spurious Nuitka build failures"
        )
    else:
        write_err("gcc not installed !")
        write_err(f"You can't compile {chkcert_file} to {lchkexe} !")
        write_cmd("Nuitka downloads its own pinned winlibs-gcc build automatically on first compile")
        write_cmd("On a machine without Internet access, pre-stage the exact zip named in Nuitka's")
        write_cmd(r"error message under $env:LOCALAPPDATA\Nuitka\Nuitka\Cache\downloads\gcc\x86_64\ - see prerequisites")

    # ── Python / pip ──────────────────────────────────────────────────────
    _append_file("")
    _append_file("python -V")

    py_version = None
    pip_list_output = []
    if which("python"):
        py_version_lines = run_capture(["python", "-V"])
        py_version_line = py_version_lines[0] if py_version_lines else ""
        _append_file(py_version_line)
        m = re.search(r"(\d+\.\d+\.\d+)", py_version_line)
        if m:
            py_version = m.group(1)

        if which("pip"):
            for ln in run_capture(["pip", "-V"]):
                _append_file(ln)
        else:
            write_warn("pip not installed or not in PATH !")
            write_cmd("python -m ensurepip --upgrade")

        # ── Multiple python(.exe) in PATH ─────────────────────────────────
        # The first match wins silently - worth flagging since it's a common
        # source of "wrong environment" confusion.
        python_bin_name = "python.exe" if IS_WINDOWS else "python"
        all_pythons = []
        for p in os.environ.get("PATH", "").split(os.pathsep):
            candidate = os.path.join(p, python_bin_name)
            if os.path.isfile(candidate):
                all_pythons.append(candidate)
        if len(all_pythons) > 1:
            write_warn(f"Multiple {python_bin_name} found in PATH ({len(all_pythons)}) - the first one is used, verify this is the intended one")
            for p in all_pythons:
                _append_file(p)

        # ── OS / Python architecture consistency ─────────────────────────
        os_arch_bits = "64" if sys.maxsize > 2 ** 32 else "32"
        py_arch_bits = str(struct.calcsize("P") * 8)
        _append_file(f"OS architecture: {os_arch_bits}-bit | Python architecture: {py_arch_bits}-bit")
        if py_arch_bits and py_arch_bits.strip() != os_arch_bits:
            write_warn(f"Architecture mismatch: OS is {os_arch_bits}-bit but Python is {py_arch_bits}-bit - a known source of MinGW/Nuitka build failures")
    else:
        write_warn("python not installed or not in PATH !")
        write_cmd("Install WinPython or python.org distribution, then add its Scripts\\ and root folder to PATH")

    _append_file("")
    _append_file("pip list -v")
    pip_list_output = run_capture(["pip", "list", "-v"])
    for ln in pip_list_output:
        _append_file(ln)

    _append_file("")
    _append_file("python -m nuitka --version")
    for ln in run_capture(["python", "-m", "nuitka", "--version"]):
        _append_file(ln)

    # ── Required/optional Python package check ───────────────────────────
    _append_file("")
    _append_file("Required Python packages: cryptography, nuitka, paramiko, zstandard | Optional: colorama")
    write_info("Required Python packages: cryptography, nuitka, paramiko, zstandard | Optional: colorama")
    test_pip_package("cryptography", pip_list_output, True)
    test_pip_package("nuitka", pip_list_output, True)
    test_pip_package("paramiko", pip_list_output, True)
    test_pip_package("zstandard", pip_list_output, True)
    test_pip_package("colorama", pip_list_output, False)

    _append_file("")
    _append_file("pip list --outdated")
    # pip list --outdated calls out to PyPI to compare installed vs
    # latest versions - on a server with no or partial internet access
    # (common for these build machines, which are often deliberately
    # locked down) this can otherwise hang indefinitely: pip's own
    # --retries/--timeout only bound each individual socket attempt, not
    # a DNS resolution that never returns, and with the default 5
    # retries that's still a long wait before pip gives up on its own.
    # --retries 1 --timeout 5 make pip fail fast when it can fail on its
    # own; the outer subprocess timeout is the hard backstop for cases
    # even that doesn't save it from (e.g. a hung DNS lookup).
    try:
        no_internet = not has_internet()
        if no_internet:
            outdated_output = []
            outdated_timed_out = False
            write_err("No Internet access detected (pypi.org:443 unreachable) - "
                       "'pip list --outdated' skipped")
        else:
            outdated_proc = subprocess.run(
                ["pip", "list", "--outdated", "--disable-pip-version-check",
                 "--retries", "1", "--timeout", "5"],
                capture_output=True, text=True, encoding="utf-8", errors="replace",
                timeout=30,
            )
            outdated_output = ((outdated_proc.stdout or "") + (outdated_proc.stderr or "")).splitlines()
            outdated_timed_out = False
    except subprocess.TimeoutExpired:
        no_internet = False
        outdated_output = []
        outdated_timed_out = True
    except (FileNotFoundError, OSError):
        no_internet = False
        outdated_output = []
        outdated_timed_out = False

    for ln in outdated_output:
        _append_file(ln)
    # First 2 lines are the "Package / Version / Latest / Type" header +
    # separator, so more than that means at least one package is genuinely
    # outdated.
    if no_internet:
        pass  # already reported above via write_err(); nothing more to say
    elif outdated_timed_out:
        write_warn(
            "pip list --outdated timed out after 30s (no/partial internet "
            "access on this machine?) - skipped, not treated as an error"
        )
    elif len(outdated_output) > 2:
        write_warn("Some pip packages are outdated - see 'pip list --outdated' above")
        write_info(
            'pip list --outdated --format=json | python -c '
            '"import json,sys; [print(p[\'name\']) for p in json.load(sys.stdin)]" '
            '| xargs -n1 pip install --upgrade'
        )
    else:
        write_ok("All pip packages are up to date")

    # ── Source usage + imports ───────────────────────────────────────────
    sr_base_name = "chkcert"
    if chkcert_file:
        _append_file("")
        write_info(f"Source utilisation : {chkcert_file}")
        _append_file(f"python {chkcert_file} --version")
        for ln in remove_blank_lines(run_capture(["python", chkcert_file, "--version"])):
            _append_file(ln)

        _append_file("")
        _append_file(f"cat {chkcert_file}")
        import_re = re.compile(r"^\s*(import|from|colorama)\b")
        with open(chkcert_file, encoding="utf-8", errors="replace") as f:
            for raw_line in f:
                line = raw_line.strip()
                if import_re.match(raw_line) and not line.startswith("#") and "from this certificat" not in line:
                    _append_file(line)

        # ── VERSION constant vs. filename version cross-check ────────────
        version_match = None
        with open(chkcert_file, encoding="utf-8", errors="replace") as f:
            for raw_line in f:
                m = re.match(r'^\s*VERSION\s*=\s*"([^"]+)"', raw_line)
                if m:
                    version_match = m
                    break
        _append_file("")
        _append_file("Check: does the VERSION constant inside the source match the version encoded in the filename ?")
        if version_match:
            internal_version = version_match.group(1)
            filename_no_ext = Path(chkcert_file).stem
            filename_base = filename_no_ext.split("_")[0]
            filename_version = filename_no_ext[len(filename_base) + 1:]
            _append_file(f"VERSION in source: {internal_version} | filename version: {filename_version}")
            if internal_version != filename_version:
                write_err(f'Version mismatch: source has VERSION="{internal_version}" but filename ({chkcert_file}) says {filename_version} !')
            else:
                write_ok(f"Source VERSION matches filename ({internal_version})")
        else:
            write_warn(f"VERSION constant not found in {chkcert_file}")

        chkcert_script_no_ext = Path(chkcert_file).stem
        sr_base_name = chkcert_script_no_ext.split("_")[0]
    else:
        write_warn("No source script python chkcert found !")

    # ── Compiled binary + GCC version cross-check ────────────────────────
    # nuitka_compile_V1R1M1.py names the output binary
    # "<file_prefix>_<SR_BASE_NAME>.exe" - file_prefix is the same tag used
    # for the report filename above: the short Windows OS tag (W10/W11/...)
    # on Windows, or the distro/version label (e.g. "Linux_AlmaLinux_10.2")
    # on Linux. Using os_short_tag alone here would be wrong on Linux -
    # _detect_windows_os_tag() only knows about Windows and falls back to
    # "other" there, which would never match the actual compiled binary.
    chkcert_exe_name = f"{file_prefix}_{sr_base_name}.exe"
    chkcert_exe = chkcert_exe_name if os.path.isfile(chkcert_exe_name) else None

    version_gc_compiled = None
    if chkcert_exe:
        _append_file("")
        write_info(f"Version compiled gcc: {chkcert_exe}")

        exe_size_mb = round(os.path.getsize(chkcert_exe) / (1024 * 1024), 1)
        gcc_line_exe = None
        start = time.monotonic()

        # Fast path: read the GCC/Python/Nuitka versions embedded in the
        # binary's FileDescription metadata (written by
        # nuitka_compile_windows_V1R1M1.py at compile time), instead of
        # scanning the whole file for embedded strings.
        file_description = get_file_description(chkcert_exe)
        _append_file(f"FileDescription: {file_description}")
        if file_description:
            m = re.search(r"gcc_V(\d+\.\d+\.\d+)", file_description)
            if m:
                version_gc_compiled = m.group(1)
                gcc_line_exe = f"GCC: {version_gc_compiled} (from FileDescription metadata)"

            m = re.search(r"Python_V(\d+\.\d+\.\d+)", file_description)
            if m:
                py_version_compiled = m.group(1)
                py_version_current_trim = py_version.strip() if py_version else None
                _append_file(f"Python compiled: '{py_version_compiled}' | Python current: '{py_version_current_trim}'")
                if py_version_current_trim and py_version_compiled != py_version_current_trim:
                    write_warn(f"Difference version: {chkcert_exe} was compiled with Python {py_version_compiled} but current Python is {py_version_current_trim} !")

            m = re.search(r"Nuika_V(\d+\.\d+\.\d+)", file_description)
            if m:
                nu_version_compiled = m.group(1)
                nu_version_current_lines = run_capture(["python", "-m", "nuitka", "--version"])
                nu_version_current_trim = nu_version_current_lines[0].strip() if nu_version_current_lines else None
                _append_file(f"Nuitka compiled: '{nu_version_compiled}' | Nuitka current: '{nu_version_current_trim}'")
                if nu_version_current_trim and nu_version_compiled != nu_version_current_trim:
                    write_warn(f"Difference version: {chkcert_exe} was compiled with Nuitka {nu_version_compiled} but current Nuitka is {nu_version_current_trim} !")

        # ── Source vs. binary freshness check ────────────────────────────
        if chkcert_file:
            src_time = datetime.datetime.fromtimestamp(os.path.getmtime(chkcert_file))
            exe_time = datetime.datetime.fromtimestamp(os.path.getmtime(chkcert_exe))
            _append_file(f"Source last modified: {src_time} | Binary last modified: {exe_time}")
            if src_time > exe_time:
                write_warn(f"{chkcert_file} was modified after {chkcert_exe} was compiled - the binary may not reflect the current source, recompilation recommended")

        # Fallback: only scan the whole file if the metadata doesn't have
        # the expected marker (e.g. a binary compiled before this change).
        if not gcc_line_exe:
            _append_file("gcc_VX.X.X not found in FileDescription - falling back to full-file scan")
            _append_file(f"strings {chkcert_exe} | Select-String ^GCC   (file size: {exe_size_mb} MB)")
            try:
                # Primary: only considers genuine printable runs delimited by
                # non-printable bytes (like the `strings` utility), so it
                # can't return a match straddling two unrelated byte runs.
                # find_embedded_string() greps the raw file directly instead,
                # which was observed in practice to return corrupted matches
                # (e.g. "GCCCCC") — kept only as a fallback here.
                gcc_line_exe = find_string_with_prefix(chkcert_exe, "GCC:")
                if not gcc_line_exe:
                    gcc_line_exe = find_embedded_string(chkcert_exe, r"GCC[\x20-\x7E]{3,}")
                if gcc_line_exe:
                    m = re.search(r"(\d+\.\d+\.\d+)\s*$", gcc_line_exe) or re.search(r"(\d+\.\d+\.\d+)", gcc_line_exe)
                    if m:
                        version_gc_compiled = m.group(1)
            except OSError as e:
                write_warn(f"GCC string lookup in {chkcert_exe} failed: {e}")
        elapsed_lookup = time.monotonic() - start
        _append_file(f"(lookup took {elapsed_lookup} s)")

        if gcc_line_exe:
            _append_file(gcc_line_exe)
            _append_file("")
            write_info(f"{chkcert_exe} compiled version GCC {gcc_line_exe}")

            _append_file("")
            chkcert_exe_run_path = os.path.join(".", chkcert_exe)
            _append_file(f"{chkcert_exe_run_path} --version")
            exe_version_output = run_capture([chkcert_exe_run_path, "--version"])
            for ln in format_chkcert_version(exe_version_output):
                _append_file(ln)

            version_gc_current_clean = version_gc_current.strip() if version_gc_current else None
            version_go_compiled_clean = version_gc_compiled.strip() if version_gc_compiled else None
            _append_file("")
            _append_file(f"Check: does the GCC version currently installed match the one used to compile {chkcert_exe} ?")
            _append_file(f"GCC current: '{version_gc_current_clean}' | GCC compiled: '{version_go_compiled_clean}'")
            if version_gc_current_clean and version_go_compiled_clean and version_gc_current_clean != version_go_compiled_clean:
                write_warn(f"Difference version {version_go_compiled_clean} gcc objet compiled {chkcert_exe} with gcc current version {version_gc_current_clean} !")
        else:
            write_warn(f"No embedded GCC string found in {chkcert_exe} (binary may be stripped or use a different toolchain marker).")
    else:
        write_warn(f"No compiled object found: {chkcert_exe_name}")

    # ── Link (.lnk) presence + validity check ────────────────────────────
    # nuitka_compile_V1R1M1.py names this link differently per platform:
    # on Windows it's "<chkcert_exe_name>.lnk" next to the OS-tagged
    # binary (a real symlink when Developer Mode/Administrator privileges
    # allow it, otherwise a plain-copy fallback); on Linux it's the plain
    # untagged launcher name "<sr_base_name>.exe" (e.g. "chkcert.exe")
    # pointing at the distro-tagged binary. subprocess.run() calls
    # CreateProcess directly, so unlike the PowerShell '&' operator it
    # runs either kind of file without any extension-association
    # workaround being needed.
    chkcert_lnk_name = f"{chkcert_exe_name}.lnk" if IS_WINDOWS else f"{sr_base_name}.exe"
    _append_file("")
    if os.path.exists(chkcert_lnk_name):
        lnk_target_path = None
        lnk_kind = None

        if os.path.islink(chkcert_lnk_name):
            lnk_kind = "NTFS symlink" if IS_WINDOWS else "symlink"
            lnk_target_path = os.readlink(chkcert_lnk_name)
            # os.readlink() returns the raw link target exactly as stored
            # (nuitka_compile_V1R1M1.py creates it as a bare relative name,
            # e.g. "Linux_Ubuntu_26.04_chkcert.exe", no "./" prefix). On
            # Windows, CreateProcess implicitly searches the current
            # directory for a bare filename, so that's runnable as-is; on
            # Unix, execvp() only searches PATH for names without a "/" in
            # them - the current directory is deliberately not searched
            # for security reasons - so a bare relative target must be
            # re-anchored to the link's own directory before it can be
            # run or checked for existence.
            if not IS_WINDOWS and not os.path.isabs(lnk_target_path):
                lnk_target_path = os.path.join(
                    os.path.dirname(os.path.abspath(chkcert_lnk_name)), lnk_target_path
                )
        else:
            # A genuine Windows Shell Shortcut (.lnk binary format) requires
            # COM (win32com.client, part of pywin32) to resolve reliably -
            # attempt it optionally, and fall back to treating the file as
            # a plain-copy binary (nuitka_compile_windows_V1R1M1.py's own
            # fallback when it lacks symlink privileges) otherwise.
            try:
                import win32com.client  # type: ignore[import]
                shell = win32com.client.Dispatch("WScript.Shell")
                shortcut = shell.CreateShortCut(os.path.abspath(chkcert_lnk_name))
                if shortcut.Targetpath:
                    lnk_kind = "Windows Shell Shortcut"
                    lnk_target_path = shortcut.Targetpath
            except Exception:
                pass

        if not lnk_kind:
            lnk_kind = "plain copy"
            lnk_target_path = os.path.abspath(chkcert_lnk_name)

        lnk_size_kb = round(os.path.getsize(chkcert_lnk_name) / 1024, 1)
        write_info(f"{chkcert_lnk_name} is a {lnk_kind} -> {lnk_target_path} (size: {lnk_size_kb} KB)")

        if not os.path.exists(lnk_target_path):
            write_warn(f"{chkcert_lnk_name} target does not exist: {lnk_target_path}")
        else:
            lnk_version_output, lnk_rc = run_capture_rc([lnk_target_path, "--version"])
            if lnk_rc != 0 or not lnk_version_output:
                write_warn(f"{chkcert_lnk_name} target failed to run or produced no output -- link may be broken or stale")
                _append_file(f"{lnk_target_path} --version")
                for ln in format_chkcert_version(lnk_version_output):
                    _append_file(ln)
            elif chkcert_exe:
                exe_version_output = run_capture([os.path.join(".", chkcert_exe), "--version"])
                if "\n".join(lnk_version_output) != "\n".join(exe_version_output):
                    write_warn(f"{chkcert_lnk_name} --version output differs from {chkcert_exe} -- link may be stale (pointing to an older build)")
                    _append_file(f"{lnk_target_path} --version")
                    for ln in format_chkcert_version(lnk_version_output):
                        _append_file(ln)
                else:
                    write_ok(f"{chkcert_lnk_name} is valid and matches {chkcert_exe}")
                    _append_file(f"{chkcert_lnk_name} --version matches {chkcert_exe} (output not repeated, see above)")
    else:
        write_warn(f"No link found: {chkcert_lnk_name}")

    # ── properties/ directory + config files check ───────────────────────
    # chkcert_V1R1M1.py reads properties\class_bcolors.py (colour palette)
    # and properties\constantes_V1R1M1.properties (runtime overrides) from
    # the same directory as the script/binary. Both are optional - their
    # absence just means built-in defaults apply - so this section reports
    # status without ever treating a missing file as fatal.
    _append_file("")
    write_info("properties directory + config files check")

    properties_dir = os.path.join(".", "properties")
    licence_found = False
    if not os.path.isdir(properties_dir):
        write_warn(f"properties directory not found: {properties_dir} (built-in defaults will be used)")
    else:
        write_ok(f"properties directory found: {properties_dir}")

        bcolors_file = os.path.join(properties_dir, "class_bcolors.py")
        constants_file = os.path.join(properties_dir, "constantes_V1R1M1.properties")

        if os.path.isfile(bcolors_file):
            write_ok(f"class_bcolors.py found: {bcolors_file}")
        else:
            write_warn(f"class_bcolors.py not found: {bcolors_file} (built-in colour palette will be used)")

        if os.path.isfile(constants_file):
            write_ok(f"constantes_V1R1M1.properties found: {constants_file}")

            # Same parsing rules as chkcert_V1R1M1.py's
            # _parse_properties_file(): '#'/';' comment lines ignored,
            # KEY=VALUE split on the first '=', surrounding matching quotes
            # stripped, keys matched case-insensitively.
            props_raw = {}
            with open(constants_file, encoding="utf-8") as f:
                for raw_line in f:
                    line = raw_line.strip()
                    if line == "" or line.startswith("#") or line.startswith(";") or "=" not in line:
                        continue
                    key, _, val = line.partition("=")
                    key = key.strip().upper()
                    val = val.strip()
                    if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
                        val = val[1:-1]
                    props_raw[key] = val

            expected_vars = [
                "PASSWORD_DFT",
                "MIN_RSA_KEY_SIZE",
                "CMD_RSA_KEY_SIZE",
                "W_FLD_TARGET_ANA_CER",
                "U_FLD_TARGET_ANA_CER",
                "INDEX_ALIAS_SERIAL",
                "INDEX_SERIAL_ALIAS",
            ]
            for var_name in expected_vars:
                if var_name in props_raw:
                    write_ok(f"{var_name} = {props_raw[var_name]}")
                else:
                    write_warn(f"{var_name} not present in constantes_V1R1M1.properties (built-in default will be used)")
        else:
            write_warn(f"constantes_V1R1M1.properties not found: {constants_file} (built-in defaults will be used)")

        # licence.key: Enterprise licence file. Optional - its absence just
        # means the tool runs in trial mode - so a missing file is a
        # warning, not an error.
        licence_file = os.path.join(properties_dir, "licence.key")
        if os.path.isfile(licence_file):
            licence_found = True
            write_ok(f"licence.key found: {licence_file}")
            with open(licence_file, encoding="utf-8") as f:
                licence_raw = f.read()
            print(licence_raw)
            _append_file(licence_raw)  # was console-only before — now also lands in the report

            try:
                licence_data = json.loads(licence_raw)
                # tier: the single source of truth in chkcert_V1R1M1.py / build_chkcert_exe.py
                # for feature gating (e.g. --remote). Must match REMOTE_ALLOWED_TIERS there.
                tier = str(licence_data.get("tier", "")).lower()
                if tier:
                    remote_label = "included" if tier in ("team", "pro", "enterprise") else "not included (Solo tier)"
                    write_ok(f"Licence tier: {tier} — remote execution (--remote): {remote_label}")
                else:
                    write_warn("licence.key has no 'tier' field — pre-tier licence, will not validate under the current chkcert_V1R1M1.py (re-issue with build_chkcert_exe.py --tier)")
                expires = licence_data.get("expires")
                if expires and expires != "never":
                    expiry_date = datetime.datetime.strptime(expires, "%Y-%m-%d").date()
                    days_left = (expiry_date - datetime.date.today()).days
                    if days_left < 0:
                        write_warn(f"Licence EXPIRED on {expires} ({abs(days_left)} day(s) ago)")
                    else:
                        write_ok(f"Licence expires on {expires} ({days_left} day(s) remaining)")
                elif expires == "never":
                    write_ok("Licence never expires")
            except (json.JSONDecodeError, ValueError) as e:
                write_warn(f"licence.key found but could not be parsed as JSON: {e}")
        else:
            write_warn(f"licence.key not found: {licence_file} (tool will run in trial mode)")

    _append_file("")

    # ── Final rename: only flags an incomplete state, derived automatically ──
    #   no compiled object at all               -> renamed to _before_no_licence
    #   compiled object present, no licence.key -> renamed to _before_no_licence
    #     (still not a fully licensed deployment)
    #   compiled object present AND licence.key  -> kept as-is (nothing to flag)
    has_exe = chkcert_exe is not None
    if has_exe and licence_found:
        write_info(f"Report kept as: {FILE01} (compiled object + licence.key both present)")
    else:
        state_suffix = "before_no_licence"
        final_file = FILE01[:-4] + f"_{state_suffix}.log" if FILE01.endswith(".log") else f"{FILE01}_{state_suffix}"
        try:
            os.replace(FILE01, final_file)
            old_file = FILE01
            FILE01 = final_file
            write_info(f"Report renamed: {old_file} -> {final_file}")
        except OSError as e:
            write_warn(f"Could not rename report to {final_file}: {e}")

    print()
    if FILE01 and os.path.isfile(FILE01):
        with open(FILE01, encoding="utf-8") as f:
            print(f.read())

    return 0


if __name__ == "__main__":
    sys.exit(main())
