#!/bin/bash

# mkhint - Manage hint files for slackrepo scripts
#
# Usage:
#   ./mkhint --version VERSION --hintfile FILE    Update existing hint file
#   ./mkhint --version VERSION --new FILE         Create new hint file
#   ./mkhint --new FILE                           Create new hint file (no version)
#   ./mkhint --hintfile FILE                      Update hint, suggest latest version via nvchecker
#   ./mkhint --check [FILE...]                     Check all (or named) hints for upstream updates
#   ./mkhint --list                               List hint files
#   ./mkhint --clean                              Remove .bak files from HINT_DIR
#   ./mkhint --no-dl --hintfile FILE              Update hint, skip downloads, add NODOWNLOAD=yes
#   ./mkhint --no-dl --new FILE                   Create hint with NODOWNLOAD=yes
#   ./mkhint --help                               Show this help

set -e

# Default configuration
REPO_DIR="/var/lib/sbopkg/SBo-danix/"
HINT_DIR="/etc/slackrepo/SBo-danix/hintfiles/"
TMP_DIR="/tmp/mkhint"
NVCHECKER_CONFIG="$HOME/.config/nvchecker/nvchecker.toml"
# Deps that exist on Slackware stable but are unneeded on -current (system
# package or newer already present). Listed one per line, '#' comments allowed.
# Missing file = empty list = phantom-dep handling is a no-op.
PHANTOM_DEPS_FILE="$HOME/.config/mkhint/phantom-deps"

# create the temp dir if not existing
if [[ ! -d $TMP_DIR ]]; then
    mkdir $TMP_DIR
fi

readonly MKHINT_VERSION="1.0.0"

# Variables
VERSION=""
HINT_FILE=""
NEW_HINT_FILE=""
DELETE_HINT_FILES=()
MATCHED_PKGS=()
REVIEW_PKGS=()
LIST_PKGS=()
SHOW_LIST=""
RUN_REVIEW=""
COMMAND=""
NO_DL=0

# Show help message
show_help() {
    cat <<EOF
mkhint - Manage hint files for slackrepo scripts

Usage:
  ./mkhint --version VERSION --hintfile FILE    Update existing hint file
  ./mkhint --version VERSION --new FILE         Create new hint file
  ./mkhint --new FILE                           Create new hint file (no version)
  ./mkhint --hintfile FILE                      Update hint, suggest latest version via nvchecker
  ./mkhint --check [FILE...]                     Check all (or named) hints for upstream updates
  ./mkhint --fix-current                        Sweep repo, add DELREQUIRES for -current phantom deps
  ./mkhint --list                               List hint files
  ./mkhint --review                             Review hints matching SBo version, keep/delete each
  ./mkhint --review FILE...                      Review named hints (any version), keep/delete each
  ./mkhint --clean                              Remove .bak files from HINT_DIR
  ./mkhint --no-dl --hintfile FILE              Update hint, skip downloads, add NODOWNLOAD=yes
  ./mkhint --no-dl --new FILE                   Create hint with NODOWNLOAD=yes
  ./mkhint --help                               Show this help

Options:
  --version, -v VERSION    New version string (required for --hintfile)
  --hintfile, -f FILE      Path to existing hint file (required with --version)
  --new, -n FILE           Create new hint file (required with --version or standalone)
  --list, -l [FILE...]     List all hint files; FILE... = side-by-side hint vs .info
  --review, -R [FILE...]   Review hints; no args = matched only, FILE... = named hints (any version)
  --clean, -c              Remove all .bak files from HINT_DIR
  --check, -C [FILE...]    Check hints for upstream updates via nvchecker, update interactively
  --fix-current, -F        Add/merge DELREQUIRES for -current phantom deps across the whole repo
  --delete, -d FILE        Delete a hint file (and .bak if present)
  --no-dl, -N              Skip downloads; add NODOWNLOAD=yes to hint file (use with -f or -n)
  --help, -h               Show this help message

Hint files are stored in: $HINT_DIR
Temporary files are stored in: $TMP_DIR
Phantom-dep list (for --fix-current / --new): $PHANTOM_DEPS_FILE

Variables order in hint files:
  VERSION, ARCH, DOWNLOAD, MD5SUM, DOWNLOAD_x86_64, MD5SUM_x86_64

Exit codes:
  0 - Success
  1 - Invalid arguments or missing required options
  2 - File not found
  3 - File already exists
  4 - required tool not available (wget / nvchecker / nvtake / jq)
EOF
}

# Pad a possibly-multibyte glyph (0 or 1 display column) to a fixed width.
_pad_glyph() {
    local g="$1" w="$2"
    if [[ -n "$g" ]]; then printf "%s%*s" "$g" $((w - 1)) ""; else printf "%*s" "$w" ""; fi
}

# List hint files
list_hint_files() {
    if [[ ! -d "$HINT_DIR" ]]; then
        echo "Error: Hint directory does not exist: $HINT_DIR" >&2
        exit 2
    fi

    # Color only on a TTY, or when forced for tests. tput optional.
    local c_on="" c_off="" g_on=""
    if [[ -n "$MKHINT_FORCE_COLOR" || -t 1 ]]; then
        if command -v tput &>/dev/null && tput setaf 3 &>/dev/null; then
            c_on=$(tput setaf 3); g_on=$(tput setaf 2); c_off=$(tput sgr0)
        else
            c_on=$'\033[33m'; g_on=$'\033[32m'; c_off=$'\033[0m'
        fi
    fi

    echo "Hint files in: $HINT_DIR"
    echo "======================================================="
    printf "%-40s  %22s  %22s  %-20s  %-6s  %s\n" "File" "HintVer" "SBOVer" "Category" "DelReq" "Created"
    echo "-------------------------------------------------------"

    MATCHED_PKGS=()
    local count=0 matched=0
    for file in "$HINT_DIR"/*.hint; do
        if [[ -f "$file" ]]; then
            local VER; VER=$(grep "^VERSION" "$file" |cut -d '"' -f2)
            # Skip hints with no VERSION set.
            [[ -z "$VER" ]] && continue
            local delreq=""
            grep -q '^DELREQUIRES="..*"' "$file" && delreq="✓"
            local name; name=$(basename "$file")
            local pkg="${name%.hint}"
            local info_file
            info_file=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1)
            local SBO_VER=""
            local category=""
            if [[ -f "$info_file" ]]; then
                SBO_VER=$(grep "^VERSION" "$info_file" | cut -d '"' -f2)
                category=$(basename "$(dirname "$(dirname "$info_file")")")
            fi
            local date; date=$(stat -c "%y" "$file" | cut -d'.' -f1)
            local dr; dr=$(_pad_glyph "$delreq" 6)
            local hv sv
            hv=$(printf "%22s" "$VER")
            sv=$(printf "%22s" "$SBO_VER")
            if [[ -n "$SBO_VER" && "$VER" == "$SBO_VER" ]]; then
                # equal → whole row yellow
                local row
                row=$(printf "%-40s  %s  %s  %-20s  %s  %s" "$name" "$hv" "$sv" "$category" "$dr" "$date")
                printf "%s%s%s\n" "$c_on" "$row" "$c_off"
                MATCHED_PKGS+=("$pkg")
                matched=$((matched + 1))
            else
                # differ (or no SBO_VER) → green the newer cell, row plain
                if [[ -n "$SBO_VER" ]]; then
                    local newer
                    newer=$(printf '%s\n%s\n' "$VER" "$SBO_VER" | sort -V | tail -1)
                    if [[ "$newer" == "$VER" ]]; then
                        hv="${g_on}${hv}${c_off}"
                    else
                        sv="${g_on}${sv}${c_off}"
                    fi
                fi
                printf "%-40s  %s  %s  %-20s  %s  %s\n" "$name" "$hv" "$sv" "$category" "$dr" "$date"
            fi
            count=$((count + 1))
        fi
    done

    if [[ $count -eq 0 ]]; then
        echo "  (no hint files found)"
    fi

    echo "======================================================="
    echo "Total: $count file(s)"
    if [[ $matched -gt 0 && -n "$c_on" ]]; then
        echo "(yellow row = versions match; green = newer side)"
    fi
}

# Show hint side-by-side with its .info. Output to the given fd (default 1).
_show_hint_diff() {
    local pkg="$1"
    local fd="${2:-1}"
    local hint="${HINT_DIR%/}/${pkg}.hint"
    local info
    info=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1)

    echo "" >&"$fd"
    echo "=== $pkg ===" >&"$fd"
    if command -v git &>/dev/null; then
        git diff --no-index --color=auto "$hint" "$info" >&"$fd" || true
    else
        diff -y --width="${COLUMNS:-160}" "$hint" "$info" >&"$fd" || true
    fi
}

# Diff one hint against its .info and prompt Keep/Delete/Skip.
# Human-facing output goes to stderr; echoes only "deleted" or "kept" on stdout
# so the caller can capture the result.
_review_one_hint() {
    local pkg="$1"
    local hint="${HINT_DIR%/}/${pkg}.hint"

    _show_hint_diff "$pkg" 2

    local ans
    read -r -p "Review $pkg: [K]eep / [D]elete / [S]kip (default Keep): " ans
    case "$ans" in
        [Dd])
            _remove_hint "$hint" >&2
            echo deleted
            ;;
        [Ss]|[Kk]|"")
            echo "Kept: $pkg" >&2
            echo kept
            ;;
        *)
            echo "Unrecognised answer; keeping $pkg" >&2
            echo kept
            ;;
    esac
}

# Review hints. With package args: review each named hint (any version),
# validating existence first (exit 2 on a missing hint). With no args: review
# MATCHED_PKGS (populated by list_hint_files).
review_hint_files() {
    local -a pkgs
    if [[ $# -gt 0 ]]; then
        pkgs=("$@")
        local p
        for p in "${pkgs[@]}"; do
            if [[ ! -f "${HINT_DIR%/}/${p}.hint" ]]; then
                echo "Error: hint file not found: ${HINT_DIR%/}/${p}.hint" >&2
                exit 2
            fi
        done
    else
        if [[ ${#MATCHED_PKGS[@]} -eq 0 ]]; then
            echo "No hints match their SBo version; nothing to review."
            return 0
        fi
        pkgs=("${MATCHED_PKGS[@]}")
    fi

    local deleted=0 kept=0 pkg result
    for pkg in "${pkgs[@]}"; do
        result=$(_review_one_hint "$pkg")
        case "$result" in
            deleted) deleted=$((deleted + 1)) ;;
            *)       kept=$((kept + 1)) ;;
        esac
    done

    echo ""
    echo "Reviewed ${#pkgs[@]} hint(s): deleted $deleted, kept $kept."
}

# Validate wget availability
check_wget() {
    if ! command -v wget &> /dev/null; then
        echo "Error: wget is not installed. Please install wget first." >&2
        exit 4
    fi
}

# Validate nvchecker toolchain availability
check_nvchecker() {
    local missing=()
    command -v nvchecker &> /dev/null || missing+=("nvchecker")
    command -v nvtake   &> /dev/null || missing+=("nvtake")
    command -v jq       &> /dev/null || missing+=("jq")
    if [[ ${#missing[@]} -gt 0 ]]; then
        echo "Error: required tool(s) not installed: ${missing[*]}" >&2
        echo "Install nvchecker (provides nvchecker + nvtake) and jq." >&2
        exit 4
    fi
    if [[ ! -f "$NVCHECKER_CONFIG" ]]; then
        echo "Error: nvchecker config not found: $NVCHECKER_CONFIG" >&2
        exit 2
    fi
}

# Echo the newver-keyfile path declared in [__config__] of NVCHECKER_CONFIG
_nvchecker_newver_path() {
    # Grab the `newver = "..."` value; tolerate spaces around =
    local line
    line=$(grep -E '^[[:space:]]*newver[[:space:]]*=' "$NVCHECKER_CONFIG" | head -1)
    [[ -z "$line" ]] && return 1
    # extract the quoted path
    local path
    path=$(printf '%s\n' "$line" | sed -E 's/^[^"]*"([^"]*)".*/\1/')
    [[ -z "$path" ]] && return 1
    # expand a leading ~ to $HOME
    path="${path/#\~/$HOME}"
    # nvchecker resolves a relative keyfile path against the config file's
    # directory (not the CWD), so do the same here.
    if [[ "$path" != /* ]]; then
        path="$(dirname "$NVCHECKER_CONFIG")/$path"
    fi
    printf '%s\n' "$path"
}

# Echo the latest version nvchecker found for a package, or return non-zero
# Normalize an upstream version to our packaging convention: '-' is illegal in
# a SlackBuild version (breaks PRGNAM parsing), so we store it as '_'. Compare
# the normalized forms so e.g. upstream 2026-06-02 == packaged 2026_06_02.
_normalize_version() {
    printf '%s\n' "${1//-/_}"
}

# Usage: latest=$(nvchecker_latest pkg) || handle "no version"
nvchecker_latest() {
    local pkg="$1"
    local keyfile
    keyfile=$(_nvchecker_newver_path) || return 1
    [[ -f "$keyfile" ]] || return 1
    local ver
    ver=$(jq -r --arg p "$pkg" '.data[$p].version // empty' "$keyfile" 2>/dev/null)
    [[ -z "$ver" ]] && return 1
    printf '%s\n' "$ver"
}

# download files
download_file() {
    local url="$1"
    local dlfile="${TMP_DIR}/download"

    if [[ -f $dlfile ]]; then
        rm "$dlfile"
    fi

    # Download the file
    if [[ ! -z $1 ]]; then
        wget -O "$dlfile" "$url" || return 1
    fi

    # calculate md5
    local md5; md5=$(md5sum "$dlfile" | awk '{print $1}')
    rm "$dlfile"
    echo "$md5"
}

# ── phantom-dep handling (deps unneeded on -current) ──────────────────────────

# Load PHANTOM_DEPS_FILE into the PHANTOM_DEPS array (one dep per line, '#'
# comments and blank lines ignored). Missing file => empty array.
PHANTOM_DEPS=()
load_phantom_deps() {
    PHANTOM_DEPS=()
    [[ -f "$PHANTOM_DEPS_FILE" ]] || return 0
    local line
    while IFS= read -r line; do
        line="${line%%#*}"                 # strip comment
        line="${line//[[:space:]]/}"       # strip whitespace
        [[ -n "$line" ]] && PHANTOM_DEPS+=("$line")
    done < "$PHANTOM_DEPS_FILE"
}

# Read REQUIRES="..." from an .info file and echo the phantom deps it contains,
# space-separated. Empty output if none.
phantom_deps_in_info() {
    local info="$1" requires dep
    [[ -f "$info" ]] || return 0
    requires=$(. "$info" >/dev/null 2>&1; echo "${REQUIRES:-}")
    local hits=()
    for dep in "${PHANTOM_DEPS[@]}"; do
        [[ " $requires " == *" $dep "* ]] && hits+=("$dep")
    done
    echo "${hits[*]}"
}

# Merge a set of deps into a hint file's DELREQUIRES (union, dedup), preserving
# all other content. Creates a minimal hint if the file is absent. Backs up an
# existing file to .bak first (mkhint convention). Args: hintpath dep...
merge_delrequires() {
    local hint="$1"; shift
    local new_deps=("$@")
    [[ ${#new_deps[@]} -eq 0 ]] && return 0

    local existing="" combined
    if [[ -f "$hint" ]]; then
        existing=$(. "$hint" >/dev/null 2>&1; echo "${DELREQUIRES:-}")
    fi
    # union existing + new, dedup, first-seen order
    combined=$(printf '%s\n' $existing "${new_deps[@]}" | awk 'NF && !seen[$0]++' | tr '\n' ' ')
    combined="${combined% }"

    if [[ -f "$hint" ]]; then
        # no change needed? (all new deps already present) → skip, don't churn .bak
        local cur_norm new_norm
        cur_norm=$(printf '%s\n' $existing | awk 'NF && !seen[$0]++' | sort | tr '\n' ' ')
        new_norm=$(printf '%s\n' $existing "${new_deps[@]}" | awk 'NF && !seen[$0]++' | sort | tr '\n' ' ')
        [[ "$cur_norm" == "$new_norm" ]] && return 0

        cp "$hint" "${hint}.bak"
        if grep -q '^DELREQUIRES=' "$hint"; then
            sed -i "s#^DELREQUIRES=.*#DELREQUIRES=\"$combined\"#" "$hint"
        else
            printf 'DELREQUIRES="%s"\n' "$combined" >> "$hint"
        fi
    else
        printf 'DELREQUIRES="%s"\n' "$combined" > "$hint"
    fi
}

# Bulk sweep: for every package in REPO_DIR whose REQUIRES contains a phantom
# dep, ensure its hint carries the matching DELREQUIRES. Idempotent.
fix_current() {
    load_phantom_deps
    if [[ ${#PHANTOM_DEPS[@]} -eq 0 ]]; then
        echo "No phantom deps configured ($PHANTOM_DEPS_FILE). Nothing to do."
        return 0
    fi
    echo "Sweeping for phantom deps: ${PHANTOM_DEPS[*]}"
    local info prgnam deps count=0
    while IFS= read -r info; do
        prgnam=$(basename "$info" .info)
        deps=$(phantom_deps_in_info "$info")
        [[ -z "$deps" ]] && continue
        merge_delrequires "$HINT_DIR/$prgnam.hint" $deps
        echo "  $prgnam <- DELREQUIRES: $deps"
        (( count++ )) || true
    done < <(find "$REPO_DIR" -mindepth 2 -maxdepth 3 -name '*.info' | sort)
    echo "Done. $count package(s) with phantom deps."
}

# Create new hint file
create_new_hint_file() {
    cd "$HINT_DIR"
    local file="$1"
    local normalized_file="${file}"

    if [[ "$file" != *.hint ]]; then
        normalized_file="${file}.hint"
    fi

    # search repository for .info file
    info=$(find "$REPO_DIR" -mindepth 2 -name "${normalized_file%.hint}.info")

    # Check if file exists
    if [[ ! -f "$normalized_file" ]]; then
        # the hint file we want to create doesn't exists, so we can check
        # the sbo repository for a .info file and use that as hint
        if [[ -n $info ]]; then
            cp "$info" "$normalized_file"
            # remove unwanted lines from hint file
            sed -i -e "/^PRGNAM=/d" \
            -e "/^HOMEPAGE=/d" \
            -e "/^MAINTAINER=/d" \
            -e "/^EMAIL=/d" \
            -e "s/^REQUIRES=/#REQUIRES=/" \
            "${normalized_file}"

            if grep -q '^ARCH=' $normalized_file; then
                sed -i 's/^ARCH=.*/ARCH="x86_64"/' $normalized_file
            else
                echo 'ARCH="x86_64"' >> $normalized_file
            fi

            # auto-strip -current phantom deps present in the .info REQUIRES
            load_phantom_deps
            local phantom; phantom=$(phantom_deps_in_info "$info")
            if [[ -n "$phantom" ]]; then
                printf 'DELREQUIRES="%s"\n' "$phantom" >> "$normalized_file"
                echo "added DELREQUIRES=\"$phantom\" (unneeded on -current)."
            fi

            if [[ -n "$VERSION" ]]; then
                local old_version
                old_version=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//')
                sed -i "s/${old_version}/${VERSION}/g" "$normalized_file"
                update_checksums "$normalized_file"
            fi

            if [[ $NO_DL -eq 1 ]]; then
                add_nodownload "$normalized_file"
            fi

            echo "generated $normalized_file from $(basename $info)."
            echo "Check variables before using."
            add_nvchecker_section "${normalized_file%.hint}" "$info"
        fi
    else
        echo "Hint file exists: $normalized_file" >&2
        mv "$normalized_file" "${normalized_file}.bak"
        echo "Backed up to: ${normalized_file}.bak" >&2
        # Create new hint file with empty variables
        cat > "$normalized_file" <<EOF
VERSION="${VERSION}"
ARCH="x86_64"
DOWNLOAD=""
MD5SUM=""
DOWNLOAD_x86_64=""
MD5SUM_x86_64=""
EOF
        if [[ $NO_DL -eq 1 ]]; then
            add_nodownload "$normalized_file"
        fi

        echo "Created new hint file [EMPTY]: $normalized_file"

    fi
}

# Emit the TOML section label for a package: bare if the name is a valid
# bare key ([A-Za-z0-9_] only), otherwise double-quoted. nvchecker (and TOML)
# require quoting for names containing '.', '-', etc.
_nvchecker_label() {
    local pkg="$1"
    if [[ "$pkg" =~ ^[A-Za-z0-9_]+$ ]]; then
        printf '[%s]' "$pkg"
    else
        printf '["%s"]' "$pkg"
    fi
}

# Return 0 if NVCHECKER_CONFIG already has a section for pkg (bare or quoted)
_has_nvchecker_section() {
    local pkg="$1"
    [[ -f "$NVCHECKER_CONFIG" ]] || return 1
    local label; label=$(_nvchecker_label "$pkg")
    # fixed-string match of the exact label at line start, trailing space allowed
    grep -qE "^$(printf '%s' "$label" | sed 's/[][\.*^$/]/\\&/g')[[:space:]]*$" \
        "$NVCHECKER_CONFIG"
}

# Append an nvchecker [pkg] section to NVCHECKER_CONFIG, auto-detecting the
# source from the package's .info DOWNLOAD/HOMEPAGE. No-op if section exists.
add_nvchecker_section() {
    local pkg="$1"
    local info_file="$2"

    # Ensure config dir/file exist (do not create __config__; user owns that)
    mkdir -p "$(dirname "$NVCHECKER_CONFIG")"
    touch "$NVCHECKER_CONFIG"

    local label; label=$(_nvchecker_label "$pkg")

    # Skip if section already present
    if _has_nvchecker_section "$pkg"; then
        echo "nvchecker: ${label} already present in $NVCHECKER_CONFIG"
        return 0
    fi

    local download="" homepage=""
    if [[ -f "$info_file" ]]; then
        download=$(grep -E '^(DOWNLOAD|DOWNLOAD_x86_64)=' "$info_file" | head -1)
        homepage=$(grep -E '^HOMEPAGE=' "$info_file" | head -1)
    fi
    local haystack="${download} ${homepage}"

    local section=""
    if [[ "$haystack" =~ github\.com/([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+) ]]; then
        local owner="${BASH_REMATCH[1]}"
        local repo="${BASH_REMATCH[2]}"
        repo="${repo%.git}"
        section=$(cat <<EOF

${label}
source = "github"
github = "${owner}/${repo}"
use_max_tag = true
EOF
)
    elif [[ "$haystack" =~ (pypi\.org|files\.pythonhosted\.org) ]]; then
        section=$(cat <<EOF

${label}
source = "pypi"
pypi = "${pkg}"
EOF
)
    else
        section=$(cat <<EOF

${label}
# TODO: configure nvchecker source for "${pkg}"
# source = "regex"
# url = "..."
# regex = "..."
# see https://nvchecker.readthedocs.io/en/latest/usage.html
EOF
)
    fi

    printf '%s\n' "$section" >> "$NVCHECKER_CONFIG"
    echo "nvchecker: review/fill ${label} section in $NVCHECKER_CONFIG"
}

# Add NODOWNLOAD=yes after MD5SUM_x86_64 line if not already present
add_nodownload() {
    local file="$1"
    if ! grep -q '^NODOWNLOAD=' "$file"; then
        sed -i '/^MD5SUM_x86_64=/a NODOWNLOAD=yes' "$file"
    fi
}

# Parse multiline variable value (handles \ continuation lines)
# Prints each whitespace-separated token on its own line
parse_multiline_var() {
    local varname="$1"
    local file="$2"
    # Join continuation lines, strip variable name and quotes, print one token per line
    awk -v var="${varname}" '
        BEGIN { found=0; buf="" }
        !found && $0 ~ "^"var"=\"" {
            found=1
            buf=$0
            sub("^"var"=\"", "", buf)
            if (buf !~ /\\[[:space:]]*$/) {
                gsub(/"[[:space:]]*$/, "", buf)
                n=split(buf, arr, /[[:space:]]+/)
                for (k=1;k<=n;k++) if(arr[k]!="") print arr[k]
                found=0; buf=""
            } else {
                gsub(/\\[[:space:]]*$/, "", buf)
            }
            next
        }
        found {
            if ($0 ~ /\\[[:space:]]*$/) {
                line=$0; gsub(/\\[[:space:]]*$/, "", line)
                buf=buf" "line
            } else {
                line=$0; gsub(/"[[:space:]]*$/, "", line)
                buf=buf" "line
                n=split(buf, arr, /[[:space:]]+/)
                for (k=1;k<=n;k++) if(arr[k]!="") print arr[k]
                found=0; buf=""
            }
        }
    ' "$file"
}

# Prompt user for updated continuation URLs; returns updated URLs via nameref array
# First URL is always kept as-is (already updated by version sed before this call)
prompt_continuation_urls() {
    local -n _urls="$1"   # nameref: array of current URLs
    local varname="$2"

    local i
    for (( i=1; i<${#_urls[@]}; i++ )); do
        local current="${_urls[$i]}"
        echo ""
        echo "  ${varname} line $((i+1)) (current): $current"
        read -r -p "  New URL (leave blank to keep): " new_url
        if [[ -n "$new_url" ]]; then
            _urls[$i]="$new_url"
        fi
    done
}

# Build multiline variable string for writing back to file
# Usage: build_multiline_value urls_array -> prints quoted multiline value
build_multiline_value() {
    local -n _arr="$1"
    local count=${#_arr[@]}
    local i
    for (( i=0; i<count; i++ )); do
        if (( i == 0 )); then
            printf '"%s' "${_arr[$i]}"
        else
            printf ' \\\n    %s' "${_arr[$i]}"
        fi
    done
    printf '"\n'
}

# Query nvchecker for a package's latest version and let the user accept or
# override it. Echoes the chosen version on stdout. Returns non-zero if the
# user declines or no version is available (caller decides what to do).
suggest_version() {
    local pkg="$1"

    # Refresh nvchecker results (stderr only; keep stdout clean for the echo).
    # -e limits the run to this entry instead of scanning the whole config.
    nvchecker -c "$NVCHECKER_CONFIG" -e "$pkg" >&2 || true

    local latest
    latest=$(nvchecker_latest "$pkg") || {
        echo "Error: no nvchecker result for '$pkg'. Add/fix its [${pkg}] section in $NVCHECKER_CONFIG" >&2
        return 1
    }
    latest=$(_normalize_version "$latest")

    # Read current version from the hint file (best effort, for display)
    local hintpath="${HINT_DIR%/}/${pkg}.hint"
    local current=""
    [[ -f "$hintpath" ]] && current=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//')

    local answer
    read -r -p "current ${current:-?}, latest ${latest}. Use ${latest}? [Y/n] (or type a version) " answer >&2
    answer="${answer:-Y}"
    case "$answer" in
        [Yy]) printf '%s\n' "$latest" ;;
        [Nn]) return 1 ;;
        *)    printf '%s\n' "$answer" ;;
    esac
}

# Download files and update MD5SUM/MD5SUM_x86_64 in hint file
update_checksums() {
    local file="$1"

    _process_download_var "DOWNLOAD" "MD5SUM" "$file"
    _process_download_var "DOWNLOAD_x86_64" "MD5SUM_x86_64" "$file"
}

# Process one DOWNLOAD/MD5SUM variable pair in a hint file
_process_download_var() {
    local dl_var="$1"
    local md5_var="$2"
    local file="$3"

    # Read current URLs into array
    mapfile -t urls < <(parse_multiline_var "$dl_var" "$file")

    [[ ${#urls[@]} -eq 0 ]] && return
    [[ "${urls[0]}" == "UNSUPPORTED" || "${urls[0]}" == "UNTESTED" ]] && return

    # Read current md5sums into array (parallel to urls)
    mapfile -t md5s < <(parse_multiline_var "$md5_var" "$file")

    # Save original URLs for change detection after prompt
    local orig_urls=("${urls[@]}")

    # Prompt user to update continuation URLs if present
    if (( ${#urls[@]} > 1 )); then
        echo ""
        echo "Multiline ${dl_var} detected in $(basename "$file")."
        prompt_continuation_urls urls "$dl_var"
    fi

    # Download and calculate md5 for each URL
    local new_md5s=()
    local i
    for (( i=0; i<${#urls[@]}; i++ )); do
        local url="${urls[$i]}"
        if (( i == 0 )); then
            # Always re-download first URL
            echo "Downloading: $url"
            new_md5s+=( "$(download_file "$url")" )
        else
            # Only re-download if URL changed from original
            if [[ "$url" != "${orig_urls[$i]}" ]]; then
                echo "Downloading (updated): $url"
                new_md5s+=( "$(download_file "$url")" )
            else
                echo "Keeping existing md5 for: $url"
                new_md5s+=( "${md5s[$i]}" )
            fi
        fi
    done

    # Rebuild and write back DOWNLOAD variable (may have updated continuation URLs)
    local new_dl_value
    new_dl_value=$(build_multiline_value urls)
    # Strip surrounding quotes — perl wraps them in the substitution
    new_dl_value="${new_dl_value#\"}"
    new_dl_value="${new_dl_value%\"}"
    perl -i -0pe 'BEGIN{$v=shift} s|^'"${dl_var}"'="[^"]*(?:\\\n[^"]*)*"|'"${dl_var}"'="$v"|m' \
        "$new_dl_value" "$file"

    # Rebuild and write back MD5SUM variable
    local new_md5_value
    new_md5_value=$(build_multiline_value new_md5s)
    new_md5_value="${new_md5_value#\"}"
    new_md5_value="${new_md5_value%\"}"
    perl -i -0pe 'BEGIN{$v=shift} s|^'"${md5_var}"'="[^"]*(?:\\\n[^"]*)*"|'"${md5_var}"'="$v"|m' \
        "$new_md5_value" "$file"
}

# Update existing hint file
update_hint_file() {
    cd "$HINT_DIR"
    local file="$1"
    local new_version="$2"
    local old_version=""

    local normalized_file="${file}"

    if [[ "$file" != *.hint ]]; then
        normalized_file="${file}.hint"
    fi

    # Check if file exists
    if [[ ! -f "$normalized_file" ]]; then
        echo "Error: Hint file does not exist: $normalized_file" >&2
        exit 2
    fi

    # Force backup as precaution before modifying
    echo "Hint file exists: $normalized_file" >&2
    cp "$normalized_file" "${normalized_file}.bak"
    echo "Backed up to: ${normalized_file}.bak" >&2

    # Extract current version from hint file
    old_version=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//')

    # Use sed for global replacement of OLD_VERSION in all variables
    sed -i "s/${old_version}/${new_version}/g" "$normalized_file"

    update_checksums "$normalized_file"

    if [[ $NO_DL -eq 1 ]]; then
        add_nodownload "$normalized_file"
    fi

    hf=$(cat $normalized_file)
    echo "Updated hint file: $normalized_file"
    echo "=========================================="
    echo -n "$hf"
    echo; echo "=========================================="
}

# Prompt to run slackrepo update after a hint file update
prompt_slackrepo() {
    local pkg="$1"
    local answer
    read -r -p "Run 'slackrepo update $pkg'? [Y/n] " answer
    answer="${answer:-Y}"
    if [[ "$answer" =~ ^[Yy]$ ]]; then
        slackrepo update "$pkg"
    fi
}

# Remove a hint file and its .bak if present. No existence guard, no exit —
# safe to call inside loops. Echoes what was removed.
_remove_hint() {
    local full_path="$1"
    rm "$full_path"
    echo "Deleted: $full_path"
    local bak_path="${full_path}.bak"
    if [[ -f "$bak_path" ]]; then
        rm "$bak_path"
        echo "Deleted: $bak_path"
    fi
}

# Delete a hint file (and .bak if present)
delete_hint_file() {
    local file="$1"
    local normalized_file="${file}"

    if [[ "$file" != *.hint ]]; then
        normalized_file="${file}.hint"
    fi

    local full_path="${HINT_DIR}/${normalized_file}"

    if [[ ! -f "$full_path" ]]; then
        echo "Error: Hint file does not exist: $full_path" >&2
        exit 2
    fi

    _remove_hint "$full_path"
}

# Clean .bak files from HINT_DIR
clean_bak_files() {
    if [[ ! -d "$HINT_DIR" ]]; then
        echo "Error: Hint directory does not exist: $HINT_DIR" >&2
        exit 2
    fi

    local count=0
    for bakfile in "$HINT_DIR"/*.bak; do
        if [[ -f "$bakfile" ]]; then
            rm "$bakfile"
            count=$((count + 1))
        fi
    done

    echo "Removed $count .bak file(s) from $HINT_DIR"
}

# Bulk-check hint files for upstream updates and apply interactively.
# Usage: check_updates [pkg...]   (no args = all *.hint in HINT_DIR)
check_updates() {
    check_nvchecker

    if [[ ! -d "$HINT_DIR" ]]; then
        echo "Error: Hint directory does not exist: $HINT_DIR" >&2
        exit 2
    fi

    # Build the target package list
    local targets=()
    if [[ $# -gt 0 ]]; then
        targets=("$@")
    else
        local f
        for f in "$HINT_DIR"/*.hint; do
            [[ -f "$f" ]] || continue
            local b; b=$(basename "$f"); targets+=("${b%.hint}")
        done
    fi

    # Refresh nvchecker results. With exactly one explicit target, -e limits
    # the run to that entry; otherwise scan the whole config in one pass.
    echo "Running nvchecker..."
    if [[ ${#targets[@]} -eq 1 && $# -gt 0 ]]; then
        nvchecker -c "$NVCHECKER_CONFIG" -e "${targets[0]}" >&2 || true
    else
        nvchecker -c "$NVCHECKER_CONFIG" >&2 || true
    fi

    # Classify each target
    local outdated_pkgs=() outdated_old=() outdated_new=() outdated_flag=()
    local missing_sections=()
    local pkg
    for pkg in "${targets[@]}"; do
        local hintpath="${HINT_DIR%/}/${pkg}.hint"
        [[ -f "$hintpath" ]] || { echo "skip ${pkg}: no hint file"; continue; }
        local current; current=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//')
        local latest
        if ! latest=$(nvchecker_latest "$pkg"); then
            if _has_nvchecker_section "$pkg"; then
                echo "skip ${pkg}: no nvchecker result"
            else
                echo "skip ${pkg}: no nvchecker section"
                missing_sections+=("$pkg")
            fi
            continue
        fi
        local latest_norm; latest_norm=$(_normalize_version "$latest")
        [[ "$current" == "$latest_norm" ]] && continue   # up to date
        # determine direction with sort -V (compare normalized forms)
        local newest; newest=$(printf '%s\n%s\n' "$current" "$latest_norm" | sort -V | tail -1)
        local flag="update"
        [[ "$newest" == "$current" ]] && flag="?downgrade"
        outdated_pkgs+=("$pkg")
        outdated_old+=("$current")
        outdated_new+=("$latest_norm")
        outdated_flag+=("$flag")
    done

    # Offer to populate nvchecker.toml for packages with no section
    if [[ ${#missing_sections[@]} -gt 0 ]]; then
        echo ""
        echo "${#missing_sections[@]} package(s) have no nvchecker section: ${missing_sections[*]}"
        local answer
        read -r -p "Populate ${NVCHECKER_CONFIG} now? [Y/n] " answer
        answer="${answer:-Y}"
        if [[ "$answer" =~ ^[Yy]$ ]]; then
            local mp info
            for mp in "${missing_sections[@]}"; do
                info=$(find "$REPO_DIR" -mindepth 2 -name "${mp}.info" 2>/dev/null | head -1)
                if [[ -z "$info" ]]; then
                    echo "skip ${mp}: no .info found in $REPO_DIR"
                    continue
                fi
                add_nvchecker_section "$mp" "$info"
            done
            echo ""
            echo "Sections added. Review $NVCHECKER_CONFIG (fill any stubs), then re-run 'mkhint -C'."
            return 0
        fi
    fi

    if [[ ${#outdated_pkgs[@]} -eq 0 ]]; then
        echo "all up to date"
        return 0
    fi

    # Report
    echo ""
    echo "Updates available:"
    local i
    for (( i=0; i<${#outdated_pkgs[@]}; i++ )); do
        local note=""; [[ "${outdated_flag[$i]}" == "?downgrade" ]] && note=" (?downgrade)"
        printf "  %-30s %s -> %s%s\n" "${outdated_pkgs[$i]}" "${outdated_old[$i]}" "${outdated_new[$i]}" "$note"
    done
    echo ""

    # Per-package confirm + update
    local updated=()
    for (( i=0; i<${#outdated_pkgs[@]}; i++ )); do
        local p="${outdated_pkgs[$i]}"
        local note=""; [[ "${outdated_flag[$i]}" == "?downgrade" ]] && note=" (?downgrade)"
        local answer
        read -r -p "${p}  ${outdated_old[$i]} -> ${outdated_new[$i]}${note}. Update? [Y/n] " answer
        answer="${answer:-Y}"
        if [[ "$answer" =~ ^[Yy]$ ]]; then
            update_hint_file "$p" "${outdated_new[$i]}"
            nvtake -c "$NVCHECKER_CONFIG" "$p" >&2 || true
            updated+=("$p")
        fi
    done

    # Single slackrepo prompt for everything updated
    if [[ ${#updated[@]} -gt 0 ]]; then
        local answer
        read -r -p "Run 'slackrepo update ${updated[*]}'? [Y/n] " answer
        answer="${answer:-Y}"
        if [[ "$answer" =~ ^[Yy]$ ]]; then
            slackrepo update "${updated[@]}"
        fi
    fi
}

# Main function
main() {
    local parsed
    parsed=$(getopt -o vV:f:n:lcCdNhRF \
        --long version,set-version:,hintfile:,new:,list,clean,check,delete,no-dl,help,review,fix-current \
        -n 'mkhint' -- "$@") || { show_help; exit 1; }
    eval set -- "$parsed"

    while true; do
        case "$1" in
            --version|-v)
                echo "mkhint $MKHINT_VERSION"
                exit 0
                ;;
            --set-version|-V)
                VERSION="$2"
                shift 2
                ;;
            --hintfile|-f)
                HINT_FILE="$2"
                shift 2
                ;;
            --new|-n)
                NEW_HINT_FILE="$2"
                shift 2
                ;;
            --list|-l)
                SHOW_LIST=1
                shift
                ;;
            --review|-R)
                RUN_REVIEW=1
                shift
                ;;
            --clean|-c)
                COMMAND="clean"
                shift
                ;;
            --check|-C)
                COMMAND="check"
                shift
                ;;
            --fix-current|-F)
                COMMAND="fix-current"
                shift
                ;;
            --delete|-d)
                COMMAND="delete"
                shift
                ;;
            --no-dl|-N)
                NO_DL=1
                shift
                ;;
            --help|-h)
                COMMAND="help"
                shift
                ;;
            --)
                shift
                break
                ;;
            *)
                echo "Unknown option: $1" >&2
                show_help
                exit 1
                ;;
        esac
    done

    # Collect remaining positional args.
    # When -R is active and no other command set, they are review targets;
    # otherwise they are delete targets.
    while [[ $# -gt 0 ]]; do
        if [[ -n "$RUN_REVIEW" && -z "$COMMAND" ]]; then
            REVIEW_PKGS+=("$1")
        elif [[ -n "$SHOW_LIST" && -z "$COMMAND" ]]; then
            LIST_PKGS+=("$1")
        else
            DELETE_HINT_FILES+=("$1")
        fi
        shift
    done

    if [[ -z "$COMMAND" ]]; then
        # Default to update hint file if VERSION and HINT_FILE are provided
        if [[ -n "$HINT_FILE" ]]; then
            COMMAND="update"
        elif [[ -n "$NEW_HINT_FILE" ]]; then
            COMMAND="new"
        elif [[ ${#DELETE_HINT_FILES[@]} -gt 0 ]]; then
            COMMAND="delete"
        fi
    fi

    if [[ $NO_DL -eq 1 && -z "$HINT_FILE" && -z "$NEW_HINT_FILE" ]]; then
        echo "Error: --no-dl requires --hintfile or --new" >&2
        exit 1
    fi

    if [[ "$COMMAND" == "check" && ( -n "$VERSION" || -n "$HINT_FILE" || -n "$NEW_HINT_FILE" ) ]]; then
        echo "Error: --check cannot be combined with --set-version/--hintfile/--new" >&2
        exit 1
    fi

    if [[ "$COMMAND" == "fix-current" && ( -n "$VERSION" || -n "$HINT_FILE" || -n "$NEW_HINT_FILE" ) ]]; then
        echo "Error: --fix-current cannot be combined with --set-version/--hintfile/--new" >&2
        exit 1
    fi

    if [[ -n "$SHOW_LIST" && ${#LIST_PKGS[@]} -gt 0 ]]; then
        local p
        for p in "${LIST_PKGS[@]}"; do
            if [[ ! -f "${HINT_DIR%/}/${p}.hint" ]]; then
                echo "Error: hint file not found: ${HINT_DIR%/}/${p}.hint" >&2
                exit 2
            fi
        done
        for p in "${LIST_PKGS[@]}"; do
            _show_hint_diff "$p"
        done
        exit 0
    fi

    if [[ -n "$SHOW_LIST" || -n "$RUN_REVIEW" ]]; then
        [[ -n "$SHOW_LIST" ]] && list_hint_files
        if [[ -n "$RUN_REVIEW" ]]; then
            if [[ ${#REVIEW_PKGS[@]} -gt 0 ]]; then
                review_hint_files "${REVIEW_PKGS[@]}"
            else
                # ensure MATCHED_PKGS is populated even when -l was not given
                [[ -z "$SHOW_LIST" ]] && list_hint_files >/dev/null
                review_hint_files
            fi
        fi
        exit $?
    fi

    case "$COMMAND" in
        help)
            show_help
            ;;
        clean)
            clean_bak_files
            ;;
        check)
            check_updates "${DELETE_HINT_FILES[@]}"
            ;;
        fix-current)
            fix_current
            ;;
        update)
            check_wget
            if [[ -z "$VERSION" ]]; then
                check_nvchecker
                VERSION=$(suggest_version "$HINT_FILE") || { echo "Aborted." >&2; exit 0; }
                check_nvchecker_take=1
            fi
            update_hint_file "$HINT_FILE" "$VERSION"
            if [[ "${check_nvchecker_take:-0}" -eq 1 ]]; then
                nvtake -c "$NVCHECKER_CONFIG" "$HINT_FILE" >&2 || true
            fi
            prompt_slackrepo "$HINT_FILE"
            ;;
        new)
            if [[ -n "$VERSION" ]]; then
                check_wget
            fi
            create_new_hint_file "$NEW_HINT_FILE"
            ;;
        delete)
            for f in "${DELETE_HINT_FILES[@]}"; do
                delete_hint_file "$f"
            done
            ;;
        *)
            echo "Error: Unknown command: $COMMAND" >&2
            show_help
            exit 1
            ;;
    esac
}

main "$@"
