#!/usr/bin/env bash
# chk_feature_V1R1.sh - Bash port of the Unix branch of chk_feature_V1R1.py
# Diagnostic script: reports OS, Java/keytool, runmqakm, gcc, Python,
# Nuitka and chkcert build/runtime environment. Writes a timestamped
# report file (<Linux>_<distro>_<version>_<hostname>_features_sh_version_
# <date>_<time>.log) in the current directory, in addition to printing
# to the console. The "_sh" tag right before "_version" identifies this
# bash port specifically, since the .py/.sh/.ps1 ports can otherwise
# report slightly different environment details for the same machine.
#
# 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.
#
# Notes on fidelity vs chk_feature_V1R1.py:
#   - get_file_description() (reading a Windows PE binary's
#     FileDescription via ctypes/version.dll) is Windows-only in the
#     Python original and always returns None on Unix, so it is not
#     ported here at all -- this script always uses the "full-file
#     scan" fallback (an embedded "GCC ..." string, which gcc itself
#     writes into the ELF .comment section) to identify the compiler
#     used to build the binary.
#   - licence.key is parsed for its "expires" field with a GNU grep -P
#     extraction rather than a real JSON parser (no JSON tooling in
#     plain bash) -- sufficient for this fixed, known file format.
#   - platform.platform()'s exact Linux caption string ("Linux-<kernel>
#     -<machine>-with-glibc<version>") is approximated the same way;
#     cosmetic only, not used for any file naming or logic.

set -u

ESC=$'\033'
ANSI_RED="${ESC}[91m"
ANSI_GREEN="${ESC}[92m"
ANSI_YELLOW="${ESC}[93m"
ANSI_CYAN="${ESC}[96m"
ANSI_MAGENTA="${ESC}[95m"
ANSI_RESET="${ESC}[0m"

FILE01=""

append_file() {
    if [ -n "$FILE01" ]; then
        printf '%s\n' "$1" >>"$FILE01"
    fi
}

write_info() { printf '%s# Info       : %s%s\n' "$ANSI_CYAN" "$1" "$ANSI_RESET"; append_file "${ANSI_CYAN}# Info       : $1${ANSI_RESET}"; }
write_warn() { printf '%s# Warning    : %s%s\n' "$ANSI_YELLOW" "$1" "$ANSI_RESET"; append_file "${ANSI_YELLOW}# Warning    : $1${ANSI_RESET}"; }
write_err()  { printf '%s# Error      : %s%s\n' "$ANSI_RED" "$1" "$ANSI_RESET"; append_file "${ANSI_RED}# Error      : $1${ANSI_RESET}"; }
write_cmd()  { printf '%s# Command    : %s%s\n' "$ANSI_MAGENTA" "$1" "$ANSI_RESET"; append_file "${ANSI_MAGENTA}# Command    : $1${ANSI_RESET}"; }
write_ok()   { printf '%s# OK         : %s%s\n' "$ANSI_GREEN" "$1" "$ANSI_RESET"; append_file "${ANSI_GREEN}# OK         : $1${ANSI_RESET}"; }

# run_capture CMD... : prints combined stdout+stderr to stdout, one line
# per output line, and never aborts the script on a non-zero exit or a
# missing executable (matching subprocess.run()'s tolerant capture).
run_capture() {
    "$@" 2>&1
    return 0
}

# format_chkcert_version: drop blank lines and the fixed legal
# paragraph, keeping just the name/version + copyright lines. Reads
# lines from stdin, one per line.
format_chkcert_version() {
    grep -v '^[[:space:]]*$' | grep -vE '^(This software|Licensed for|resale, reverse|is strictly|Use of this|provided by)'
}

# remove_blank_lines: strip only the blank lines.
remove_blank_lines() {
    grep -v '^[[:space:]]*$'
}

# has_internet: fast TCP-connect check against pypi.org:443, used to skip
# pip's own PyPI lookups outright when there is no network path at all --
# without it, a failed DNS resolution is fast enough that it never trips
# pip's own --timeout/--retries or the outer `timeout` wrapper, so the
# caller would otherwise misread a wall of connection errors as
# "N packages are outdated". Uses bash's /dev/tcp pseudo-device (no
# external dependency); falls back to `true` (assume reachable, let pip
# itself fail/timeout) if /dev/tcp is unavailable, e.g. under dash-based
# `sh` rather than bash, or a hardened bash build without net redirection.
has_internet() {
    local host="${1:-pypi.org}" port="${2:-443}" timeout_s="${3:-2}"
    if command -v timeout >/dev/null 2>&1; then
        timeout "$timeout_s" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null
        local rc=$?
        return $rc
    fi
    (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null
}

# find_embedded_string PATH PATTERN: first match of an extended-regex
# PATTERN found anywhere in the (possibly binary) file at PATH -- the
# Unix equivalent of `strings PATH | grep -oE PATTERN | head -n1`,
# done directly against the raw file so it works even where the
# `strings` utility itself isn't installed.
find_embedded_string() {
    grep -a -o -m1 -E "$2" "$1" 2>/dev/null
}

# test_pip_package NAME PIP_LIST_FILE IS_MANDATORY : required/optional
# Python package check, parsed from a `pip list -v` capture stored in
# the file at PIP_LIST_FILE (case-insensitive match, since pip lists
# package names using their PyPI metadata casing, e.g. "Nuitka" not
# "nuitka").
test_pip_package() {
    local name="$1" pip_list_file="$2" is_mandatory="$3"
    local line ver
    line=$(grep -im1 -E "^${name}[[:space:]]" "$pip_list_file" 2>/dev/null)
    if [ -n "$line" ]; then
        ver=$(awk '{print $2}' <<<"$line")
        write_info "${name} : ${ver}"
    elif [ "$is_mandatory" = "1" ]; then
        write_err "${name} not installed (mandatory) !"
        append_file "pip install ${name}"
    else
        write_warn "${name} not installed (optional)"
    fi
}

# _detect_platform_label: build the "<Uname>[_<OsDistro>_<Version>]"
# prefix for the output filename -- Uname is `uname -s` (Linux/
# Darwin), 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 chk_feature_V1R1.py: /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 the uname
# (e.g. "Darwin") when neither file yields a usable distro/version.
_detect_platform_label() {
    local uname_s os_linux version_l line
    uname_s=$(uname -s)
    if [ "$uname_s" != "Linux" ]; then
        # Cygwin's `uname -s` 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".
        if [[ "$uname_s" =~ ^(CYGWIN_NT-[0-9]+) ]]; then
            printf '%s' "${BASH_REMATCH[1]}"
        else
            printf '%s' "$uname_s"
        fi
        return 0
    fi

    os_linux=""
    version_l=""
    if [ -f /etc/system-release ]; then
        line=$(head -n1 /etc/system-release)
        if [[ "$line" =~ ^(.*)[[:space:]]release[[:space:]]([^[:space:]]+) ]]; then
            os_linux="${BASH_REMATCH[1]// /}"
            version_l="${BASH_REMATCH[2]}"
            version_l="${version_l%\"}"
            version_l="${version_l#\"}"
        fi
    elif [ -f /etc/os-release ]; then
        os_linux=$(. /etc/os-release; printf '%s' "${NAME// /}")
        version_l=$(. /etc/os-release; printf '%s' "${VERSION_ID:-}")
    fi

    if [ -n "$os_linux" ] && [ -n "$version_l" ]; then
        printf '%s_%s_%s' "$uname_s" "$os_linux" "$version_l"
    else
        printf '%s' "$uname_s"
    fi
}

# _platform_caption: best-effort approximation of Python's
# platform.platform() string on Linux, e.g.
# "Linux-6.18.5-x86_64-with-glibc2.39" -- cosmetic only (report display
# text), not used in any file naming or comparison logic.
_platform_caption() {
    local kernel machine libc_ver
    kernel=$(uname -r)
    machine=$(uname -m)
    libc_ver=$(ldd --version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1)
    if [ -n "$libc_ver" ]; then
        printf 'Linux-%s-%s-with-glibc%s' "$kernel" "$machine" "$libc_ver"
    else
        printf 'Linux-%s-%s' "$kernel" "$machine"
    fi
}

main() {
    clear

    local lchkexe="chkcert.exe"

    # ── Output filename ──────────────────────────────────────────────
    # 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 a possible cd
    # into python_ws (Cygwin). 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.
    local platform_label date_stamp host_f file_prefix filege_pattern
    local uname_prefix_early chkcert_glob_dir chkcert_version_tag
    platform_label=$(_detect_platform_label)
    date_stamp=$(date +"%Y%m%d_%HH%M")
    host_f=$(hostname 2>/dev/null)
    [ -n "$host_f" ] || host_f="unknown-host"

    uname_prefix_early=$(uname | cut -d'_' -f1)
    if [ "$uname_prefix_early" = "CYGWIN" ]; then
        chkcert_glob_dir="/drives/c/Tools/python"
    else
        chkcert_glob_dir="."
    fi
    local -a _early_candidates=()
    shopt -s nullglob
    _early_candidates=("${chkcert_glob_dir}"/chkcert_V*R*M*.py)
    shopt -u nullglob
    if [ "${#_early_candidates[@]}" -gt 0 ]; then
        mapfile -t _early_candidates < <(printf '%s\n' "${_early_candidates[@]}" | sort -r)
        chkcert_version_tag=$(basename -- "${_early_candidates[0]}")
        chkcert_version_tag="${chkcert_version_tag%.py}"
    else
        chkcert_version_tag="$host_f"
    fi

    file_prefix="$platform_label"
    local file01_name="${file_prefix}_${chkcert_version_tag}_features_sh_version_${date_stamp}.log"
    local filege_pattern_name="${file_prefix}_${chkcert_version_tag}_features_sh_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. ──
    local _cwd
    _cwd="$(pwd -P)"
    local log_dir="${_cwd}/log"
    if mkdir -p -- "$log_dir" 2>/dev/null; then
        log_dir="$(cd -- "$log_dir" >/dev/null 2>&1 && pwd -P)"
    else
        echo "# Warning    : could not create log directory ${log_dir} - falling back to ${_cwd}" >&2
        log_dir="$_cwd"
    fi

    FILE01="${log_dir}/${file01_name}"
    filege_pattern="${log_dir}/${filege_pattern_name}"

    # ── PATH setup ────────────────────────────────────────────────────
    # WinPython, JDK, IBM MQ Client and mingw64 under C:\Tools\ are
    # Windows tool locations. On true Unix/Linux they can never exist,
    # so this whole section is skipped there. On Cygwin, however, this
    # script runs as a Unix-like shell ON TOP OF Windows, and those
    # same C:\Tools\... paths DO exist and matter (e.g. gcc --version
    # further down needs mingw64\bin on PATH) - just reached through
    # Cygwin's drive mount instead of a drive letter.
    local uname_prefix
    uname_prefix=$(uname | cut -d'_' -f1)

    if [ "$uname_prefix" = "CYGWIN" ]; then
        local drives="/drives/c/"
        local python_ws="${drives}Tools/python"
        local python_wi="${drives}Tools/WPy64-312101/python"
        local pip_wi="${drives}Tools/WPy64-312101/python/Scripts"
        local jdk_wi="${drives}Tools/jdk-25.0.3+9/bin"
        local mq_client_wi="${drives}Tools/IBM-MQC_9.4.5.1/bin64"
        local ming_wi="${drives}Tools/mingw64/bin"

        export PATH="${python_wi}:${pip_wi}:${jdk_wi}:${mq_client_wi}:${ming_wi}:${PATH}"

        if [ -d "$python_ws" ]; then
            cd "$python_ws" || write_warn "Working directory not found, staying in current location: $python_ws"
        else
            write_warn "Working directory not found, staying in current location: $python_ws"
        fi
    else
        write_info "Skipping Windows-specific PATH bootstrap (WinPython/JDK/MQ Client/mingw64) - not applicable on this platform"
    fi

    for old in $filege_pattern; do
        [ -e "$old" ] && rm -f -- "$old"
    done

    write_info "file output:${FILE01}"
    write_info "wait few second ..."

    append_file ""
    append_file "date:$(date '+%Y-%m-%d %H:%M:%S.%6N')"
    append_file "hostname"
    append_file "$host_f"

    # ── OS detection ──────────────────────────────────────────────────
    local os_caption
    os_caption=$(_platform_caption)
    append_file ""
    append_file "$os_caption"
    append_file "${os_caption} unknown"

    # ── 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.
    local free_kb free_gb
    free_kb=$(df -Pk . 2>/dev/null | awk 'NR==2{print $4}')
    if [ -n "$free_kb" ]; then
        free_gb=$(awk -v k="$free_kb" 'BEGIN{printf "%.1f", k/1024/1024}')
        append_file "Free disk space on .: ${free_gb} GB"
        if awk -v g="$free_gb" 'BEGIN{exit !(g<2)}'; then
            write_warn "Low disk space (${free_gb} GB free) - Nuitka --onefile may fail with an unclear error if it runs out of scratch space during compilation"
        fi
    fi

    # ── Write permission check ────────────────────────────────────────
    local test_file
    test_file="$(pwd)/.chk_feature_write_test.tmp"
    if ( : >"$test_file" ) 2>/dev/null; then
        rm -f -- "$test_file"
        append_file "Write permission in current directory: OK"
    else
        write_err "No write permission in current directory ($(pwd)) - compilation and cleanup will fail"
    fi

    # ── Java / keytool ────────────────────────────────────────────────
    append_file ""
    append_file "java --version"

    if command -v java >/dev/null 2>&1; then
        while IFS= read -r ln; do append_file "$ln"; done < <(run_capture java --version)

        if command -v keytool >/dev/null 2>&1; then
            append_file "keytool -version"
            kt_out=$(keytool -version 2>&1)
            kt_rc=$?
            while IFS= read -r ln; do append_file "$ln"; done <<< "$kt_out"
            if [ "$kt_rc" -ne 0 ]; then
                write_warn "keytool -version is not accepted by this JDK build (exit code $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"
                while IFS= read -r ln; do append_file "$ln"; done < <(run_capture keytool --help)
            fi
            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) !"
        fi
    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"
    fi

    # ── runmqakm ──────────────────────────────────────────────────────
    append_file ""
    append_file "runmqakm -version"

    if command -v runmqakm >/dev/null 2>&1; then
        local runmqakm_out runmqakm_rc ver_line ver_value
        runmqakm_out=$(runmqakm -version 2>&1)
        runmqakm_rc=$?
        while IFS= read -r ln; do append_file "$ln"; done <<<"$runmqakm_out"

        ver_line=$(grep -i 'productversion' <<<"$runmqakm_out" | head -n1)
        if [ -n "$ver_line" ]; then
            ver_value=$(sed -E 's/.*Version:[[:space:]]*//' <<<"$ver_line")
            append_file "runmqakm Version:${ver_value}"
            write_info "You can use runmqakm to analysing keystore cms/kdb from MQSeries"
        else
            local msg1="runmqakm -version did not report its ProductVersion (exit code: ${runmqakm_rc}) - it is likely failing to start 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"
        fi
    else
        write_warn "runmqakm from MQseries (server/client) not installed !"
        write_warn "You can't use runmqakm to analysing keystore cms/kdb from MQSeries !"
    fi

    # ── Locate chkcert_VxRxMx.py (plain descending name sort) ────────
    local chkcert_file=""
    local -a _chkcert_candidates=()
    shopt -s nullglob
    _chkcert_candidates=(chkcert_V*R*M*.py)
    shopt -u nullglob
    if [ "${#_chkcert_candidates[@]}" -gt 0 ]; then
        mapfile -t _chkcert_candidates < <(printf '%s\n' "${_chkcert_candidates[@]}" | sort -r)
        chkcert_file="${_chkcert_candidates[0]}"
    fi
    if [ -z "$chkcert_file" ]; then
        write_err "chkcert_V*R*M*.py not found !"
    fi

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

    local version_gc_current=""
    if command -v gcc >/dev/null 2>&1; then
        local gcc_line
        gcc_line=$(gcc --version 2>&1 | head -n1)
        append_file "$gcc_line"
        version_gc_current=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' <<<"$gcc_line" | head -n1)
        write_info "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 "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 'error message under $env:LOCALAPPDATA\Nuitka\Nuitka\Cache\downloads\gcc\x86_64\ - see prerequisites'
    fi

    # ── Python / pip ──────────────────────────────────────────────────
    append_file ""
    append_file "python -V"

    if command -v python >/dev/null 2>&1; then
        local py_version_line
        py_version_line=$(python -V 2>&1 | head -n1)
        append_file "$py_version_line"

        if command -v pip >/dev/null 2>&1; then
            while IFS= read -r ln; do append_file "$ln"; done < <(run_capture pip -V)
        else
            write_warn "pip not installed or not in PATH !"
            write_cmd "python -m ensurepip --upgrade"
        fi

        # ── Multiple python in PATH ────────────────────────────────────
        local all_pythons=() p_dir
        IFS=':' read -ra __path_dirs <<<"${PATH:-}"
        for p_dir in "${__path_dirs[@]}"; do
            [ -n "$p_dir" ] || continue
            [ -f "${p_dir}/python" ] && all_pythons+=("${p_dir}/python")
        done
        if [ "${#all_pythons[@]}" -gt 1 ]; then
            write_warn "Multiple python found in PATH (${#all_pythons[@]}) - the first one is used, verify this is the intended one"
            for p_dir in "${all_pythons[@]}"; do
                append_file "$p_dir"
            done
        fi

        # ── OS / Python architecture consistency ───────────────────────
        local os_arch_bits py_arch_bits
        os_arch_bits=$(getconf LONG_BIT 2>/dev/null)
        [ -n "$os_arch_bits" ] || os_arch_bits="64"
        py_arch_bits=$(python -c 'import struct; print(struct.calcsize("P") * 8)' 2>/dev/null)
        [ -n "$py_arch_bits" ] || py_arch_bits="$os_arch_bits"
        append_file "OS architecture: ${os_arch_bits}-bit | Python architecture: ${py_arch_bits}-bit"
        if [ "$py_arch_bits" != "$os_arch_bits" ]; then
            write_warn "Architecture mismatch: OS is ${os_arch_bits}-bit but Python is ${py_arch_bits}-bit - a known source of MinGW/Nuitka build failures"
        fi
    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'
    fi

    append_file ""
    append_file "pip list -v"
    local pip_list_file
    pip_list_file=$(mktemp)
    run_capture pip list -v >"$pip_list_file"
    while IFS= read -r ln; do append_file "$ln"; done <"$pip_list_file"

    append_file ""
    append_file "python -m nuitka --version"
    while IFS= read -r ln; do append_file "$ln"; done < <(run_capture python -m nuitka --version)

    # ── 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_file" 1
    test_pip_package "nuitka" "$pip_list_file" 1
    test_pip_package "paramiko" "$pip_list_file" 1
    test_pip_package "zstandard" "$pip_list_file" 1
    test_pip_package "colorama" "$pip_list_file" 0
    rm -f -- "$pip_list_file"

    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 `timeout` wrapper is the hard backstop for cases
    # even that doesn't save it from (e.g. a hung DNS lookup). Falls
    # back to running without a hard backstop if `timeout` itself isn't
    # installed (rare, but not guaranteed on every Cygwin setup).
    local outdated_file outdated_count outdated_timed_out=0 no_internet=0
    outdated_file=$(mktemp)
    if ! has_internet; then
        no_internet=1
        write_err "No Internet access detected (pypi.org:443 unreachable) - 'pip list --outdated' skipped"
        : >"$outdated_file"
    elif command -v timeout >/dev/null 2>&1; then
        timeout 30 pip list --outdated --disable-pip-version-check --retries 1 --timeout 5 >"$outdated_file" 2>&1
        [ "$?" -eq 124 ] && outdated_timed_out=1
    else
        pip list --outdated --disable-pip-version-check --retries 1 --timeout 5 >"$outdated_file" 2>&1
    fi
    while IFS= read -r ln; do append_file "$ln"; done <"$outdated_file"
    outdated_count=$(wc -l <"$outdated_file")
    rm -f "$outdated_file"
    # 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" -eq 1 ]; then
        : # already reported above via write_err(); nothing more to say
    elif [ "$outdated_timed_out" -eq 1 ]; then
        write_warn "pip list --outdated timed out after 30s (no/partial internet access on this machine?) - skipped, not treated as an error"
    elif [ "$outdated_count" -gt 2 ]; then
        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"
    fi

    # ── Source usage + imports ─────────────────────────────────────────
    local sr_base_name="chkcert"
    if [ -n "$chkcert_file" ]; then
        append_file ""
        write_info "Source utilisation : ${chkcert_file}"
        append_file "python ${chkcert_file} --version"
        while IFS= read -r ln; do append_file "$ln"; done < <(run_capture python "$chkcert_file" --version | remove_blank_lines)

        append_file ""
        append_file "cat ${chkcert_file}"
        while IFS= read -r raw_line; do
            if [[ "$raw_line" =~ ^[[:space:]]*(import|from|colorama)\b ]]; then
                line="${raw_line#"${raw_line%%[![:space:]]*}"}"
                if [[ "$line" != \#* ]] && [[ "$line" != *"from this certificat"* ]]; then
                    append_file "$line"
                fi
            fi
        done <"$chkcert_file"

        # ── VERSION constant vs. filename version cross-check ─────────
        local internal_version=""
        internal_version=$(grep -m1 -E '^[[:space:]]*VERSION[[:space:]]*=[[:space:]]*"[^"]+"' "$chkcert_file" | sed -E 's/^[[:space:]]*VERSION[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
        append_file ""
        append_file "Check: does the VERSION constant inside the source match the version encoded in the filename ?"
        if [ -n "$internal_version" ]; then
            local filename_no_ext filename_version
            filename_no_ext=$(basename -- "$chkcert_file")
            filename_no_ext="${filename_no_ext%.*}"
            filename_version="${filename_no_ext#*_}"
            append_file "VERSION in source: ${internal_version} | filename version: ${filename_version}"
            if [ "$internal_version" != "$filename_version" ]; then
                write_err "Version mismatch: source has VERSION=\"${internal_version}\" but filename (${chkcert_file}) says ${filename_version} !"
            else
                write_ok "Source VERSION matches filename (${internal_version})"
            fi
        else
            write_warn "VERSION constant not found in ${chkcert_file}"
        fi

        local chkcert_script_no_ext
        chkcert_script_no_ext=$(basename -- "$chkcert_file")
        chkcert_script_no_ext="${chkcert_script_no_ext%.*}"
        sr_base_name="${chkcert_script_no_ext%%_*}"
    else
        write_warn "No source script python chkcert found !"
    fi

    # ── Compiled binary + GCC version cross-check ──────────────────────
    # nuitka_compile_V1R1M1.sh names the output binary
    # "<file_prefix>_<sr_base_name>.exe" - file_prefix is the same tag
    # used for the report filename above (the distro/version label,
    # e.g. "Linux_AlmaLinux_10.2").
    local chkcert_exe_name="${file_prefix}_${sr_base_name}.exe"
    local chkcert_exe=""
    [ -f "$chkcert_exe_name" ] && chkcert_exe="$chkcert_exe_name"

    local version_gc_compiled=""
    if [ -n "$chkcert_exe" ]; then
        append_file ""
        write_info "Version compiled gcc: ${chkcert_exe}"

        local exe_size_mb gcc_line_exe="" start_ts elapsed_lookup
        exe_size_mb=$(awk -v b="$(stat -c%s -- "$chkcert_exe" 2>/dev/null || stat -f%z -- "$chkcert_exe")" 'BEGIN{printf "%.1f", b/1048576}')

        start_ts=$(date +%s.%N 2>/dev/null || date +%s)

        # No Windows PE FileDescription metadata to read on Unix (that
        # fast path is Windows-only, and nuitka_compile_V1R1M1.sh never
        # embeds it in the first place) -- go straight to the full-file
        # scan for GCC's own embedded compiler-identification string.
        append_file "strings ${chkcert_exe} | Select-String ^GCC   (file size: ${exe_size_mb} MB)"
        # Primary: `strings` properly extracts contiguous printable runs
        # delimited by non-printable bytes, so `grep "^GCC:"` reliably
        # matches the genuine "GCC: (GNU) ..." string embedded by the
        # compiler. find_embedded_string() greps the RAW binary directly
        # instead, which can straddle real string boundaries and return
        # corrupted matches (observed in practice, e.g. "GCCCCC") -- kept
        # here only as a fallback for systems where `strings` is missing.
        if command -v strings >/dev/null 2>&1; then
            gcc_line_exe=$(strings -- "$chkcert_exe" | grep -m1 "^GCC:")
        fi
        if [ -z "$gcc_line_exe" ]; then
            gcc_line_exe=$(find_embedded_string "$chkcert_exe" 'GCC[ -~]{3,}')
        fi
        if [ -n "$gcc_line_exe" ]; then
            version_gc_compiled=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' <<<"$gcc_line_exe" | tail -n1)
        fi

        elapsed_lookup=$(awk -v s="$start_ts" -v e="$(date +%s.%N 2>/dev/null || date +%s)" 'BEGIN{printf "%.6f", e-s}')
        append_file "(lookup took ${elapsed_lookup} s)"

        # ── Source vs. binary freshness check ─────────────────────────
        if [ -n "$chkcert_file" ]; then
            local src_mtime exe_mtime
            src_mtime=$(stat -c '%y' -- "$chkcert_file" 2>/dev/null | cut -d. -f1)
            exe_mtime=$(stat -c '%y' -- "$chkcert_exe" 2>/dev/null | cut -d. -f1)
            append_file "Source last modified: ${src_mtime} | Binary last modified: ${exe_mtime}"
            if [ "$(stat -c '%Y' -- "$chkcert_file" 2>/dev/null)" -gt "$(stat -c '%Y' -- "$chkcert_exe" 2>/dev/null)" ] 2>/dev/null; then
                write_warn "${chkcert_file} was modified after ${chkcert_exe} was compiled - the binary may not reflect the current source, recompilation recommended"
            fi
        fi

        if [ -n "$gcc_line_exe" ]; then
            append_file "$gcc_line_exe"
            append_file ""
            write_info "${chkcert_exe} compiled version GCC ${gcc_line_exe}"

            append_file ""
            local chkcert_exe_run_path="./${chkcert_exe}"
            append_file "${chkcert_exe_run_path} --version"
            while IFS= read -r ln; do append_file "$ln"; done < <(run_capture "$chkcert_exe_run_path" --version | format_chkcert_version)

            local version_gc_current_clean="${version_gc_current}" version_go_compiled_clean="${version_gc_compiled}"
            append_file ""
            append_file "Check: does the GCC version currently installed match the one used to compile ${chkcert_exe} ?"
            append_file "GCC current: '${version_gc_current_clean}' | GCC compiled: '${version_go_compiled_clean}'"
            if [ -n "$version_gc_current_clean" ] && [ -n "$version_go_compiled_clean" ] && [ "$version_gc_current_clean" != "$version_go_compiled_clean" ]; then
                write_warn "Difference version ${version_go_compiled_clean} gcc objet compiled ${chkcert_exe} with gcc current version ${version_gc_current_clean} !"
            fi
        else
            write_warn "No embedded GCC string found in ${chkcert_exe} (binary may be stripped or use a different toolchain marker)."
        fi
    else
        write_warn "No compiled object found: ${chkcert_exe_name}"
    fi

    # ── Link presence + validity check ─────────────────────────────────
    # nuitka_compile_V1R1M1.sh names this link the plain, untagged
    # launcher name "<sr_base_name>.exe" (e.g. "chkcert.exe"), pointing
    # at the distro-tagged binary.
    local chkcert_lnk_name="${sr_base_name}.exe"
    append_file ""
    if [ -e "$chkcert_lnk_name" ]; then
        local lnk_target_path="" lnk_kind=""

        if [ -L "$chkcert_lnk_name" ]; then
            lnk_kind="symlink"
            lnk_target_path=$(readlink -- "$chkcert_lnk_name")
            # A bare relative symlink target (e.g.
            # "Linux_AlmaLinux_10.2_chkcert.exe", no "./" prefix) has to
            # be re-anchored to the link's own directory before it can
            # be run: execvp() only searches PATH for names without a
            # "/" in them - the current directory is deliberately not
            # searched for security reasons.
            case "$lnk_target_path" in
                /*) : ;;
                *) lnk_target_path="$(cd "$(dirname -- "$(readlink -f -- "$chkcert_lnk_name" 2>/dev/null || echo "$chkcert_lnk_name")")" 2>/dev/null && pwd)/${lnk_target_path}" ;;
            esac
        else
            lnk_kind="plain copy"
            lnk_target_path="$(cd "$(dirname -- "$chkcert_lnk_name")" && pwd)/$(basename -- "$chkcert_lnk_name")"
        fi

        local lnk_size_kb
        lnk_size_kb=$(awk -v b="$(stat -c%s -- "$chkcert_lnk_name" 2>/dev/null || stat -f%z -- "$chkcert_lnk_name")" 'BEGIN{printf "%.1f", b/1024}')
        write_info "${chkcert_lnk_name} is a ${lnk_kind} -> ${lnk_target_path} (size: ${lnk_size_kb} KB)"

        if [ ! -e "$lnk_target_path" ]; then
            write_warn "${chkcert_lnk_name} target does not exist: ${lnk_target_path}"
        else
            local lnk_version_output lnk_rc
            lnk_version_output=$("$lnk_target_path" --version 2>&1)
            lnk_rc=$?
            if [ "$lnk_rc" != "0" ] || [ -z "$lnk_version_output" ]; then
                write_warn "${chkcert_lnk_name} target failed to run or produced no output -- link may be broken or stale"
                append_file "${lnk_target_path} --version"
                while IFS= read -r ln; do append_file "$ln"; done < <(format_chkcert_version <<<"$lnk_version_output")
            elif [ -n "$chkcert_exe" ]; then
                local exe_version_output
                exe_version_output=$(run_capture "./${chkcert_exe}" --version)
                if [ "$lnk_version_output" != "$exe_version_output" ]; then
                    write_warn "${chkcert_lnk_name} --version output differs from ${chkcert_exe} -- link may be stale (pointing to an older build)"
                    append_file "${lnk_target_path} --version"
                    while IFS= read -r ln; do append_file "$ln"; done < <(format_chkcert_version <<<"$lnk_version_output")
                else
                    write_ok "${chkcert_lnk_name} is valid and matches ${chkcert_exe}"
                    append_file "${chkcert_lnk_name} --version matches ${chkcert_exe} (output not repeated, see above)"
                fi
            fi
        fi
    else
        write_warn "No link found: ${chkcert_lnk_name}"
    fi

    # ── properties/ directory + config files check ──────────────────────
    append_file ""
    write_info "properties directory + config files check"

    local properties_dir="./properties"
    local licence_found=0
    if [ ! -d "$properties_dir" ]; then
        write_warn "properties directory not found: ${properties_dir} (built-in defaults will be used)"
    else
        write_ok "properties directory found: ${properties_dir}"

        local bcolors_file="${properties_dir}/class_bcolors.py"
        local constants_file="${properties_dir}/constantes_V1R1M1.properties"

        if [ -f "$bcolors_file" ]; then
            write_ok "class_bcolors.py found: ${bcolors_file}"
        else
            write_warn "class_bcolors.py not found: ${bcolors_file} (built-in colour palette will be used)"
        fi

        if [ -f "$constants_file" ]; then
            write_ok "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.
            local 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)
            local var_name found_val
            for var_name in "${expected_vars[@]}"; do
                found_val=$(
                    grep -viE '^[[:space:]]*($|#|;)' "$constants_file" 2>/dev/null \
                    | grep -i "^[[:space:]]*${var_name}[[:space:]]*=" \
                    | head -n1 \
                    | sed -E "s/^[[:space:]]*${var_name}[[:space:]]*=//I" \
                    | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//' \
                    | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'
                )
                if [ -n "$found_val" ] || grep -qi "^[[:space:]]*${var_name}[[:space:]]*=" "$constants_file" 2>/dev/null; then
                    write_ok "${var_name} = ${found_val}"
                else
                    write_warn "${var_name} not present in constantes_V1R1M1.properties (built-in default will be used)"
                fi
            done
        else
            write_warn "constantes_V1R1M1.properties not found: ${constants_file} (built-in defaults will be used)"
        fi

        # 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. Parsed with a targeted grep -P for
        # the "expires" field rather than a real JSON parser.
        local licence_file="${properties_dir}/licence.key"
        if [ -f "$licence_file" ]; then
            licence_found=1
            write_ok "licence.key found: ${licence_file}"
            cat -- "$licence_file"
            echo
            cat -- "$licence_file" >>"$FILE01" 2>/dev/null  # was console-only before — now also lands in the report
            echo >>"$FILE01" 2>/dev/null

            # 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.
            local tier
            tier=$(grep -oE '"tier"[[:space:]]*:[[:space:]]*"[^"]*"' "$licence_file" 2>/dev/null \
                | head -n1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/')
            tier="${tier//$'\r'/}"
            tier=$(printf '%s' "$tier" | tr '[:upper:]' '[:lower:]')
            if [ -n "$tier" ]; then
                local remote_label="not included (Solo tier)"
                case "$tier" in
                    team|pro|enterprise) remote_label="included" ;;
                esac
                write_ok "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)"
            fi

            local expires
            # POSIX ERE (-E), not PCRE (-P/\K): Cygwin's bundled grep and
            # some minimal grep builds (busybox, older macOS/BSD grep)
            # don't support -P, which would silently leave $expires empty
            # regardless of the file's actual content. \r is stripped
            # defensively in case the value ever picks one up from a
            # CRLF-terminated line, independent of the file's own line
            # endings.
            expires=$(grep -oE '"expires"[[:space:]]*:[[:space:]]*"[^"]*"' "$licence_file" 2>/dev/null \
                | head -n1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/')
            expires="${expires//$'\r'/}"
            if [ -n "$expires" ] && [ "$expires" != "never" ]; then
                local expiry_epoch today_epoch days_left
                expiry_epoch=$(date -d "$expires" +%s 2>/dev/null)
                today_epoch=$(date -d "today" +%s 2>/dev/null)
                if [ -n "$expiry_epoch" ] && [ -n "$today_epoch" ]; then
                    days_left=$(( (expiry_epoch - today_epoch) / 86400 ))
                    if [ "$days_left" -lt 0 ]; then
                        write_warn "Licence EXPIRED on ${expires} ($(( -days_left )) day(s) ago)"
                    else
                        write_ok "Licence expires on ${expires} (${days_left} day(s) remaining)"
                    fi
                else
                    write_warn "licence.key found but could not be parsed as JSON: unrecognised date format for 'expires'"
                fi
            elif [ "$expires" = "never" ]; then
                write_ok "Licence never expires"
            else
                write_warn "licence.key found but could not be parsed as JSON: 'expires' field not found"
            fi
        else
            write_warn "licence.key not found: ${licence_file} (tool will run in trial mode)"
        fi
    fi

    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)
    local has_exe=0
    [ -n "$chkcert_exe" ] && has_exe=1

    if [ "$has_exe" = "1" ] && [ "$licence_found" = "1" ]; then
        write_info "Report kept as: ${FILE01} (compiled object + licence.key both present)"
    else
        local state_suffix="before_no_licence"
        local final_file
        case "$FILE01" in
            *.log) final_file="${FILE01%.log}_${state_suffix}.log" ;;
            *) final_file="${FILE01}_${state_suffix}" ;;
        esac
        if mv -f -- "$FILE01" "$final_file" 2>/dev/null; then
            local old_file="$FILE01"
            FILE01="$final_file"
            write_info "Report renamed: ${old_file} -> ${final_file}"
        else
            write_warn "Could not rename report to ${final_file}"
        fi
    fi

    echo
    if [ -n "$FILE01" ] && [ -f "$FILE01" ]; then
        cat -- "$FILE01"
    fi

    return 0
}

main "$@"
