name: Release

on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:
    inputs:
      version:
        description: "Release version, with or without the leading v (example: 0.2.1)"
        required: true
        type: string

permissions:
  contents: write

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  test-base:
    name: Base tests Python ${{ matrix.python-version }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - name: Install
        run: |
          python -m pip install -U pip
          python -m pip install -e . --no-deps
          python -m pip install "Pillow>=9.0"
      - name: Unit tests
        env:
          HF_HUB_OFFLINE: "1"
          TRANSFORMERS_OFFLINE: "1"
          DIFFUSERS_OFFLINE: "1"
        run: python -m unittest discover -s tests -p "test_*.py" -q

  test-local-diffusers:
    name: Local Diffusers backend tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
          cache: pip
      - name: Install
        run: |
          python -m pip install -U pip
          python -m pip install -e ".[diffusers]"
      - name: Diffusers backend unit tests
        env:
          HF_HUB_OFFLINE: "1"
          TRANSFORMERS_OFFLINE: "1"
          DIFFUSERS_OFFLINE: "1"
        run: python -m unittest tests.test_huggingface_diffusers_backend -q

  docs:
    name: Build docs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
          cache: pip
      - name: Install
        run: |
          python -m pip install -U pip
          python -m pip install "mkdocs>=1.5.0" "mkdocs-material>=9.0.0"
      - name: MkDocs build
        run: mkdocs build -q

  build:
    name: Build distributions
    runs-on: ubuntu-latest
    needs: [test-base, test-local-diffusers, docs]
    outputs:
      version: ${{ steps.release-meta.outputs.version }}
      tag: ${{ steps.release-meta.outputs.tag }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
          cache: pip
      - name: Install build tools
        run: |
          python -m pip install -U pip
          python -m pip install build twine
      - name: Validate tag, version, and changelog
        id: release-meta
        env:
          RELEASE_INPUT_VERSION: ${{ github.event.inputs.version || '' }}
        run: |
          python - <<'PY'
          import os
          import re
          from pathlib import Path

          version_input = os.environ.get("RELEASE_INPUT_VERSION", "").strip()
          ref_name = os.environ["GITHUB_REF_NAME"].strip()
          raw_version = version_input or ref_name
          version = raw_version[1:] if raw_version.startswith("v") else raw_version
          if not re.fullmatch(r"\d+\.\d+\.\d+(?:[a-zA-Z0-9.-]+)?", version):
              raise SystemExit(f"Invalid release version: {raw_version!r}")
          tag = f"v{version}"

          version_text = Path("src/abstractvision/__init__.py").read_text(encoding="utf-8")
          version_match = re.search(r'__version__\s*=\s*"([^"]+)"', version_text)
          if not version_match:
              raise SystemExit("Could not find __version__ in src/abstractvision/__init__.py")
          package_version = version_match.group(1)
          if version != package_version:
              raise SystemExit(
                  f"Tag version {version!r} does not match package version {package_version!r}"
              )

          changelog = Path("CHANGELOG.md").read_text(encoding="utf-8")
          header = re.search(
              rf"^##\s+\[?{re.escape(version)}\]?(?:\s+-.*)?$",
              changelog,
              re.MULTILINE,
          )
          if not header:
              raise SystemExit(f"CHANGELOG.md has no entry for {version}")

          rest = changelog[header.end():]
          next_header = re.search(r"^##\s+", rest, re.MULTILINE)
          notes = rest[: next_header.start() if next_header else len(rest)].strip()
          if not notes:
              raise SystemExit(f"CHANGELOG.md entry for {version} is empty")

          Path("release-notes.md").write_text(notes + "\n", encoding="utf-8")
          github_output = Path(os.environ["GITHUB_OUTPUT"])
          with github_output.open("a", encoding="utf-8") as fh:
              print(f"version={version}", file=fh)
              print(f"tag={tag}", file=fh)
          PY
      - name: Build
        run: python -m build
      - name: Check distributions
        run: python -m twine check dist/*
      - name: Clean base wheel smoke
        run: |
          python -m venv /tmp/abstractvision-base-smoke
          /tmp/abstractvision-base-smoke/bin/python -m pip install -U pip
          /tmp/abstractvision-base-smoke/bin/python -m pip install dist/*.whl
          /tmp/abstractvision-base-smoke/bin/python - <<'PY'
          import importlib.metadata as metadata
          import sys

          import abstractvision
          import abstractvision.backends.openai_compatible
          import abstractvision.integrations.abstractcore_plugin

          blocked = {"torch", "diffusers", "transformers", "PIL", "stable_diffusion_cpp"}
          imported = blocked.intersection(sys.modules)
          if imported:
              raise SystemExit(f"Unexpected heavy imports: {sorted(imported)}")

          eps = metadata.entry_points(group="abstractcore.capabilities_plugins")
          if not any(ep.name == "abstractvision" for ep in eps):
              raise SystemExit("Missing AbstractCore plugin entry point")
          PY
      - uses: actions/upload-artifact@v7
        with:
          name: release-dist
          path: dist/*
      - uses: actions/upload-artifact@v7
        with:
          name: release-notes
          path: release-notes.md

  ensure-tag:
    name: Ensure release tag
    runs-on: ubuntu-latest
    needs: build
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Create annotated tag for manual releases
        if: github.event_name == 'workflow_dispatch'
        run: |
          tag="${{ needs.build.outputs.tag }}"
          if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
            echo "Tag ${tag} already exists on origin."
            exit 1
          fi
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git tag -a "${tag}" -m "AbstractVision ${tag}"
          git push origin "${tag}"
      - name: Tag already supplied by push trigger
        if: github.event_name != 'workflow_dispatch'
        run: echo "Using existing tag ${{ needs.build.outputs.tag }}"

  publish-pypi:
    name: Publish to PyPI
    runs-on: ubuntu-latest
    needs: [build, ensure-tag]
    environment:
      name: pypi
      url: https://pypi.org/project/abstractvision/
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: release-dist
          path: dist
      - uses: pypa/gh-action-pypi-publish@release/v1

  verify-pypi:
    name: Verify PyPI release
    runs-on: ubuntu-latest
    needs: [build, publish-pypi]
    steps:
      - name: Wait for PyPI package
        env:
          VERSION: ${{ needs.build.outputs.version }}
        run: |
          python - <<'PY'
          import json
          import os
          import time
          import urllib.request

          version = os.environ["VERSION"]
          url = f"https://pypi.org/pypi/abstractvision/{version}/json"
          for attempt in range(1, 31):
              try:
                  with urllib.request.urlopen(url, timeout=10) as response:
                      if response.status == 200:
                          json.loads(response.read().decode("utf-8"))
                          print(f"abstractvision {version} is available on PyPI")
                          raise SystemExit(0)
              except Exception as exc:
                  print(f"PyPI not ready yet (attempt {attempt}/30): {exc}")
              time.sleep(10)
          raise SystemExit(f"Timed out waiting for abstractvision {version} on PyPI")
          PY

  github-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    needs: [build, ensure-tag, verify-pypi]
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: release-dist
          path: dist
      - uses: actions/download-artifact@v8
        with:
          name: release-notes
          path: .
      - uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ needs.build.outputs.tag }}
          name: AbstractVision ${{ needs.build.outputs.tag }}
          body_path: release-notes.md
          files: dist/*
          fail_on_unmatched_files: true

  deploy-docs:
    name: Deploy docs to GitHub Pages
    runs-on: ubuntu-latest
    needs: [build, github-release]
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
          cache: pip
      - name: Install docs dependencies
        run: |
          python -m pip install -U pip
          python -m pip install "mkdocs>=1.5.0" "mkdocs-material>=9.0.0"
      - name: Configure git author
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
      - name: Deploy MkDocs site
        run: mkdocs gh-deploy --force --clean --message "Deploy docs for ${{ needs.build.outputs.tag }}"
