name: CI

on:
  push:
    branches: [main]
    paths:
      - 'codeflash/**'
      - 'codeflash-benchmark/**'
      - 'codeflash-java-runtime/**'
      - 'tests/**'
      - 'packages/**'
      - 'pyproject.toml'
      - 'uv.lock'
      - 'mypy_allowlist.txt'
      - '.github/workflows/ci.yaml'
      - '.github/actions/**'
  pull_request:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}
  cancel-in-progress: true

jobs:
  # ---------------------------------------------------------------------------
  # Linked issue check — every PR must reference an issue or discussion.
  # Skipped on push to main and workflow_dispatch.
  # ---------------------------------------------------------------------------
  check-linked-issue:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    steps:
      - name: Check PR body for linked issue or discussion
        env:
          PR_BODY: ${{ github.event.pull_request.body }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
          AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
        run: |
          # Skip for bots (dependabot, renovate, github-actions)
          if [[ "$PR_AUTHOR" == *"[bot]"* || "$PR_AUTHOR" == "dependabot" ]]; then
            echo "Bot PR — skipping linked issue check."
            exit 0
          fi

          # Skip for org members
          if [[ "$AUTHOR_ASSOCIATION" == "MEMBER" ]]; then
            echo "Org member ($PR_AUTHOR) — skipping linked issue check."
            exit 0
          fi

          if [ -z "$PR_BODY" ]; then
            echo "::error::PR body is empty. Every PR must link an issue or discussion."
            echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
            exit 1
          fi

          # Match: #123, GH-123, org/repo#123, Closes/Fixes/Relates/Resolves #123,
          # or a github.com URL to an issue or discussion
          if echo "$PR_BODY" | grep -qiP '(close[sd]?|fix(e[sd])?|relate[sd]?\s+to|resolve[sd]?)\s+#\d+'; then
            echo "Found linked issue keyword."
            exit 0
          fi
          if echo "$PR_BODY" | grep -qP '#\d+'; then
            echo "Found issue reference."
            exit 0
          fi
          if echo "$PR_BODY" | grep -qiP 'github\.com/[^\s]+/(issues|discussions)/\d+'; then
            echo "Found GitHub issue/discussion URL."
            exit 0
          fi
          if echo "$PR_BODY" | grep -qiP 'CF-#?\d+'; then
            echo "Found Linear ticket reference."
            exit 0
          fi

          echo "::error::No linked issue or discussion found in PR body."
          echo "Every PR must reference an issue or discussion. See CONTRIBUTING.md for details."
          echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
          exit 1

  # ---------------------------------------------------------------------------
  # Change detection — decides which downstream jobs actually run.
  # On push/workflow_dispatch every flag is true so all jobs execute.
  # On pull_request we diff against the merge base (same approach as astral-sh/ruff).
  # ---------------------------------------------------------------------------
  determine-changes:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      unit_tests: ${{ github.event_name != 'pull_request' || steps.check.outputs.unit_tests == 'true' }}
      type_check: ${{ github.event_name != 'pull_request' || steps.check.outputs.type_check == 'true' }}
      e2e: ${{ github.event_name != 'pull_request' || steps.check.outputs.e2e == 'true' }}
      e2e_js: ${{ github.event_name != 'pull_request' || steps.check.outputs.e2e_js == 'true' }}
      e2e_java: ${{ github.event_name != 'pull_request' || steps.check.outputs.e2e_java == 'true' }}
    steps:
      - uses: actions/checkout@v6
        if: github.event_name == 'pull_request'
        with:
          fetch-depth: 0

      - name: Determine merge base
        if: github.event_name == 'pull_request'
        id: merge_base
        run: |
          sha=$(git merge-base HEAD "origin/${{ github.event.pull_request.base.ref }}")
          echo "sha=${sha}" >> "$GITHUB_OUTPUT"

      - name: Check changed paths
        if: github.event_name == 'pull_request'
        id: check
        run: |
          check_paths() {
            local name="$1"; shift
            if ! git diff --quiet "$MERGE_BASE...HEAD" -- "$@" 2>/dev/null; then
              echo "${name}=true" >> "$GITHUB_OUTPUT"
            else
              echo "${name}=false" >> "$GITHUB_OUTPUT"
            fi
          }

          # Unit tests: code + test infra + packages + build config
          check_paths unit_tests \
            'codeflash/' 'codeflash-benchmark/' \
            'tests/' 'packages/' 'pyproject.toml' 'uv.lock'

          # Type checking: code + build config + mypy config
          check_paths type_check \
            'codeflash/' 'pyproject.toml' 'uv.lock' 'mypy_allowlist.txt'

          # E2E tests: Python pipeline + tests + build config (excludes java/ and javascript/)
          check_paths e2e \
            'codeflash/*.py' \
            'codeflash/api/' 'codeflash/benchmarking/' 'codeflash/cli_cmds/' \
            'codeflash/code_utils/' 'codeflash/discovery/' 'codeflash/github/' \
            'codeflash/languages/python/' 'codeflash/languages/*.py' \
            'codeflash/lsp/' 'codeflash/models/' 'codeflash/optimization/' \
            'codeflash/picklepatch/' 'codeflash/result/' 'codeflash/setup/' \
            'codeflash/telemetry/' 'codeflash/tracing/' 'codeflash/verification/' \
            'tests/' 'pyproject.toml' 'uv.lock'

          # JS E2E tests: JS language support + shared pipeline + packages + test fixtures
          check_paths e2e_js \
            'codeflash/languages/javascript/' 'codeflash/languages/base.py' \
            'codeflash/languages/registry.py' 'codeflash/optimization/' \
            'codeflash/verification/' 'packages/' \
            'code_to_optimize/js/' 'tests/scripts/end_to_end_test_js*'

          # Java E2E tests: Java language support + shared pipeline + runtime
          check_paths e2e_java \
            'codeflash/languages/java/' 'codeflash/languages/base.py' \
            'codeflash/languages/registry.py' 'codeflash/optimization/' \
            'codeflash/verification/' 'codeflash-java-runtime/' \
            'code_to_optimize/java/' 'tests/scripts/end_to_end_test_java*' \
            'tests/test_languages/fixtures/java_tracer_e2e/'
        env:
          MERGE_BASE: ${{ steps.merge_base.outputs.sha }}

  # ---------------------------------------------------------------------------
  # Unit tests — 6 Linux + 1 Windows matrix
  # ---------------------------------------------------------------------------
  unit-tests:
    needs: determine-changes
    if: needs.determine-changes.outputs.unit_tests == 'true'
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            python-version: "3.9"
          - os: ubuntu-latest
            python-version: "3.10"
          - os: ubuntu-latest
            python-version: "3.11"
          - os: ubuntu-latest
            python-version: "3.12"
          - os: ubuntu-latest
            python-version: "3.13"
          - os: ubuntu-latest
            python-version: "3.14"
          - os: windows-latest
            python-version: "3.13"
    runs-on: ${{ matrix.os }}
    env:
      PYTHONIOENCODING: utf-8
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true

      - name: Install dependencies
        shell: bash
        run: |
          if [[ "${{ matrix.python-version }}" == "3.9" || "${{ matrix.python-version }}" == "3.13" ]]; then
            uv sync --group tests
          else
            uv sync
          fi

      - name: Unit tests
        run: uv run pytest tests/

  # ---------------------------------------------------------------------------
  # Coverage — single run on ubuntu/py3.13 to enforce the coverage floor.
  # ---------------------------------------------------------------------------
  coverage:
    needs: determine-changes
    if: needs.determine-changes.outputs.unit_tests == 'true'
    runs-on: ubuntu-latest
    env:
      PYTHONIOENCODING: utf-8
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          python-version: "3.13"
          enable-cache: true

      - name: Install dependencies
        run: uv sync

      - name: Run tests with coverage
        run: uv run pytest tests/ --ignore=tests/test_tracer.py --cov=codeflash --cov-report=xml:coverage.xml --cov-report=term-missing --cov-config=.coveragerc

      - name: Check coverage floor
        run: uv run coverage report --fail-under=58

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage.xml
          retention-days: 30

  # ---------------------------------------------------------------------------
  # Mypy type checking
  # ---------------------------------------------------------------------------
  type-check:
    needs: determine-changes
    if: needs.determine-changes.outputs.type_check == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          enable-cache: true

      - name: Install dependencies
        run: uv sync

      - name: Run mypy
        run: uv run mypy --non-interactive --config-file pyproject.toml @mypy_allowlist.txt

  # ---------------------------------------------------------------------------
  # Lint (prek) — pull_request only
  # ---------------------------------------------------------------------------
  prek:
    needs: determine-changes
    if: >-
      github.event_name == 'pull_request'
      && (needs.determine-changes.outputs.e2e == 'true'
          || needs.determine-changes.outputs.e2e_js == 'true')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.head_ref }}
          fetch-depth: 0
      - uses: astral-sh/setup-uv@v8.0.0
        with:
          enable-cache: true

      - name: Auto-fix formatting
        run: |
          uv run ruff check --fix . || true
          uv run ruff format .
          # uv-dynamic-versioning rewrites version.py on every `uv run` — discard those changes
          git checkout HEAD -- codeflash/version.py codeflash-benchmark/codeflash_benchmark/version.py 2>/dev/null || true

      - name: Commit and push fixes
        run: |
          git diff --quiet && exit 0
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add -u
          git commit -m "style: auto-format with ruff"
          git push

      - uses: j178/prek-action@v2
        with:
          extra-args: '--from-ref origin/${{ github.base_ref }} --to-ref HEAD'

  # ---------------------------------------------------------------------------
  # E2E tests — only on pull_request and workflow_dispatch (not push to main)
  # ---------------------------------------------------------------------------

  # --- Standard Python E2Es (9 tests) ---
  e2e-python:
    needs: determine-changes
    if: >-
      needs.determine-changes.outputs.e2e == 'true'
      && github.event_name != 'push'
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: tracer-replay
            script: end_to_end_test_tracer_replay.py
            expected_improvement: 10
          - name: bubble-sort-pytest-nogit
            script: end_to_end_test_bubblesort_pytest.py
            expected_improvement: 70
            remove_git: true
          - name: bubble-sort-unittest
            script: end_to_end_test_bubblesort_unittest.py
            expected_improvement: 40
          - name: futurehouse-structure
            script: end_to_end_test_futurehouse.py
            expected_improvement: 5
          - name: topological-sort
            script: end_to_end_test_topological_sort_worktree.py
            expected_improvement: 5
          - name: async-optimization
            script: end_to_end_test_async.py
            expected_improvement: 10
          - name: benchmark-bubble-sort
            script: end_to_end_test_benchmark_sort.py
            expected_improvement: 5
          - name: coverage-e2e
            script: end_to_end_test_coverage.py
            extra_deps: black
          - name: init-optimization
            script: end_to_end_test_init_optimization.py
            expected_improvement: 10
    environment: ${{ ((github.event_name == 'workflow_dispatch' && github.actor != 'misrasaurabh1' && github.actor != 'KRRT7') || (contains(toJSON(github.event.pull_request.files.*.filename), '.github/workflows/') && github.event.pull_request.user.login != 'misrasaurabh1' && github.event.pull_request.user.login != 'KRRT7')) && 'external-trusted-contributors' || '' }}
    runs-on: ubuntu-latest
    env:
      CODEFLASH_AIS_SERVER: prod
      POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
      CODEFLASH_API_KEY: ${{ secrets.CODEFLASH_API_KEY }}
      MAX_RETRIES: 3
      RETRY_DELAY: 5
      CODEFLASH_END_TO_END: 1
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.ref || '' }}
          repository: ${{ github.event.pull_request.head.repo.full_name || '' }}
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Validate PR
        if: github.event_name == 'pull_request'
        uses: ./.github/actions/validate-pr
        with:
          base_sha: ${{ github.event.pull_request.base.sha }}
          head_sha: ${{ github.event.pull_request.head.sha }}
          author: ${{ github.event.pull_request.user.login }}
          pr_state: ${{ github.event.pull_request.state }}

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          python-version: 3.11.6
          enable-cache: true

      - name: Install dependencies
        run: uv sync

      - name: Install extra dependencies
        if: matrix.extra_deps
        run: uv add ${{ matrix.extra_deps }}

      - name: Set test configuration
        if: matrix.expected_improvement
        run: |
          echo "COLUMNS=110" >> "$GITHUB_ENV"
          echo "EXPECTED_IMPROVEMENT_PCT=${{ matrix.expected_improvement }}" >> "$GITHUB_ENV"

      - name: Remove .git
        if: matrix.remove_git
        run: |
          if [ -d ".git" ]; then
            echo ".git directory exists!"
            sudo rm -rf .git
            if [ -d ".git" ]; then
              echo ".git directory still exists after removal attempt!"
              exit 1
            else
              echo ".git directory successfully removed."
            fi
          else
            echo ".git directory does not exist. Nothing to remove."
            exit 1
          fi

      - name: Run E2E test
        run: uv run python tests/scripts/${{ matrix.script }}

  # --- JS E2Es (3 tests, need Node.js + packages/) ---
  e2e-js:
    needs: determine-changes
    if: >-
      needs.determine-changes.outputs.e2e_js == 'true'
      && github.event_name != 'push'
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: js-cjs-function
            script: end_to_end_test_js_cjs_function.py
            js_project_dir: code_to_optimize/js/code_to_optimize_js
            expected_improvement: 50
          - name: js-esm-async
            script: end_to_end_test_js_esm_async.py
            js_project_dir: code_to_optimize/js/code_to_optimize_js_esm
            expected_improvement: 10
            allow_failure: true
          - name: js-ts-class
            script: end_to_end_test_js_ts_class.py
            js_project_dir: code_to_optimize/js/code_to_optimize_ts
            expected_improvement: 30
    continue-on-error: ${{ matrix.allow_failure || false }}
    environment: ${{ ((github.event_name == 'workflow_dispatch' && github.actor != 'misrasaurabh1' && github.actor != 'KRRT7') || (contains(toJSON(github.event.pull_request.files.*.filename), '.github/workflows/') && github.event.pull_request.user.login != 'misrasaurabh1' && github.event.pull_request.user.login != 'KRRT7')) && 'external-trusted-contributors' || '' }}
    runs-on: ubuntu-latest
    env:
      CODEFLASH_AIS_SERVER: prod
      POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
      CODEFLASH_API_KEY: ${{ secrets.CODEFLASH_API_KEY }}
      COLUMNS: 110
      MAX_RETRIES: 3
      RETRY_DELAY: 5
      EXPECTED_IMPROVEMENT_PCT: ${{ matrix.expected_improvement }}
      CODEFLASH_END_TO_END: 1
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.ref || '' }}
          repository: ${{ github.event.pull_request.head.repo.full_name || '' }}
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Validate PR
        if: github.event_name == 'pull_request'
        uses: ./.github/actions/validate-pr
        with:
          base_sha: ${{ github.event.pull_request.base.sha }}
          head_sha: ${{ github.event.pull_request.head.sha }}
          author: ${{ github.event.pull_request.user.login }}
          pr_state: ${{ github.event.pull_request.state }}

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: |
            packages/codeflash/package-lock.json
            code_to_optimize/js/*/package-lock.json

      - name: Install codeflash npm package dependencies
        run: |
          cd packages/codeflash
          npm install

      - name: Install JS test project dependencies
        run: |
          cd ${{ matrix.js_project_dir }}
          npm install

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          python-version: 3.11.6
          enable-cache: true

      - name: Install dependencies
        run: uv sync

      - name: Run E2E test
        run: uv run python tests/scripts/${{ matrix.script }}

  # --- Java E2Es (3 tests, need JDK + Maven) ---
  e2e-java:
    needs: determine-changes
    if: >-
      needs.determine-changes.outputs.e2e_java == 'true'
      && github.event_name != 'push'
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: java-fibonacci-nogit
            script: end_to_end_test_java_fibonacci.py
            expected_improvement: 70
            remove_git: true
          - name: java-tracer
            script: end_to_end_test_java_tracer.py
            expected_improvement: 10
          - name: java-void-optimization-nogit
            script: end_to_end_test_java_void_optimization.py
            expected_improvement: 70
            remove_git: true
    environment: ${{ ((github.event_name == 'workflow_dispatch' && github.actor != 'misrasaurabh1' && github.actor != 'KRRT7') || (contains(toJSON(github.event.pull_request.files.*.filename), '.github/workflows/') && github.event.pull_request.user.login != 'misrasaurabh1' && github.event.pull_request.user.login != 'KRRT7')) && 'external-trusted-contributors' || '' }}
    runs-on: ubuntu-latest
    env:
      CODEFLASH_AIS_SERVER: prod
      POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
      CODEFLASH_API_KEY: ${{ secrets.CODEFLASH_API_KEY }}
      COLUMNS: 110
      MAX_RETRIES: 3
      RETRY_DELAY: 5
      EXPECTED_IMPROVEMENT_PCT: ${{ matrix.expected_improvement }}
      CODEFLASH_END_TO_END: 1
      CODEFLASH_LOOPING_TIME: 5
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.ref || '' }}
          repository: ${{ github.event.pull_request.head.repo.full_name || '' }}
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Validate PR
        if: github.event_name == 'pull_request'
        uses: ./.github/actions/validate-pr
        with:
          base_sha: ${{ github.event.pull_request.base.sha }}
          head_sha: ${{ github.event.pull_request.head.sha }}
          author: ${{ github.event.pull_request.user.login }}
          pr_state: ${{ github.event.pull_request.state }}

      - name: Set up JDK 11
        uses: actions/setup-java@v5
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven

      - name: Install uv
        uses: astral-sh/setup-uv@v8.0.0
        with:
          python-version: 3.11.6
          enable-cache: true

      - name: Install dependencies
        run: uv sync

      - name: Cache codeflash-runtime JAR
        id: runtime-jar-cache
        uses: actions/cache@v4
        with:
          path: ~/.m2/repository/io/codeflash
          key: codeflash-runtime-${{ hashFiles('codeflash-java-runtime/pom.xml', 'codeflash-java-runtime/src/**') }}

      - name: Build and install codeflash-runtime JAR
        if: steps.runtime-jar-cache.outputs.cache-hit != 'true'
        run: |
          cd codeflash-java-runtime
          mvn install -q -DskipTests

      - name: Remove .git
        if: matrix.remove_git
        run: |
          if [ -d ".git" ]; then
            sudo rm -rf .git
            echo ".git directory removed."
          else
            echo ".git directory does not exist."
            exit 1
          fi

      - name: Run E2E test
        run: uv run python tests/scripts/${{ matrix.script }}

  # ---------------------------------------------------------------------------
  # Gate job — the ONLY required check in the GitHub ruleset.
  # Accepts "success" and "skipped" (job skipped by change detection).
  # Rejects "failure" and "cancelled".
  # ---------------------------------------------------------------------------
  required-checks-passed:
    name: required checks passed
    if: always()
    needs:
      - check-linked-issue
      - unit-tests
      - coverage
      - type-check
      - prek
      - e2e-python
      - e2e-js
      - e2e-java
    runs-on: ubuntu-latest
    steps:
      - name: Verify all required jobs passed
        run: |
          failing=$(echo "$NEEDS_JSON" | jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | "\(.key): \(.value.result)"')
          if [ -n "$failing" ]; then
            echo "Required jobs failed or were cancelled:"
            echo "$failing"
            exit 1
          fi
        env:
          NEEDS_JSON: ${{ toJSON(needs) }}
