name: Release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write
  packages: write
  id-token: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: onllm-dev/4dpocket

jobs:
  # ─── Extract version from tag ─────────────────────────────────
  meta:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      tag: ${{ steps.version.outputs.tag }}
    steps:
      - name: Extract version from tag
        id: version
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          VERSION="${TAG#v}"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "Version: $VERSION (tag: $TAG)"

  # ─── Build per-platform Docker images (native runners) ────────
  docker-build:
    needs: meta
    timeout-minutes: 30
    strategy:
      matrix:
        include:
          - platform: linux/amd64
            runner: ubuntu-latest
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: docker-meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.docker-meta.outputs.labels }}
          cache-from: type=gha,scope=${{ matrix.platform }}
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
          outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true

      - name: Export digest
        run: |
          mkdir -p /tmp/digests
          digest="${{ steps.build.outputs.digest }}"
          touch "/tmp/digests/${digest#sha256:}"

      - name: Upload digest
        uses: actions/upload-artifact@v4
        with:
          name: digest-${{ matrix.runner }}
          path: /tmp/digests/*
          if-no-files-found: error

  # ─── Merge multi-arch manifest and push tags ──────────────────
  docker:
    runs-on: ubuntu-latest
    needs: [meta, docker-build]
    steps:
      - name: Download digests
        uses: actions/download-artifact@v4
        with:
          pattern: digest-*
          path: /tmp/digests
          merge-multiple: true

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta (tags + labels)
        id: docker-meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=latest

      - name: Create and push manifest
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create \
            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)

  # ─── Smoke test published image ────────────────────────────────
  smoke-test:
    runs-on: ubuntu-latest
    needs: [meta, docker]
    steps:
      - name: Smoke test Docker image
        run: |
          docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.meta.outputs.version }}
          docker run -d --name 4dp-smoke -p 4040:4040 \
            -e FDP_DATABASE__URL=sqlite:////data/4dpocket.db \
            -e FDP_AUTH__MODE=single \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.meta.outputs.version }}

          # Wait for health
          for i in $(seq 1 30); do
            if curl -sf http://localhost:4040/api/v1/health > /dev/null 2>&1; then
              echo "Container ready"
              break
            fi
            sleep 1
          done

          # Validate SPA root serves index.html
          STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4040/)
          if [ "$STATUS" != "200" ]; then
            echo "FAIL: GET / returned $STATUS (expected 200)"
            docker logs 4dp-smoke
            exit 1
          fi
          echo "SPA root: OK"

          # Validate API
          STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4040/api/v1/health)
          if [ "$STATUS" != "200" ]; then
            echo "FAIL: API health returned $STATUS (expected 200)"
            docker logs 4dp-smoke
            exit 1
          fi
          echo "API health: OK"

          docker stop 4dp-smoke

  # ─── Build Python package ──────────────────────────────────────
  python:
    runs-on: ubuntu-latest
    needs: meta
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install pnpm
        run: corepack enable

      - name: Build frontend
        working-directory: frontend
        run: pnpm install --frozen-lockfile && pnpm build

      - name: Install build tools
        run: pip install build

      - name: Verify version matches tag
        run: |
          PKG_VERSION=$(python -c "
          import re
          with open('pyproject.toml') as f:
              m = re.search(r'version\s*=\s*\"(.+?)\"', f.read())
              print(m.group(1))
          ")
          if [ "$PKG_VERSION" != "${{ needs.meta.outputs.version }}" ]; then
            echo "ERROR: pyproject.toml version ($PKG_VERSION) != tag (${{ needs.meta.outputs.version }})"
            exit 1
          fi

      - name: Build sdist and wheel
        run: python -m build

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: python-dist
          path: dist/

  # ─── Build Chrome extension ────────────────────────────────────
  extension:
    runs-on: ubuntu-latest
    needs: meta
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install pnpm
        run: corepack enable

      - name: Install dependencies
        working-directory: extension
        run: pnpm install --frozen-lockfile

      - name: Build Chrome extension
        working-directory: extension
        run: pnpm build

      - name: Zip extension
        run: |
          cd extension/dist/chrome-mv3
          zip -r ../../../4dpocket-chrome-extension-${{ needs.meta.outputs.version }}.zip .

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: chrome-extension
          path: 4dpocket-chrome-extension-${{ needs.meta.outputs.version }}.zip

  # ─── Publish MCP npm package (Trusted Publishing via OIDC) ────
  # Setup: npmjs.com → org "onllm-dev" → package/org Trusted Publishers →
  # Add GitHub Actions with repo=onllm-dev/4DPocket, workflow=release.yml.
  # No long-lived NPM_TOKEN needed — CI authenticates via GitHub OIDC.
  npm:
    runs-on: ubuntu-latest
    needs: meta
    permissions:
      contents: read
      id-token: write  # required for OIDC trusted publishing
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: "https://registry.npmjs.org"

      - name: Upgrade npm to version supporting trusted publishing
        # npm >= 11.5.1 is required for OIDC trusted publishing. Installing
        # into a user prefix avoids the known self-upgrade bug where the
        # existing system npm breaks mid-update (MODULE_NOT_FOUND for
        # promise-retry inside arborist).
        run: |
          NPM_PREFIX="$HOME/.npm-global"
          mkdir -p "$NPM_PREFIX"
          npm install -g --prefix "$NPM_PREFIX" npm@11.12.1
          echo "$NPM_PREFIX/bin" >> "$GITHUB_PATH"

      - name: Verify mcp-npm version matches tag
        working-directory: mcp-npm
        run: |
          PKG_VERSION=$(node -p "require('./package.json').version")
          if [ "$PKG_VERSION" != "${{ needs.meta.outputs.version }}" ]; then
            echo "ERROR: mcp-npm/package.json version ($PKG_VERSION) != tag (${{ needs.meta.outputs.version }})"
            exit 1
          fi

      - name: Publish to npm (trusted publishing + provenance)
        working-directory: mcp-npm
        run: |
          VERSION="${{ needs.meta.outputs.version }}"
          if npm view "@onllm-dev/4dpocket-mcp@$VERSION" version > /dev/null 2>&1; then
            echo "@onllm-dev/4dpocket-mcp@$VERSION is already on npm — skipping publish."
            exit 0
          fi
          npm publish --access public --provenance

  # ─── Create GitHub Release ─────────────────────────────────────
  # Intentionally does NOT depend on `npm` — the npm job requires the trusted
  # publisher to be configured on npmjs.com, which is a one-time manual step.
  # Keep the release notes / docker / pypi flow resilient if npm fails.
  release:
    runs-on: ubuntu-latest
    needs: [meta, docker, python, extension, smoke-test]
    steps:
      - uses: actions/checkout@v4

      - name: Download Python artifacts
        uses: actions/download-artifact@v4
        with:
          name: python-dist
          path: dist/

      - name: Download Chrome extension
        uses: actions/download-artifact@v4
        with:
          name: chrome-extension
          path: .

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.meta.outputs.tag }}
          name: 4DPocket ${{ needs.meta.outputs.tag }}
          generate_release_notes: true
          files: |
            dist/*
            4dpocket-chrome-extension-${{ needs.meta.outputs.version }}.zip
          body: |
            ## Install

            **Docker (recommended):**
            ```bash
            docker pull ghcr.io/onllm-dev/4dpocket:${{ needs.meta.outputs.version }}
            ```

            **Docker Compose:**
            ```bash
            curl -O https://raw.githubusercontent.com/onllm-dev/4DPocket/${{ needs.meta.outputs.tag }}/docker-compose.yml
            curl -O https://raw.githubusercontent.com/onllm-dev/4DPocket/${{ needs.meta.outputs.tag }}/.env.example
            cp .env.example .env
            docker compose up -d
            ```

            **Python:**
            ```bash
            pip install 4dpocket==${{ needs.meta.outputs.version }}
            ```

            ## MCP Client

            Connect stdio-based MCP clients (Claude Desktop, Cursor, etc.) to your instance:

            ```bash
            npx -y @onllm-dev/4dpocket-mcp --url https://your.pocket.tld --token fdp_pat_xxx
            ```

            See [`mcp-npm/README.md`](https://github.com/onllm-dev/4DPocket/blob/${{ needs.meta.outputs.tag }}/mcp-npm/README.md) for full client config snippets.

            ## Chrome Extension

            1. Download `4dpocket-chrome-extension-${{ needs.meta.outputs.version }}.zip` from the assets below
            2. Unzip it
            3. Go to `chrome://extensions`, enable **Developer mode**
            4. Click **Load unpacked** and select the unzipped folder
            5. Click the extension icon, open **Settings**, enter your server URL and login

  # ─── Publish to PyPI (trusted publisher via OIDC) ─────────────
  pypi:
    runs-on: ubuntu-latest
    needs: [meta, python, smoke-test]
    environment: pypi
    permissions:
      id-token: write
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: python-dist
          path: dist/

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
