name: Detect Unused NPM Packages

on:
  pull_request:
    paths:
      - 'package.json'
      - 'package-lock.json'
      - 'client/**'
      - 'api/**'
      - 'packages/client/**'
      - 'packages/api/**'

jobs:
  detect-unused-packages:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js 20.x
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install depcheck
        run: npm install -g depcheck

      - name: Validate JSON files
        run: |
          for FILE in package.json client/package.json api/package.json packages/client/package.json; do
            if [[ -f "$FILE" ]]; then
              jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
            fi
          done

      - name: Extract Dependencies Used in Scripts
        id: extract-used-scripts
        run: |
          extract_deps_from_scripts() {
            local package_file=$1
            if [[ -f "$package_file" ]]; then
              jq -r '.scripts | to_entries[].value' "$package_file" | \
                grep -oE '([a-zA-Z0-9_-]+)' | sort -u > used_scripts.txt
            else
              touch used_scripts.txt
            fi
          }

          extract_deps_from_scripts "package.json"
          mv used_scripts.txt root_used_deps.txt

          extract_deps_from_scripts "client/package.json"
          mv used_scripts.txt client_used_deps.txt

          extract_deps_from_scripts "api/package.json"
          mv used_scripts.txt api_used_deps.txt

      - name: Extract Dependencies Used in Source Code
        id: extract-used-code
        run: |
          extract_deps_from_code() {
            local folder=$1
            local output_file=$2
            
            # Initialize empty output file
            > "$output_file"
            
            if [[ -d "$folder" ]]; then
              # Extract require() statements (use explicit includes for portability)
              grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" \
                --include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
                sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" >> "$output_file" || true
          
              # Extract ES6 imports - import x from 'module'
              grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
                --include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
                sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
              
              # import 'module' (side-effect imports)
              grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
                --include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
                sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
              
              # export { x } from 'module' or export * from 'module'
              grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
                --include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
                sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
              
              # import type { x } from 'module' (TypeScript)
              grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
                --include='*.ts' --include='*.tsx' 2>/dev/null | \
                sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
          
              # Remove subpath imports but keep the base package
              # For scoped packages: '@scope/pkg/subpath' -> '@scope/pkg'
              # For regular packages: 'pkg/subpath' -> 'pkg'
              # Scoped packages (must keep @scope/package, strip anything after)
              sed -i -E 's|^(@[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
              # Non-scoped packages (keep package name, strip subpath)
              sed -i -E 's|^([a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
              
              sort -u "$output_file" -o "$output_file"
            fi
          }

          extract_deps_from_code "." root_used_code.txt
          extract_deps_from_code "client" client_used_code.txt
          extract_deps_from_code "api" api_used_code.txt
          
          # Extract dependencies used by workspace packages
          # These packages are used in the workspace but dependencies are provided by parent package.json
          extract_deps_from_code "packages/client" packages_client_used_code.txt
          extract_deps_from_code "packages/api" packages_api_used_code.txt

      - name: Get @librechat/client dependencies
        id: get-librechat-client-deps
        run: |
          if [[ -f "packages/client/package.json" ]]; then
            # Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
            DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
            DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
            PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
            
            # Combine all dependencies
            echo "$DEPS" > librechat_client_deps.txt
            echo "$DEV_DEPS" >> librechat_client_deps.txt
            echo "$PEER_DEPS" >> librechat_client_deps.txt
            
            # Also include dependencies that are imported in packages/client
            cat packages_client_used_code.txt >> librechat_client_deps.txt
            
            # Remove empty lines and sort
            grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
            mv temp_deps.txt librechat_client_deps.txt
          else
            touch librechat_client_deps.txt
          fi

      - name: Get @librechat/api dependencies
        id: get-librechat-api-deps
        run: |
          if [[ -f "packages/api/package.json" ]]; then
            # Get all dependencies from @librechat/api (dependencies, devDependencies, and peerDependencies)
            DEPS=$(jq -r '.dependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
            DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
            PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
            
            # Combine all dependencies
            echo "$DEPS" > librechat_api_deps.txt
            echo "$DEV_DEPS" >> librechat_api_deps.txt
            echo "$PEER_DEPS" >> librechat_api_deps.txt
            
            # Also include dependencies that are imported in packages/api
            cat packages_api_used_code.txt >> librechat_api_deps.txt
            
            # Remove empty lines and sort
            grep -v '^$' librechat_api_deps.txt | sort -u > temp_deps.txt
            mv temp_deps.txt librechat_api_deps.txt
          else
            touch librechat_api_deps.txt
          fi

      - name: Extract Workspace Dependencies
        id: extract-workspace-deps
        run: |
          # Function to get dependencies from a workspace package that are used by another package
          get_workspace_package_deps() {
            local package_json=$1
            local output_file=$2
            
            # Get all workspace dependencies (starting with @librechat/)
            if [[ -f "$package_json" ]]; then
              local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
              
              # For each workspace dependency, get its dependencies
              for dep in $workspace_deps; do
                # Convert @librechat/api to packages/api
                local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
                local workspace_package_json="${workspace_path}/package.json"
                
                if [[ -f "$workspace_package_json" ]]; then
                  # Extract all dependencies from the workspace package
                  jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
                  # Also extract peerDependencies
                  jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
                fi
              done
            fi
            
            if [[ -f "$output_file" ]]; then
              sort -u "$output_file" -o "$output_file"
            else
              touch "$output_file"
            fi
          }
          
          # Get workspace dependencies for each package
          get_workspace_package_deps "package.json" root_workspace_deps.txt
          get_workspace_package_deps "client/package.json" client_workspace_deps.txt
          get_workspace_package_deps "api/package.json" api_workspace_deps.txt

      - name: Run depcheck for root package.json
        id: check-root
        run: |
          if [[ -f "package.json" ]]; then
            UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
            # Exclude dependencies used in scripts, code, and workspace packages
            UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "")
            echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
            echo "$UNUSED" >> $GITHUB_ENV
            echo "EOF" >> $GITHUB_ENV
          fi

      - name: Run depcheck for client/package.json
        id: check-client
        run: |
          if [[ -f "client/package.json" ]]; then
            chmod -R 755 client
            cd client
            UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
            # Exclude dependencies used in scripts, code, workspace packages, and @librechat/client imports
            UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../librechat_client_deps.txt 2>/dev/null | sort -u) || echo "")
            # Filter out false positives
            UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
            echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
            echo "$UNUSED" >> $GITHUB_ENV
            echo "EOF" >> $GITHUB_ENV
            cd ..
          fi

      - name: Run depcheck for api/package.json
        id: check-api
        run: |
          if [[ -f "api/package.json" ]]; then
            chmod -R 755 api
            cd api
            UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
            # Exclude dependencies used in scripts, code, workspace packages, and @librechat/api imports
            UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../librechat_api_deps.txt 2>/dev/null | sort -u) || echo "")
            echo "API_UNUSED<<EOF" >> $GITHUB_ENV
            echo "$UNUSED" >> $GITHUB_ENV
            echo "EOF" >> $GITHUB_ENV
            cd ..
          fi

      - name: Post comment on PR if unused dependencies are found
        if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
        run: |
          PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")

          ROOT_LIST=$(echo "$ROOT_UNUSED" | awk '{print "- `" $0 "`"}')
          CLIENT_LIST=$(echo "$CLIENT_UNUSED" | awk '{print "- `" $0 "`"}')
          API_LIST=$(echo "$API_UNUSED" | awk '{print "- `" $0 "`"}')

          COMMENT_BODY=$(cat <<EOF
          ### 🚨 Unused NPM Packages Detected

          The following **unused dependencies** were found:

          $(if [[ ! -z "$ROOT_UNUSED" ]]; then echo "#### 📂 Root \`package.json\`"; echo ""; echo "$ROOT_LIST"; echo ""; fi)

          $(if [[ ! -z "$CLIENT_UNUSED" ]]; then echo "#### 📂 Client \`client/package.json\`"; echo ""; echo "$CLIENT_LIST"; echo ""; fi)

          $(if [[ ! -z "$API_UNUSED" ]]; then echo "#### 📂 API \`api/package.json\`"; echo ""; echo "$API_LIST"; echo ""; fi)

          ⚠️ **Please remove these unused dependencies to keep your project clean.**
          EOF
          )

          gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
            -f body="$COMMENT_BODY" \
            -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Fail workflow if unused dependencies found
        if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
        run: exit 1
