# syntax=docker/dockerfile:1.7
#
# Föräldraledigplannerare — backend (FastAPI + uvicorn, Python 3.14, uv).
#
# Build context: backend/
#
#   podman build -t foraldraledig-backend backend/                            # slim default
#   podman build --target scraper -t foraldraledig-backend:scraper backend/    # + Chromium
#
# ---------------------------------------------------------------------------
# WHY TWO RUNTIMES
#
# `playwright` sits in [project].dependencies (not an extra), so the Python
# package is always installed — but the *browser* it drives is a separate
# ~170 MB download plus its system libraries, and Playwright publishes no
# musl/Alpine build at all. Shipping Chromium in the default image would
# several-fold inflate it for a feature most deployments never turn on.
#
# So:
#   * target `runtime` (default, and deliberately the LAST stage so a plain
#     `podman build` lands here) — Alpine, NO Chromium. Runs with
#     APP_SCRAPER_ENABLED=false baked in; the FK-scraper endpoints are the
#     only thing unavailable.
#   * target `scraper` (opt-in) — Debian slim + Chromium + its libs, built
#     from its own Debian dependency stage because musl wheels cannot be
#     reused on glibc.
# ---------------------------------------------------------------------------


# ===========================================================================
# Stage 1 — dependency build (glibc / Debian), only for the scraper variant
# ===========================================================================
FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS deps-glibc

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    UV_PYTHON_DOWNLOADS=never \
    # Build the venv at its FINAL path. A uv/virtualenv venv is not
    # relocatable: console-script shebangs are absolute, so a venv built at
    # /build/.venv and copied to /app/.venv yields "uvicorn: not found"
    # (exit 127 — a missing interpreter, not a missing script).
    UV_PROJECT_ENVIRONMENT=/app/.venv

WORKDIR /build
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev --no-install-project


# ===========================================================================
# Stage 2 — opt-in scraper runtime: Debian slim + Chromium
# ===========================================================================
FROM python:3.14-slim-trixie AS scraper

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/app/.venv/bin:$PATH" \
    APP_SQLITE_PATH=/data/foraldraledig.db \
    APP_SCRAPER_ENABLED=true \
    APP_SCRAPER_HEADLESS=true \
    # Browsers outside $HOME so they survive a read-only /app and any UID.
    PLAYWRIGHT_BROWSERS_PATH=/opt/playwright

WORKDIR /app

# --chown here, not a `chown -R /app` below: see the note in the default
# runtime stage — a recursive chown duplicates the whole venv into a new layer.
COPY --from=deps-glibc --chown=10001:10001 /app/.venv /app/.venv
COPY --chown=10001:10001 alembic.ini ./
COPY --chown=10001:10001 migrations ./migrations
COPY --chown=10001:10001 app ./app

# `playwright install --with-deps` pulls Chromium *and* the system libraries
# it needs. --only-shell installs the headless shell only (no headed browser,
# no ffmpeg), which is all the FK scraper uses. This single line is why this
# variant is big, and the whole reason it is opt-in.
RUN python -m compileall -q /app/app /app/migrations \
    && playwright install --with-deps --only-shell chromium \
    && rm -rf /var/lib/apt/lists/* /root/.cache \
    && groupadd -g 10001 app \
    && useradd -u 10001 -g app -M -d /app app \
    && mkdir -p /data \
    && chown app:app /data \
    && chmod -R a+rX /opt/playwright

VOLUME ["/data"]
USER 10001:10001
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
    CMD ["python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).status == 200 else 1)"]

# Chromium needs writable scratch space; with --read-only add
#   --tmpfs /tmp --tmpfs /dev/shm:size=256m
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]


# ===========================================================================
# Stage 3 — dependency build (musl / Alpine), for the default slim runtime
# ===========================================================================
FROM ghcr.io/astral-sh/uv:python3.14-alpine AS deps-musl

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    UV_PYTHON_DOWNLOADS=never \
    UV_PROJECT_ENVIRONMENT=/app/.venv

# No compiler needed: every dependency in uv.lock except playwright publishes
# a cp314 musllinux aarch64/x86_64 wheel today (argon2-cffi, greenlet, uvloop,
# httptools, pydantic-core all do). If a future dependency bump breaks that,
# add `RUN apk add --no-cache build-base libffi-dev` here — it is builder-only
# and never reaches the final image.

WORKDIR /build

# Manifests first: this layer stays cached until the dependency set actually
# changes, so source edits never re-resolve or re-compile the tree.
COPY pyproject.toml uv.lock ./

# --no-install-project: the app is plain source we COPY in below, so the venv
# holds third-party dependencies only. --no-dev drops pytest/ruff/httpx.
#
# --no-install-package playwright: playwright ships NO musllinux wheel at all
# (manylinux/macOS/win only), so on Alpine it is not merely undesirable, it is
# uninstallable. Dropping it is safe *only* because the import is lazy —
# app/scrapers/fk_scraper.py does `from playwright.async_api import
# async_playwright` inside the function that drives the browser, not at module
# scope, so `app.main` imports cleanly without it. The scraper is disabled via
# APP_SCRAPER_ENABLED=false in the runtime stage; use the `scraper` target if
# you need it. Keep that import lazy, or this stage breaks at boot.
RUN uv sync --locked --no-dev --no-install-project --no-install-package playwright


# ===========================================================================
# Stage 4 — DEFAULT runtime: Alpine, no build toolchain, no Chromium
# ===========================================================================
FROM python:3.14-alpine AS runtime

# Pinned by tag, not digest, on purpose: a tag pin keeps rebuilds picking up
# Alpine/CPython security patches, which matters more here than bit-for-bit
# reproducibility — and the *Python* dependency set is already byte-pinned by
# uv.lock (`uv sync --locked`). Pin the digest in the deployment pipeline if
# full supply-chain immutability outranks patching.

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/app/.venv/bin:$PATH" \
    # SQLite lives on the mounted volume, never in an image layer.
    APP_SQLITE_PATH=/data/foraldraledig.db \
    # No Chromium in this image — see "WHY TWO RUNTIMES" above.
    APP_SCRAPER_ENABLED=false

WORKDIR /app

# --chown on the COPY itself: a later `chown -R /app` would rewrite every
# file's metadata and so duplicate the entire 89 MB venv into a second layer
# (it cost ~86 MB before this was fixed).
COPY --from=deps-musl --chown=10001:10001 /app/.venv /app/.venv

# Source last — it changes most often, so it is the only layer that rebuilds.
# alembic.ini and migrations/ must sit in WORKDIR: app/db/connection.py loads
# `Config("alembic.ini")` by a path relative to the process CWD.
COPY --chown=10001:10001 alembic.ini ./
COPY --chown=10001:10001 migrations ./migrations
COPY --chown=10001:10001 app ./app

# Pre-compile so a read-only root filesystem never needs to write .pyc files.
RUN python -m compileall -q /app/app /app/migrations \
    && addgroup -g 10001 -S app \
    && adduser -u 10001 -S -G app -h /app app \
    && mkdir -p /data \
    && chown app:app /data

# SQLite database + its WAL/SHM sidecars. Mount a named volume or host path
# here; without one the data dies with the container.
#
# Read-only root filesystem (verified working) — run with:
#   --read-only --tmpfs /tmp -v <volume>:/data
# Nothing outside /data is written at runtime: bytecode is pre-compiled above
# and PYTHONDONTWRITEBYTECODE is set.
VOLUME ["/data"]

USER 10001:10001
EXPOSE 8000

# /health is defined in app/main.py. urllib keeps this dependency-free — no
# curl or wget needs to exist in the image.
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
    CMD ["python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).status == 200 else 1)"]

# Exec form => uvicorn is PID 1 and receives SIGTERM/SIGINT directly, so it
# runs its own graceful shutdown (lifespan closes the DB connection) rather
# than being SIGKILLed after the stop timeout. No init shim needed: a single
# uvicorn process spawns no children and has nothing to reap.
#
# MIGRATIONS: the app lifespan already calls `alembic upgrade head` itself on
# SQLite (app/main.py -> app/db/connection.py:run_migrations), so the schema is
# applied on boot. That is forward-only `upgrade`, never `downgrade`, so no
# destructive DDL runs implicitly. On Postgres (APP_DATABASE_TYPE=postgres) the
# lifespan does NOT migrate — run it as a one-shot before rollout:
#   podman run --rm -e APP_DATABASE_TYPE=postgres ... <image> alembic upgrade head
# To take the boot migration off the hot path on SQLite too, run that same
# one-shot and treat the auto-migrate as a no-op safety net.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
