# ShappeTrack Backend — production image
# Stack proven from repo: Python 3.11 + FastAPI, run by gunicorn/uvicorn worker.
# (requirements.txt, main.py, docker-entrypoint.sh)

# ---------- Stage 1: build dependency wheels ----------
# Same base as runtime so wheels are ABI-compatible.
FROM python:3.11-slim AS builder

WORKDIR /app

# No compiler installed: every dependency in requirements.txt is pure-Python or
# ships a manylinux wheel (psycopg2-BINARY, cryptography, bcrypt). Verified — no
# source builds are required.
COPY requirements.txt .

# Build wheels for all pinned deps into /wheels for a clean copy into runtime.
# requirements.txt is fully version-pinned (no lockfile in repo) -> reproducible.
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt


# ---------- Stage 2: runtime ----------
FROM python:3.11-slim AS runtime

# PYTHONUNBUFFERED        — flush stdout/stderr immediately so logs reach
#                           CloudWatch in real time and aren't lost on crash.
# PYTHONDONTWRITEBYTECODE — no .pyc clutter in an immutable/ephemeral container.
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# Runtime OS deps:
#   git  — the app shells out to the git binary via subprocess
#          (core/git_server.py: "git init --bare", "git log", "git for-each-ref";
#           core/merge_service.py: "git rev-parse", "git merge-base").
#   tini — PID 1 init that forwards signals (SIGTERM from ECS) and REAPS the
#          orphaned git subprocesses the app spawns, preventing zombie buildup.
# postgresql-client and gcc are intentionally NOT installed — nothing in the
# repo invokes psql/pg_* and no dependency compiles from source.
RUN apt-get update \
    && apt-get install -y --no-install-recommends git tini \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Install the prebuilt wheels, then drop them. --no-cache-dir keeps pip's cache
# out of the final layer.
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
    && rm -rf /wheels

# Application source. .dockerignore excludes .env*, storage/, logs/, .git, caches
# and tests/ so secrets and runtime state never enter the image.
COPY . .

# Entrypoint is a real tracked script (docker-entrypoint.sh): it runs
# `alembic upgrade head` (gated by RUN_MIGRATIONS) then execs gunicorn.
RUN chmod +x /app/docker-entrypoint.sh

# Writable runtime directories, owned by the non-root user:
#   storage/ — bare git repos (config.py GIT_STORAGE_PATH=./storage/git) and
#              release uploads (api/v1/releases.py writes BLOB_STORAGE_PATH/releases/<id>)
#   logs/    — config.py ensure_storage_paths() creates dirname(LOG_FILE)=./logs
# (Application logs go to stdout, but the logs/ dir is still created on boot.)
RUN useradd --create-home --uid 10001 appuser \
    && mkdir -p /app/storage /app/logs \
    && chown -R appuser:appuser /app/storage /app/logs

USER appuser

# Port 8000 is the only bound port (config.py HOST=0.0.0.0 PORT=8000; entrypoint
# binds ${HOST}:${PORT}). No SSH listener exists in this process, so 2222 is NOT
# exposed (SSH_PORT in config.py is advertised clone-URL metadata only).
EXPOSE 8000

# /health is a real unauthenticated endpoint (main.py @app.get("/health")).
# Use the stdlib so the check needs no extra packages.
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
    CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health', timeout=5).status==200 else 1)" || exit 1

# tini as PID 1 (exec form): forwards SIGTERM to the entrypoint and reaps the
# orphaned git subprocesses the app spawns. The entrypoint then `exec`s gunicorn
# so signals reach it directly.
# Reads ENVIRONMENT / WEB_CONCURRENCY / GUNICORN_TIMEOUT / RUN_MIGRATIONS from env.
ENTRYPOINT ["/usr/bin/tini", "--", "/app/docker-entrypoint.sh"]
