name: Check Node.js Version Drift

on:
  workflow_dispatch:
  schedule:
    # Runs weekly on Monday at 06:00 UTC
    - cron: "0 6 * * 1"

permissions:
  contents: read
  issues: write

jobs:
  check-node-versions:
    if: github.repository == 'community-scripts/ProxmoxVE'
    runs-on: coolify-runner

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Install dependencies
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq jq curl > /dev/null 2>&1

      - name: Check upstream Node.js versions
        id: check
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail

          echo "================================================"
          echo "  Checking Node.js version drift in install scripts"
          echo "================================================"

          # Alpine version -> Node major cache (populated on demand)
          declare -A ALPINE_NODE_CACHE

          # Resolve Node.js major version from Alpine package registry
          # Usage: resolve_alpine_node "3.21" => sets REPLY to major version (e.g. "22")
          resolve_alpine_node() {
            local alpine_ver="$1"
            if [[ -n "${ALPINE_NODE_CACHE[$alpine_ver]+x}" ]]; then
              REPLY="${ALPINE_NODE_CACHE[$alpine_ver]}"
              return
            fi

            local full_ver=""
            local arch_label url_arch url page
            for arch_pair in "amd64:x86_64" "arm64:aarch64"; do
              arch_label="${arch_pair%%:*}"
              url_arch="${arch_pair#*:}"
              url="https://pkgs.alpinelinux.org/package/v${alpine_ver}/main/${url_arch}/nodejs"
              page=$(curl -sf "$url" 2>/dev/null || echo "")
              if [[ -n "$page" ]]; then
                # Parse: "Version | 24.13.0-r1" or similar table row.
                full_ver=$(echo "$page" | grep -oP 'Version\s*\|\s*\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
                if [[ -z "$full_ver" ]]; then
                  # Fallback: look for version pattern after "Version".
                  full_ver=$(echo "$page" | grep -oP '(?<=Version</td><td>)[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
                fi
              fi
              if [[ -n "$full_ver" ]]; then
                echo "Resolved Alpine ${alpine_ver} nodejs via ${arch_label}"
                break
              fi
            done

            local major=""
            if [[ -n "$full_ver" ]]; then
              major="${full_ver%%.*}"
            fi

            ALPINE_NODE_CACHE[$alpine_ver]="$major"
            REPLY="$major"
          }

          # Extract Node major from a Dockerfile content
          # Sets: DF_NODE_MAJOR, DF_SOURCE (description of where we found it)
          extract_dockerfile_node() {
            local content="$1"
            DF_NODE_MAJOR=""
            DF_SOURCE=""

            # 1) FROM node:XX (e.g. node:24-alpine, node:22.9.0-bookworm-slim, node:20)
            local node_from
            node_from=$(echo "$content" | grep -oP '(?i)FROM\s+(--platform=[^\s]+\s+)?node:\K[0-9]+' | head -1 || echo "")
            if [[ -n "$node_from" ]]; then
              DF_NODE_MAJOR="$node_from"
              DF_SOURCE="FROM node:${node_from}"
              return
            fi

            # 2) nodesource/setup_XX.x
            local nodesource
            nodesource=$(echo "$content" | grep -oP 'nodesource/setup_\K[0-9]+' | head -1 || echo "")
            if [[ -n "$nodesource" ]]; then
              DF_NODE_MAJOR="$nodesource"
              DF_SOURCE="nodesource/setup_${nodesource}.x"
              return
            fi

            # 3) FROM alpine:X.Y — resolve via Alpine packages
            local alpine_ver
            alpine_ver=$(echo "$content" | grep -oP '(?i)FROM\s+(--platform=[^\s]+\s+)?alpine:\K[0-9]+\.[0-9]+' | head -1 || echo "")
            if [[ -n "$alpine_ver" ]]; then
              resolve_alpine_node "$alpine_ver"
              if [[ -n "$REPLY" ]]; then
                DF_NODE_MAJOR="$REPLY"
                DF_SOURCE="alpine:${alpine_ver} (pkg: nodejs ${DF_NODE_MAJOR})"
                return
              fi
            fi
          }

          # Extract Node major from engines.node in package.json
          # Sets: ENGINES_NODE_RAW (raw string), ENGINES_MIN_MAJOR, ENGINES_IS_MINIMUM
          extract_engines_node() {
            local content="$1"
            ENGINES_NODE_RAW=""
            ENGINES_MIN_MAJOR=""
            ENGINES_IS_MINIMUM="false"

            ENGINES_NODE_RAW=$(echo "$content" | jq -r '.engines.node // empty' 2>/dev/null || echo "")
            if [[ -z "$ENGINES_NODE_RAW" ]]; then
              return
            fi

            # Detect if constraint is a minimum (>=, ^) vs exact pinning
            if [[ "$ENGINES_NODE_RAW" =~ ^(\>=|\^|\~) ]]; then
              ENGINES_IS_MINIMUM="true"
            fi

            # Extract the first number (major) from the constraint
            # Handles: ">=24.13.1", "^22", ">=18.0.0", ">=18.15.0 <19 || ^20", etc.
            ENGINES_MIN_MAJOR=$(echo "$ENGINES_NODE_RAW" | grep -oP '\d+' | head -1 || echo "")
          }

          # Check if our_version satisfies an engines.node constraint
          # Returns 0 if satisfied, 1 if not
          # Usage: version_satisfies_engines "22" ">=18.0.0" "true"
          version_satisfies_engines() {
            local our="$1"
            local min_major="$2"
            local is_minimum="$3"

            if [[ -z "$min_major" || -z "$our" ]]; then
              return 1
            fi

            if [[ "$is_minimum" == "true" ]]; then
              # >= or ^ constraint: our version must be >= min_major
              if [[ "$our" -ge "$min_major" ]]; then
                return 0
              fi
            fi
            return 1
          }

          # Search for files in subdirectories via GitHub API tree
          # Usage: find_repo_file "owner/repo" "branch" "filename" => sets REPLY to raw URL or empty
          find_repo_file() {
            local repo="$1"
            local branch="$2"
            local filename="$3"
            REPLY=""

            # Try root first (fast)
            local root_url="https://raw.githubusercontent.com/${repo}/${branch}/${filename}"
            if curl -sfI "$root_url" >/dev/null 2>&1; then
              REPLY="$root_url"
              return
            fi

            # Search via GitHub API tree (recursive)
            local tree_url="https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1"
            local tree_json
            tree_json=$(curl -sf -H "Authorization: token $GH_TOKEN" "$tree_url" 2>/dev/null || echo "")
            if [[ -z "$tree_json" ]]; then
              return
            fi

            # Find first matching path (prefer shorter/root-level paths)
            local match_path
            match_path=$(echo "$tree_json" | jq -r --arg fn "$filename" \
              '.tree[]? | select(.path | endswith("/" + $fn) or . == $fn) | .path' 2>/dev/null \
              | sort | head -1 || echo "")

            if [[ -n "$match_path" ]]; then
              REPLY="https://raw.githubusercontent.com/${repo}/${branch}/${match_path}"
            fi
          }

          # Extract Node major from .nvmrc or .node-version
          # Sets: NVMRC_NODE_MAJOR
          extract_nvmrc_node() {
            local content="$1"
            NVMRC_NODE_MAJOR=""
            # .nvmrc/.node-version typically has: "v22.9.0", "22", "lts/iron", etc.
            local ver
            ver=$(echo "$content" | tr -d '[:space:]' | grep -oP '^v?\K[0-9]+' | head -1 || echo "")
            NVMRC_NODE_MAJOR="$ver"
          }

          # Collect results
          declare -a issue_scripts=()
          declare -a report_lines=()
          total=0
          checked=0
          drift_count=0

          for script in install/*-install.sh; do
            [[ ! -f "$script" ]] && continue
            if ! grep -q 'setup_nodejs' "$script"; then
              continue
            fi

            total=$((total + 1))
            slug=$(basename "$script" | sed 's/-install\.sh$//')

            # Extract Source URL (GitHub only) from the "# Source:" line
            # Supports both:
            #   # Source: https://github.com/owner/repo
            #   # Source: https://example.com | Github: https://github.com/owner/repo
            # NOTE: Must filter for "# Source:" line first to avoid matching the License URL
            source_url=$(head -20 "$script" | grep -i '# Source:' | grep -oP 'https://github\.com/[^\s|]+' | head -1 || echo "")
            if [[ -z "$source_url" ]]; then
              report_lines+=("| \`$slug\` | — | — | — | — | ⏭️ No GitHub source |")
              continue
            fi

            repo=$(echo "$source_url" | sed -E 's|https://github\.com/||; s|/$||; s|\.git$||')
            if [[ -z "$repo" || "$repo" != */* ]]; then
              report_lines+=("| \`$slug\` | — | — | — | — | ⏭️ Invalid repo |")
              continue
            fi

            checked=$((checked + 1))

            # Extract our NODE_VERSION
            our_version=$(grep -oP 'NODE_VERSION="(\d+)"' "$script" | head -1 | grep -oP '\d+' || echo "")
            if [[ -z "$our_version" ]]; then
              if grep -q 'NODE_VERSION=\$(' "$script"; then
                our_version="dynamic"
              else
                our_version="unset"
              fi
            fi

            # Determine default branch via GitHub API (fast, single call)
            detected_branch=""
            api_default=$(curl -sf -H "Authorization: token $GH_TOKEN" \
              "https://api.github.com/repos/${repo}" 2>/dev/null \
              | jq -r '.default_branch // empty' 2>/dev/null || echo "")
            if [[ -n "$api_default" ]]; then
              detected_branch="$api_default"
            else
              detected_branch="main"
            fi

            # Fetch upstream Dockerfile (root + subdirectories)
            df_content=""
            find_repo_file "$repo" "$detected_branch" "Dockerfile"
            if [[ -n "$REPLY" ]]; then
              df_content=$(curl -sf "$REPLY" 2>/dev/null || echo "")
            fi

            DF_NODE_MAJOR=""
            DF_SOURCE=""
            if [[ -n "$df_content" ]]; then
              extract_dockerfile_node "$df_content"
            fi

            # Fetch upstream package.json (root + subdirectories)
            pkg_content=""
            find_repo_file "$repo" "$detected_branch" "package.json"
            if [[ -n "$REPLY" ]]; then
              pkg_content=$(curl -sf "$REPLY" 2>/dev/null || echo "")
            fi

            ENGINES_NODE_RAW=""
            ENGINES_MIN_MAJOR=""
            ENGINES_IS_MINIMUM="false"
            if [[ -n "$pkg_content" ]]; then
              extract_engines_node "$pkg_content"
            fi

            # Fallback: check .nvmrc or .node-version
            NVMRC_NODE_MAJOR=""
            if [[ -z "$DF_NODE_MAJOR" && -z "$ENGINES_MIN_MAJOR" ]]; then
              for nvmfile in .nvmrc .node-version; do
                find_repo_file "$repo" "$detected_branch" "$nvmfile"
                if [[ -n "$REPLY" ]]; then
                  nvmrc_content=$(curl -sf "$REPLY" 2>/dev/null || echo "")
                  if [[ -n "$nvmrc_content" ]]; then
                    extract_nvmrc_node "$nvmrc_content"
                    [[ -n "$NVMRC_NODE_MAJOR" ]] && break
                  fi
                fi
              done
            fi

            # Determine upstream recommended major version
            upstream_major=""
            upstream_hint=""

            if [[ -n "$DF_NODE_MAJOR" ]]; then
              upstream_major="$DF_NODE_MAJOR"
              upstream_hint="$DF_SOURCE"
            elif [[ -n "$ENGINES_MIN_MAJOR" ]]; then
              upstream_major="$ENGINES_MIN_MAJOR"
              upstream_hint="engines: $ENGINES_NODE_RAW"
            elif [[ -n "$NVMRC_NODE_MAJOR" ]]; then
              upstream_major="$NVMRC_NODE_MAJOR"
              upstream_hint=".nvmrc/.node-version"
            fi

            # Build display values
            engines_display="${ENGINES_NODE_RAW:-—}"
            dockerfile_display="${DF_SOURCE:-—}"

            # Compare
            status="✅"
            if [[ "$our_version" == "dynamic" ]]; then
              status="🔄 Dynamic"
            elif [[ "$our_version" == "unset" ]]; then
              if [[ -n "$upstream_major" ]]; then
                status="⚠️ NODE_VERSION not set (upstream=$upstream_major via $upstream_hint)"
              else
                status="⚠️ NODE_VERSION not set (no upstream info found)"
              fi
              issue_scripts+=("$slug|$our_version|$upstream_major|$upstream_hint|$repo")
              drift_count=$((drift_count + 1))
            elif [[ -n "$upstream_major" && "$our_version" != "$upstream_major" ]]; then
              if (( our_version < upstream_major )); then
                # Check if engines.node is a minimum constraint that our version satisfies
                if [[ -z "$DF_NODE_MAJOR" && "$ENGINES_IS_MINIMUM" == "true" ]] && \
                   version_satisfies_engines "$our_version" "$ENGINES_MIN_MAJOR" "$ENGINES_IS_MINIMUM"; then
                  status="✅ (engines: $ENGINES_NODE_RAW — ours: $our_version satisfies)"
                else
                  status="🔸 Drift → upstream=$upstream_major ($upstream_hint)"
                  issue_scripts+=("$slug|$our_version|$upstream_major|$upstream_hint|$repo")
                  drift_count=$((drift_count + 1))
                fi
              else
                status="✅ Ahead of upstream ($upstream_major via $upstream_hint)"
              fi
            fi

            report_lines+=("| \`$slug\` | $our_version | $engines_display | $dockerfile_display | [$repo](https://github.com/$repo) | $status |")

            # Rate-limit to avoid GitHub secondary rate limits
            sleep 0.3

          done

          # Print summary
          echo ""
          echo "========================================="
          echo "  Total scripts with setup_nodejs: $total"
          echo "  Checked (with GitHub source):    $checked"
          echo "  Version drift detected:          $drift_count"
          echo "========================================="

          # Export
          {
            echo "drift_count=$drift_count"
            echo "total=$total"
            echo "checked=$checked"
          } >> "$GITHUB_OUTPUT"

          # Save issue details for next step
          printf '%s\n' "${issue_scripts[@]}" > /tmp/drift_scripts.txt 2>/dev/null || touch /tmp/drift_scripts.txt

          # Save full report
          {
            echo "## Node.js Version Drift Report"
            echo ""
            echo "**Generated:** $(date -u +%Y-%m-%dT%H:%M:%SZ)"
            echo "**Scripts checked:** $total | **With GitHub source:** $checked | **Drift detected:** $drift_count"
            echo ""
            echo "| Script | Our Version | engines.node | Dockerfile | Upstream Repo | Status |"
            echo "|--------|-------------|-------------|------------|---------------|--------|"
            printf '%s\n' "${report_lines[@]}" | sort
          } > /tmp/drift_report.md

          cat /tmp/drift_report.md

      - name: Create or update summary issue
        if: steps.check.outputs.drift_count != '0'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail

          TITLE="[Automated] Node.js Version Drift Report"
          DATE=$(date -u +%Y-%m-%d)
          DRIFT_COUNT="${{ steps.check.outputs.drift_count }}"
          TOTAL="${{ steps.check.outputs.total }}"
          CHECKED="${{ steps.check.outputs.checked }}"

          # Build checklist from drift data
          CHECKLIST=""
          while IFS='|' read -r slug our_version upstream_major upstream_hint repo; do
            [[ -z "$slug" ]] && continue
            CHECKLIST+="- [ ] **\`${slug}\`** — ours: \`${our_version}\` → upstream: \`${upstream_major}\` (${upstream_hint}) — [repo](https://github.com/${repo})"$'\n'
          done < /tmp/drift_scripts.txt

          # Build full report table
          REPORT=$(cat /tmp/drift_report.md)

          BODY=$(cat <<ISSUE_EOF
          ## Node.js Version Drift Report — ${DATE}

          **${DRIFT_COUNT}** script(s) with version drift detected (out of ${CHECKED} checked / ${TOTAL} total).

          ### Scripts requiring investigation

          ${CHECKLIST}

          ### How to resolve

          1. Check upstream Dockerfile / package.json to confirm the required Node.js version
          2. Test the script with the new Node version
          3. Update \`NODE_VERSION\` in \`install/<slug>-install.sh\`
          4. Update \`NODE_VERSION\` in \`ct/<slug>.sh\` (update section) if applicable
          5. Check off the item above once done

          <details>
          <summary>Full report</summary>

          ${REPORT}

          </details>

          ---
          *This issue is automatically created/updated weekly by the Node.js version drift check workflow.*
          *Last updated: ${DATE}*
          ISSUE_EOF
          )

          # Check if a matching open issue already exists
          EXISTING=$(gh issue list --state open --label "automated,dependencies" --search "\"[Automated] Node.js Version Drift Report\"" --json number --jq '.[0].number // empty' 2>/dev/null || echo "")

          if [[ -n "$EXISTING" ]]; then
            gh issue edit "$EXISTING" --body "$BODY"
            echo "Updated existing issue #$EXISTING"
          else
            gh issue create \
              --title "$TITLE" \
              --body "$BODY" \
              --label "automated,dependencies"
            echo "Created new summary issue"
          fi

      - name: Close issue if no drift
        if: steps.check.outputs.drift_count == '0'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          EXISTING=$(gh issue list --state open --label "automated,dependencies" --search "\"[Automated] Node.js Version Drift Report\"" --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
          if [[ -n "$EXISTING" ]]; then
            gh issue close "$EXISTING" --comment "All Node.js versions are in sync with upstream. Closing automatically."
            echo "Closed issue #$EXISTING"
          fi
