#!/bin/bash

# Check that every declaration of the server's Python version agrees with
# roundup-server/.python-version (issue #612).
#
# Before this check, CI installed 3.12 while the deployed venv kept the 3.11
# that first created it, so nothing in the pipeline ran the version production
# ran: a 3.12-only construct passed every workflow and failed on the server.
# deploy.yml now rebuilds the venv from .python-version, which makes that file
# the one place the version is chosen — and makes every other copy of it a way
# for the gap to reopen quietly. Each copy below is a real consumer that cannot
# read .python-version for itself:
#
#   - ruff's target-version authorises rewrites (UP017's `datetime.UTC`), so a
#     value ahead of the deployed interpreter produces code production cannot
#     import.
#   - sonar.python.version selects the grammar SonarCloud parses with.
#   - The README tells a contributor what to install.
#
# The workflows are checked the other way round: they must not pin a version at
# all, only read the file. That is deliberate and it has a cost: a workflow that
# legitimately wants a literal — a matrix over two versions, say, which is
# option 2 in issue #612 — cannot be added without relaxing the rule here, and
# neither can an unrelated workflow that happens to need Python for something
# other than the server. It is written this way because a single deployed
# version is the decision the project made; changing that decision means
# changing this check with it, which is the point rather than an obstacle.

set -e

echo "Checking Python version declarations..."

VERSION_FILE="roundup-server/.python-version"
ERRORS=0

if [ ! -f "$VERSION_FILE" ]; then
  echo "Error: $VERSION_FILE is missing. It is the source of truth for the server's Python version."
  exit 1
fi

VERSION=$(tr -d '[:space:]' < "$VERSION_FILE")

if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
  echo "Error: $VERSION_FILE must contain a major.minor version such as '3.12', found '$VERSION'."
  echo "       A patch-level version ('3.12.4', pyenv's usual spelling) is not accepted: the deploy"
  echo "       resolves the host interpreter as python<version>, and there is no 'python3.12.4'."
  exit 1
fi

echo "Source of truth: Python $VERSION ($VERSION_FILE)"

# ruff: target-version is spelled without the dot, e.g. 3.12 -> py312.
RUFF_EXPECTED="py${VERSION//./}"
RUFF_ACTUAL=$(grep -E '^target-version *= *"' roundup-server/ruff.toml | sed -E 's/.*"(.*)".*/\1/' || true)
if [ "$RUFF_ACTUAL" != "$RUFF_EXPECTED" ]; then
  echo "Error: roundup-server/ruff.toml has target-version = \"$RUFF_ACTUAL\", expected \"$RUFF_EXPECTED\" for Python $VERSION."
  ERRORS=$((ERRORS + 1))
fi

# SonarCloud analyses the server from two properties files depending on which
# workflow runs it; both name the grammar version explicitly.
# Both files must exist and must state the version: dropping the line entirely
# would let SonarCloud fall back to its own default grammar, which is the same
# silent drift this check exists to catch.
#
# Only roundup-server/sonar-project.properties is read by a scan today —
# sonarcloud.yml's Analyze (Python) job passes projectBaseDir: roundup-server,
# so that is the file the scanner picks up. The root sonar-project-server.
# properties is referenced by nothing; it is checked anyway so that a stale
# value cannot sit there waiting to mislead whoever wires it up or reads it.
for props in sonar-project-server.properties roundup-server/sonar-project.properties; do
  if [ ! -f "$props" ]; then
    echo "Error: $props is missing. It declares sonar.python.version for a Python Sonar project;"
    echo "       if that project is genuinely gone, remove it from this check as well."
    ERRORS=$((ERRORS + 1))
    continue
  fi
  SONAR_ACTUAL=$(grep -E '^sonar\.python\.version *=' "$props" | sed -E 's/.*= *//' | tr -d '[:space:]' || true)
  if [ -z "$SONAR_ACTUAL" ]; then
    echo "Error: $props does not set sonar.python.version. Expected $VERSION."
    ERRORS=$((ERRORS + 1))
  elif [ "$SONAR_ACTUAL" != "$VERSION" ]; then
    echo "Error: $props sets sonar.python.version=$SONAR_ACTUAL, expected $VERSION."
    ERRORS=$((ERRORS + 1))
  fi
done

# The README's install instructions for roundup-server.
# Matched with a trailing boundary so that "Python 3.12+" — a range, which is
# what this project deliberately no longer offers — and "3.121" do not satisfy
# it, while ordinary prose after the number, a comma, or a full stop all do.
README_EXPECTED="Requirements: Python $VERSION"
README_MATCHES=$(grep -cE "Requirements: Python ${VERSION//./\\.}(\$|[^0-9.+]|\.(\$|[^0-9]))" README.md || true)
README_CLAIMS=$(grep -cE "Requirements: Python" README.md || true)
if [ "$README_MATCHES" -eq 0 ]; then
  echo "Error: README.md does not state \"$README_EXPECTED\" for roundup-server."
  echo "       A range such as \"$README_EXPECTED+\" is not accepted: CI, ruff and the server all run"
  echo "       this exact version."
  ERRORS=$((ERRORS + 1))
elif [ "$README_MATCHES" -ne "$README_CLAIMS" ]; then
  # A correct line elsewhere in the file would otherwise let a stale one stand.
  echo "Error: README.md has $README_CLAIMS \"Requirements: Python\" lines but only $README_MATCHES state $VERSION:"
  grep -nE "Requirements: Python" README.md | grep -vE "Requirements: Python ${VERSION//./\\.}(\$|[^0-9.+]|\.(\$|[^0-9]))"
  ERRORS=$((ERRORS + 1))
fi

# Workflows must read the file rather than restate the version, so that moving
# the version is a one-file change.
PINNED=$(grep -rnE '^[[:space:]]*python-version:' .github/workflows/ || true)
if [ -n "$PINNED" ]; then
  echo "Error: workflows must use 'python-version-file: $VERSION_FILE' rather than pinning a version:"
  echo "$PINNED"
  ERRORS=$((ERRORS + 1))
fi

# Every workflow that runs Python against roundup-server must declare where the
# version comes from. Without this the checker was satisfied by the *absence* of
# a declaration: deleting a setup-python block entirely left the job running on
# whatever python3 the runner ships, which is issue #612's original failure with
# this check reporting success.
#
# The assertion is per file, not per job: a second Python job added to one of
# these workflows without its own setup-python step would run on the runner's
# stock python3 and still satisfy this. Checking per job means parsing the YAML,
# which is more machinery than this script is worth; the ban on literal pins
# below is what makes such a job visible in review.
for wf in test-server lint sonarcloud convention-checks; do
  path=".github/workflows/$wf.yml"
  if [ ! -f "$path" ]; then
    echo "Error: $path is missing. If that workflow is genuinely gone, remove it from this check as well."
    ERRORS=$((ERRORS + 1))
  elif ! grep -qE "python-version-file:[[:space:]]*$VERSION_FILE([[:space:]]|\$)" "$path"; then
    echo "Error: $path does not declare 'python-version-file: $VERSION_FILE'."
    echo "       It runs Python against roundup-server, so it must install the version the server runs."
    ERRORS=$((ERRORS + 1))
  fi
done

while IFS= read -r line; do
  path=$(echo "$line" | sed -E 's/.*python-version-file:[[:space:]]*//' | tr -d '[:space:]')
  if [ "$path" != "$VERSION_FILE" ]; then
    echo "Error: $line"
    echo "       expected python-version-file: $VERSION_FILE"
    ERRORS=$((ERRORS + 1))
  fi
done < <(grep -rnE 'python-version-file:' .github/workflows/ || true)

# deploy.yml is what makes the deployed venv follow the file; without that read
# the server keeps whatever interpreter created its venv, which is the bug.
# Matched on the read itself, not a mention of the path: a comment naming the
# file would otherwise satisfy this while the rebuild logic was gutted.
if ! grep -qE 'git show.*:roundup-server/\.python-version' .github/workflows/deploy.yml; then
  echo "Error: .github/workflows/deploy.yml no longer reads $VERSION_FILE, so the deployed venv can drift from CI."
  ERRORS=$((ERRORS + 1))
fi

if [ "$ERRORS" -gt 0 ]; then
  echo ""
  echo "Found $ERRORS Python version problem(s). Every declaration above must match $VERSION_FILE."
  echo "See the Python version section of docs/ci-cd-setup.md."
  exit 1
fi

echo "All Python version declarations agree on $VERSION."
exit 0
