name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_run:
    workflows: ["Skill Release"]
    types: [completed]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    # Production build only: manual dispatch, push to main, or trusted release workflows.
    # PR validation runs in .github/workflows/pages-verify.yml.
    if: |
      github.event_name == 'workflow_dispatch' ||
      (
        github.event_name == 'push' &&
        github.ref_name == 'main'
      ) ||
      (
        github.event_name == 'workflow_run' &&
        github.event.workflow_run.conclusion == 'success' &&
        github.event.workflow_run.name == 'Skill Release' &&
        github.event.workflow_run.event != 'pull_request'
      )
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Verify signing key consistency (repo + docs)
        run: ./scripts/ci/verify_signing_key_consistency.sh

      - name: Auto-discover skills from releases
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail
          mkdir -p public/skills
          mkdir -p public/releases/download

          echo "Fetching releases from GitHub API..."

          # Helper function to download release asset by ID (works for private repos)
          download_asset() {
            local asset_id="$1"
            local output_file="$2"
            curl -fsSL \
              -H "Authorization: Bearer ${GITHUB_TOKEN}" \
              -H "Accept: application/octet-stream" \
              "https://api.github.com/repos/${REPO}/releases/assets/${asset_id}" \
              -o "$output_file"
          }
          export -f download_asset  # Export for use in subshells (while loop)

          # Fetch all releases (paginated)
          RELEASES=$(gh api --paginate \
            -H "Accept: application/vnd.github+json" \
            "/repos/${REPO}/releases?per_page=100" \
            | jq -s 'add // []')

          # Start building skills index
          echo '{"version":"1.0.0","updated":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skills":[' > public/skills/index.json

          FIRST_SKILL=true
          declare -A PROCESSED_SKILLS=()

          # Process each release (using process substitution to avoid subshell)
          while read -r release; do
            TAG=$(echo "$release" | jq -r '.tag_name')

            # Parse skill-name-v* pattern
            if [[ "$TAG" =~ ^(.+)-v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
              SKILL_NAME="${BASH_REMATCH[1]}"
              VERSION="${BASH_REMATCH[2]}"

              # Skip if we already processed a newer version of this skill
              if [[ -n "${PROCESSED_SKILLS[$SKILL_NAME]+x}" ]]; then
                echo "Skipping older version: $TAG (already have newer)"
                continue
              fi

              echo "Processing: $SKILL_NAME v$VERSION"

              # Get skill.json asset ID from release
              SKILL_JSON_ID=$(echo "$release" | jq -r '.assets[] | select(.name=="skill.json") | .id')

              if [ -n "$SKILL_JSON_ID" ] && [ "$SKILL_JSON_ID" != "null" ]; then
                # Basic safety checks before using tag/asset names as paths
                if [[ "$TAG" == *"/"* ]] || [[ "$TAG" == *".."* ]]; then
                  echo "  Warning: Skipping suspicious tag name: $TAG"
                  continue
                fi

                # Download skill.json first to decide whether the skill is internal
                SKILL_JSON_TMP=$(mktemp)
                download_asset "$SKILL_JSON_ID" "$SKILL_JSON_TMP"

                # Skip internal skills (not shown in public catalog or mirrored)
                IS_INTERNAL=$(jq -r '.openclaw.internal // false' "$SKILL_JSON_TMP")
                if [ "$IS_INTERNAL" = "true" ]; then
                  echo "  Skipping internal skill: $SKILL_NAME"
                  rm -f "$SKILL_JSON_TMP"
                  continue
                fi

                # Security: Download to temp directory first, verify signatures, then mirror to final location.
                # This ensures unverified releases never appear in public/releases or the skills catalog.

                # Use temp directory for downloads before verification
                TEMP_DOWNLOAD_DIR=$(mktemp -d)

                # Move skill.json to temp dir first
                mv "$SKILL_JSON_TMP" "$TEMP_DOWNLOAD_DIR/skill.json"

                # Download all remaining assets to temp dir
                while read -r asset; do
                  ASSET_ID=$(echo "$asset" | jq -r '.id')
                  ASSET_NAME=$(echo "$asset" | jq -r '.name')

                  # Prevent path traversal / nested directories
                  if [[ "$ASSET_NAME" == *"/"* ]] || [[ "$ASSET_NAME" == *".."* ]]; then
                    echo "  Warning: Skipping suspicious asset name: $ASSET_NAME"
                    continue
                  fi

                  # Already downloaded above
                  if [ "$ASSET_NAME" = "skill.json" ]; then
                    continue
                  fi

                  download_asset "$ASSET_ID" "$TEMP_DOWNLOAD_DIR/$ASSET_NAME"
                  echo "  Downloaded to temp: $ASSET_NAME"
                done < <(echo "$release" | jq -c '.assets[]')

                # Verify signed checksums when signature artifacts are present.
                # Legacy releases without signatures are still mirrored for backward compatibility.
                if [ -f "$TEMP_DOWNLOAD_DIR/checksums.sig" ] && [ -f "$TEMP_DOWNLOAD_DIR/signing-public.pem" ] && [ -f "$TEMP_DOWNLOAD_DIR/checksums.json" ]; then
                  openssl base64 -d -A -in "$TEMP_DOWNLOAD_DIR/checksums.sig" -out "$TEMP_DOWNLOAD_DIR/checksums.sig.bin"
                  # Verify Ed25519 signature (requires -rawin)
                  if ! openssl pkeyutl -verify -rawin -pubin -inkey "$TEMP_DOWNLOAD_DIR/signing-public.pem" -sigfile "$TEMP_DOWNLOAD_DIR/checksums.sig.bin" -in "$TEMP_DOWNLOAD_DIR/checksums.json"; then
                    echo "  Warning: Invalid checksums signature for $TAG; skipping skill"
                    rm -rf "$TEMP_DOWNLOAD_DIR"
                    continue
                  fi
                  rm -f "$TEMP_DOWNLOAD_DIR/checksums.sig.bin"
                  echo "  Verified checksums signature"
                elif [ -f "$TEMP_DOWNLOAD_DIR/checksums.json" ]; then
                  echo "  Warning: Unsigned legacy checksums for $TAG (missing checksums.sig/signing-public.pem)"
                fi

                # Verification passed or skipped (legacy) - mirror to final location
                MIRROR_DIR="public/releases/download/${TAG}"
                mkdir -p "$MIRROR_DIR"
                cp -r "$TEMP_DOWNLOAD_DIR"/* "$MIRROR_DIR"/
                echo "  Mirrored to: $MIRROR_DIR"

                # Clean up temp directory
                rm -rf "$TEMP_DOWNLOAD_DIR"

                # Copy the subset needed for the site catalog (skill pages)
                mkdir -p "public/skills/${SKILL_NAME}"
                cp "$MIRROR_DIR/skill.json" "public/skills/${SKILL_NAME}/skill.json"
                echo "  Added to catalog: skill.json"

                for file in checksums.json checksums.sig signing-public.pem README.md SKILL.md; do
                  if [ -f "$MIRROR_DIR/$file" ]; then
                    cp "$MIRROR_DIR/$file" "public/skills/${SKILL_NAME}/$file"
                    echo "  Added to catalog: $file"
                  fi
                done

                # Build skill entry for index
                SKILL_DATA=$(jq -c --arg tag "$TAG" '
                  . as $skill |
                  def object_or_empty($value):
                    if ($value | type) == "object" then $value else {} end;
                  def object_field($name):
                    object_or_empty($skill[$name]?);
                  def platform_meta:
                    ($skill.platform as $platform
                      | if ($platform | type) == "string" then object_or_empty($skill[$platform]?)
                        else {}
                        end);
                  def platform_list:
                    ([]
                      + (if ($skill.platforms | type) == "array" then $skill.platforms else [] end)
                      + (if ($skill.platform | type) == "string" then [$skill.platform] else [] end)
                      + (["openclaw", "hermes", "nanoclaw", "picoclaw"] | map(select((object_field(.) | length) > 0))))
                    | map(select(type == "string") | ascii_downcase)
                    | unique;
                  {
                    id: .name,
                    name: .name,
                    version: .version,
                    description: .description,
                    emoji: (platform_meta.emoji // object_field("openclaw").emoji // object_field("hermes").emoji // object_field("nanoclaw").emoji // object_field("picoclaw").emoji // "📦"),
                    category: (platform_meta.category // object_field("openclaw").category // object_field("hermes").category // object_field("nanoclaw").category // object_field("picoclaw").category // "utility"),
                    platforms: platform_list,
                    trust: .trust.level,
                    tag: $tag
                  }
                ' "$MIRROR_DIR/skill.json")

                # Append to index (handle first entry without comma)
                if [ -f "public/skills/.first_done" ]; then
                  echo "," >> public/skills/index.json
                else
                  touch "public/skills/.first_done"
                fi
                echo "$SKILL_DATA" >> public/skills/index.json

                # Mark this skill as processed (track newest only)
                PROCESSED_SKILLS["$SKILL_NAME"]=1
              else
                echo "  Warning: skill.json not found in release assets"
              fi
            fi
          done < <(echo "$RELEASES" | jq -c '.[]')

          # Close the JSON array
          echo ']}' >> public/skills/index.json

          # Clean up temp file
          rm -f "public/skills/.first_done"

          echo ""
          echo "=== Skills Index ==="
          cat public/skills/index.json | jq . || cat public/skills/index.json

          echo ""
          echo "=== Skills Directory ==="
          ls -la public/skills/

      - name: Copy advisory feed to public
        run: |
          set -euo pipefail
          mkdir -p public/advisories
          cp advisories/feed.json public/advisories/feed.json
          if [ -f advisories/ghsa-without-cve.json ]; then
            cp advisories/ghsa-without-cve.json public/advisories/ghsa-without-cve.json
          fi
          echo "Copied advisory feed to public/advisories/"
          cat public/advisories/feed.json | jq '.advisories | length' | xargs -I {} echo "Feed contains {} advisories"
          if [ -f public/advisories/ghsa-without-cve.json ]; then
            cat public/advisories/ghsa-without-cve.json | jq '.advisories | length' | xargs -I {} echo "GHSA provisional feed contains {} advisories"
          fi

      - name: Sign advisory feed and verify
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: public/advisories/feed.json
          signature_file: public/advisories/feed.json.sig
          public_key_output: public/signing-public.pem

      - name: Sign provisional GHSA feed and verify
        if: hashFiles('public/advisories/ghsa-without-cve.json') != ''
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: public/advisories/ghsa-without-cve.json
          signature_file: public/advisories/ghsa-without-cve.json.sig

      - name: Generate advisory checksums manifest
        run: |
          set -euo pipefail

          FILES_JSON="{}"
          ADVISORY_ARTIFACTS=(public/advisories/*.json public/advisories/*.json.sig)
          for file in "${ADVISORY_ARTIFACTS[@]}"; do
            [ -e "$file" ] || continue
            REL_PATH="${file#public/}"
            FILE_SHA=$(sha256sum "$file" | awk '{print $1}')
            FILE_SIZE=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
            FILES_JSON=$(jq \
              --arg path "$REL_PATH" \
              --arg sha "$FILE_SHA" \
              --argjson size "$FILE_SIZE" \
              '. + {($path): {sha256: $sha, size: $size, path: $path, url: ("https://clawsec.prompt.security/" + $path)}}' \
              <<< "$FILES_JSON")
          done

          # Generate checksums manifest conforming to parseChecksumsManifest expectations:
          # - schema_version: "1" (manifest format version)
          # - algorithm: "sha256" (hash algorithm)
          # - version: "1.1.0" (feed content version, for informational purposes)
          # - generated_at, repository: metadata
          # - files: map of path -> {sha256, size, path, url}
          jq -n \
            --arg schema_version "1" \
            --arg algorithm "sha256" \
            --arg version "1.1.0" \
            --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --arg repo "${{ github.repository }}" \
            --argjson files "$FILES_JSON" \
            '{
              schema_version: $schema_version,
              algorithm: $algorithm,
              version: $version,
              generated_at: $generated,
              repository: $repo,
              files: $files
            }' > public/checksums.json

          echo "Generated public/checksums.json"
          jq . public/checksums.json

      - name: Sign checksums and verify
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: public/checksums.json
          signature_file: public/checksums.sig

      - name: Verify generated public signing key matches canonical key
        run: |
          set -euo pipefail
          CANONICAL_FPR=$(openssl pkey -pubin -in clawsec-signing-public.pem -outform DER | sha256sum | awk '{print $1}')
          GENERATED_FPR=$(openssl pkey -pubin -in public/signing-public.pem -outform DER | sha256sum | awk '{print $1}')
          echo "Canonical key fingerprint: $CANONICAL_FPR"
          echo "Generated key fingerprint: $GENERATED_FPR"
          if [ "$CANONICAL_FPR" != "$GENERATED_FPR" ]; then
            echo "::error::public/signing-public.pem fingerprint mismatch vs clawsec-signing-public.pem"
            exit 1
          fi

      - name: Copy public key to advisory directory
        run: |
          # Clients expect the public key at advisories/feed-signing-public.pem
          mkdir -p public/advisories
          cp public/signing-public.pem public/advisories/feed-signing-public.pem
          echo "Public key available at:"
          echo "  - public/signing-public.pem (root)"
          echo "  - public/advisories/feed-signing-public.pem (advisory-specific)"

      - name: Show signed advisory artifacts
        run: |
          echo "Signed advisory artifacts:"
          ls -la public/advisories/*.json*
          ls -la public/checksums.json public/checksums.sig public/signing-public.pem

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: '20'
          cache: 'npm'

      - name: Get latest clawsec-suite release URL
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
        run: |
          LATEST_TAG=$(
            gh api --paginate \
              -H "Accept: application/vnd.github+json" \
              "/repos/${REPO}/releases?per_page=100" \
              | jq -r -s 'add // [] | [.[] | select(.tag_name | startswith("clawsec-suite-v"))] | first | .tag_name // empty'
          )
          
          if [ -n "$LATEST_TAG" ]; then
            echo "Found latest clawsec-suite tag: $LATEST_TAG"
            echo "VITE_CLAWSEC_SUITE_URL=https://clawsec.prompt.security/releases/download/${LATEST_TAG}/SKILL.md" >> $GITHUB_ENV

            # Create a local "latest" mirror path for clients that use GitHub-style URLs.
            # This enables swapping the host:
            #   https://github.com/<repo>/releases/latest/download/<file>
            # → https://clawsec.prompt.security/releases/latest/download/<file>
            MIRROR_TAG_DIR="public/releases/download/${LATEST_TAG}"
            MIRROR_LATEST_DIR="public/releases/latest/download"
            rm -rf "$MIRROR_LATEST_DIR"
            mkdir -p "$MIRROR_LATEST_DIR"

            if [ -d "$MIRROR_TAG_DIR" ]; then
              cp -f "$MIRROR_TAG_DIR"/* "$MIRROR_LATEST_DIR"/ 2>/dev/null || true
              echo "Mirrored suite release assets to: $MIRROR_LATEST_DIR"
            else
              echo "Warning: Suite release assets not mirrored (missing: $MIRROR_TAG_DIR)"
            fi

            # Mirror advisories feed + signatures at the path referenced by suite docs/heartbeat
            if [ -f "public/advisories/feed.json" ]; then
              mkdir -p "$MIRROR_LATEST_DIR/advisories"
              cp "public/advisories/feed.json" "$MIRROR_LATEST_DIR/advisories/feed.json"
              cp "public/advisories/feed.json" "$MIRROR_LATEST_DIR/feed.json"
            fi
            if [ -f "public/advisories/feed.json.sig" ]; then
              mkdir -p "$MIRROR_LATEST_DIR/advisories"
              cp "public/advisories/feed.json.sig" "$MIRROR_LATEST_DIR/advisories/feed.json.sig"
              cp "public/advisories/feed.json.sig" "$MIRROR_LATEST_DIR/feed.json.sig"
            fi
            if [ -f "public/advisories/ghsa-without-cve.json" ]; then
              mkdir -p "$MIRROR_LATEST_DIR/advisories"
              cp "public/advisories/ghsa-without-cve.json" "$MIRROR_LATEST_DIR/advisories/ghsa-without-cve.json"
              cp "public/advisories/ghsa-without-cve.json" "$MIRROR_LATEST_DIR/ghsa-without-cve.json"
            fi
            if [ -f "public/advisories/ghsa-without-cve.json.sig" ]; then
              mkdir -p "$MIRROR_LATEST_DIR/advisories"
              cp "public/advisories/ghsa-without-cve.json.sig" "$MIRROR_LATEST_DIR/advisories/ghsa-without-cve.json.sig"
              cp "public/advisories/ghsa-without-cve.json.sig" "$MIRROR_LATEST_DIR/ghsa-without-cve.json.sig"
            fi
            if [ -f "public/checksums.json" ]; then
              cp "public/checksums.json" "$MIRROR_LATEST_DIR/checksums.json"
            fi
            if [ -f "public/checksums.sig" ]; then
              cp "public/checksums.sig" "$MIRROR_LATEST_DIR/checksums.sig"
            fi
            if [ -f "public/signing-public.pem" ]; then
              cp "public/signing-public.pem" "$MIRROR_LATEST_DIR/signing-public.pem"
            fi
          else
            echo "No clawsec-suite release found, using fallback"
          fi

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NODE_ENV: production
          VITE_CLAWSEC_SUITE_URL: ${{ env.VITE_CLAWSEC_SUITE_URL }}

      - name: Copy skills data to dist
        run: |
          cp -r public/skills dist/skills 2>/dev/null || echo "No skills directory"
          cp public/checksums.json dist/checksums.json 2>/dev/null || echo "No checksums manifest"
          cp public/checksums.sig dist/checksums.sig 2>/dev/null || echo "No checksums signature"
          cp public/signing-public.pem dist/signing-public.pem 2>/dev/null || echo "No signing public key"
          cp -r public/advisories dist/advisories 2>/dev/null || echo "No advisories directory"

          echo "=== Dist contents ==="
          ls -la dist/
          ls -la dist/skills/ 2>/dev/null || echo "No skills in dist"
          ls -la dist/advisories/ 2>/dev/null || echo "No advisories in dist"

      - name: Add .nojekyll file
        run: touch dist/.nojekyll

      - name: Setup Pages
        uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

      - name: Upload artifact
        uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
        with:
          path: ./dist

  deploy:
    # Deploy after a production build succeeds.
    if: |
      github.event_name == 'workflow_dispatch' ||
      (
        github.event_name == 'push' &&
        github.ref_name == 'main'
      ) ||
      (
        github.event_name == 'workflow_run' &&
        github.event.workflow_run.conclusion == 'success' &&
        github.event.workflow_run.name == 'Skill Release' &&
        github.event.workflow_run.event != 'pull_request'
      )
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
