# syntax=docker/dockerfile:1
# Multi-stage build for the tavolo stack (kaya backend + Sycamore/WASM
# frontend). The Docker build context is the REPOSITORY ROOT (see
# docker-compose.yml) so this single image assembles both parts:
#
#   web-builder  rust + trunk -> compiles web/ into web/dist
#   builder      alpine python -> python venv with the backend + deps
#   runtime      alpine python + the venv + the compiled frontend
#
# apk and uv both use BuildKit cache mounts (type=cache): the package
# caches persist in the builder's cache across builds instead of being
# re-downloaded, and never land in the image layers.

# --- Web builder -------------------------------------------------------------
FROM rust:1-slim AS web-builder

ARG TRUNK_VERSION=0.21.14
# The build environment blocks plain HTTP: force the apt mirrors to HTTPS.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
 && apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
      | tar -xz -C /usr/local/bin \
 && rustup target add wasm32-unknown-unknown

WORKDIR /web

# Card images are committed in the repository (CC0 woodcut napoletane deck);
# web/fetch-cards.sh can regenerate them.
COPY web/Cargo.toml web/Cargo.lock web/index.html web/style.css web/theme.css web/Trunk.toml ./
# PWA manifest and service worker: trunk's copy-file entries need them at
# the build root, and the runtime serves both from the site root (static.py).
COPY web/manifest.webmanifest web/sw.js ./
COPY web/assets ./assets
# Vendored Bootstrap (MIT) loaded by index.html; kept out of the CDN so the
# image builds and runs without network access.
COPY web/vendor ./vendor
COPY web/src ./src

# --public-url makes trunk emit asset URLs under /static, the prefix Granian
# serves in the runtime image (see GRANIAN_STATIC_PATH_* below). Dev builds
# (trunk serve) keep the default "/" public URL.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/web/target \
    trunk build --release --public-url /static/

# --- Python builder ----------------------------------------------------------
FROM alpine:3.24 AS builder

# The official uv image ships a statically linked musl binary, so it works
# on Alpine. Pin the version so image builds do not change implicitly.
COPY --from=ghcr.io/astral-sh/uv:0.13.1 /uv /uvx /bin/

RUN --mount=type=cache,target=/var/cache/apk \
    apk add python3 build-base python3-dev

# Use Alpine's Python instead of downloading a managed interpreter, install
# the venv at the same path the runtime stage uses, and copy rather than
# hardlink from the cache mount (cache and target are different filesystems).
ENV UV_PYTHON_DOWNLOADS=0 \
    UV_PROJECT_ENVIRONMENT=/opt/venv \
    UV_LINK_MODE=copy

WORKDIR /build

# uv needs the workspace root plus the member pyprojects to know which
# package to sync. Copying only the manifests lets the third-party
# dependency layer stay cached while application sources change.
# server/pyproject.toml also carries [tool.aerich], read by the db-migrate
# compose service with working_dir=/app, next to ./migrations.
COPY server/uv.lock server/pyproject.toml ./
COPY server/packages/tavolo-platform/pyproject.toml packages/tavolo-platform/
COPY server/packages/tavolo-scopone/pyproject.toml packages/tavolo-scopone/
COPY server/packages/tavolo-scopa/pyproject.toml packages/tavolo-scopa/
COPY server/packages/tavolo-briscola/pyproject.toml packages/tavolo-briscola/
COPY server/packages/tavolo-app/pyproject.toml packages/tavolo-app/
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-install-workspace --package tavolo-app --extra otel --no-dev

# The five sibling distributions are workspace members (PEP 420 namespace
# packages sharing the `tavolo` namespace). --no-editable installs them as
# wheels, so the runtime image does not need their source tree.
COPY server/packages/ ./packages/
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-editable --package tavolo-app --extra otel --no-dev

# aerich migration files are a release artifact: the db-migrate compose
# service runs `aerich upgrade` from this image before the app starts.
COPY server/migrations/ ./migrations/

# --- Runtime ---------------------------------------------------------------
FROM alpine:3.24

RUN --mount=type=cache,target=/var/cache/apk \
    apk add python3 ca-certificates tzdata \
 && addgroup -S app && adduser -S -G app app

COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /build/migrations /app/migrations
# aerich reads [tool.aerich] from pyproject.toml (its default config file);
# the db-migrate compose service runs `aerich upgrade` with working_dir=/app.
COPY --from=builder /build/pyproject.toml /app/pyproject.toml
# The compiled single-page application. Granian serves the assets directly
# in Rust — hashed js/wasm/css under /static/* and the card images under
# /assets/* (see the GRANIAN_STATIC_PATH_* env vars below; click splits
# multi-value env vars on whitespace for routes and ':' for paths). The
# Python app only serves the SPA shell (index.html) at / and for
# client-side routes (STATIC_DIR).
COPY --from=web-builder /web/dist /app/web/dist

ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    GRANIAN_HOST=0.0.0.0 \
    GRANIAN_PORT=8080 \
    GRANIAN_INTERFACE=rsgi \
    GRANIAN_STATIC_PATH_ROUTE="/static /assets" \
    GRANIAN_STATIC_PATH_MOUNT="/app/web/dist:/app/web/dist/assets" \
    GRANIAN_STATIC_PATH_DIR_TO_FILE=index.html \
    STATIC_DIR=/app/web/dist

USER app

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
  CMD wget -q -O- http://127.0.0.1:8080/api/health || exit 1

CMD ["granian", "tavolo.app:app"]
