# syntax=docker/dockerfile:1
ARG APP_VERSION=0.2.0-rc.3
FROM python:3.13-slim-bookworm@sha256:a1165e272e578941b84abc79e4ab38a0305cd12803a5c4247979ac7655f4d641 AS source-build
ARG APP_VERSION
WORKDIR /source
# Exact public snapshot: no Git checkout, data volume or developer environment.
COPY README.md AGENTS.md LICENSE THIRD_PARTY.md .gitignore .dockerignore .env.example Dockerfile compose.yaml Caddyfile requirements-dev.txt ./
COPY requirements.lock requirements-runtime.lock requirements-pure.lock requirements-target.txt ./
COPY app/ ./app/
COPY web/ ./web/
COPY scripts/ ./scripts/
COPY tests/ ./tests/
COPY docs/ ./docs/
RUN python - <<'PY'
import os
from pathlib import Path
from scripts.build_release import build_release, verify_archive, digest

artifacts = build_release(Path('/source'), Path('/release'), os.environ['APP_VERSION'])
archive = next(path for path in artifacts if path.name.endswith('.tar.gz'))
manifest = verify_archive(archive)
assert manifest['version'] == os.environ['APP_VERSION']
target = Path('/opt/lernanker-source')
target.mkdir(parents=True)
fixed = target / 'lernanker-source.tar.gz'
fixed.write_bytes(archive.read_bytes())
verify_archive(fixed)
(target / 'RELEASE_MANIFEST.json').write_bytes(next(path for path in artifacts if path.name.endswith('.manifest.json')).read_bytes())
(target / 'SHA256SUMS').write_text(digest(fixed.read_bytes()) + '  ' + fixed.name + '\n', encoding='ascii')
for path in target.iterdir():
    path.chmod(0o444)
target.chmod(0o555)
# ZIP and temporary build artifacts stay in this discarded builder stage only.
PY

FROM python:3.13-slim-bookworm@sha256:a1165e272e578941b84abc79e4ab38a0305cd12803a5c4247979ac7655f4d641 AS latex-tools
# Build-time Linux only: never install or probe a renderer on the Mac.
RUN apt-get update \
    && apt-get install -y --no-install-recommends gcc libc6-dev linux-libc-dev libseccomp-dev texlive-latex-base texlive-latex-recommended dvisvgm dvipng \
    && rm -rf /var/lib/apt/lists/*
COPY app/latex_sandbox.c /build/latex_sandbox.c
RUN cc -O2 -Wall -Wextra -Werror -D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wl,-z,relro,-z,now /build/latex_sandbox.c -lseccomp -o /build/latex-guard
# A deny-by-default mount tree, NOT /usr or the application filesystem.
# Copy only three fixed executables, guard, their glibc dependencies and public
# TeX/font resources. No shell, Python, app, data, proc, sys, home or credentials.
RUN python - <<'PY'
import os, pathlib, re, shutil, subprocess
root = pathlib.Path('/opt/latex-root')
for name in ('bin','lib','lib64','usr/lib','usr/share','var/lib','etc','work','dev'):
    (root/name).mkdir(parents=True, exist_ok=True)
for source, dest in (('/usr/bin/latex','bin/latex'),('/usr/bin/dvisvgm','bin/dvisvgm'),('/usr/bin/dvipng','bin/dvipng'),('/build/latex-guard','bin/latex-guard')):
    actual = pathlib.Path(source).resolve()
    shutil.copy2(actual, root/dest)
    libraries = subprocess.check_output(['/usr/bin/ldd', str(actual)], text=True)
    for library in re.findall(r'(?:=> )?(/[^\s()]+)', libraries):
        target = root/library.lstrip('/')
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(pathlib.Path(library).resolve(), target)
for directory in ('/usr/share/texlive','/usr/share/texmf','/usr/share/fonts','/var/lib/texmf','/etc/texmf','/etc/fonts'):
    if pathlib.Path(directory).exists():
        shutil.copytree(directory, root/directory.lstrip('/'), dirs_exist_ok=True, symlinks=False)
(root/'dev/null').touch()
# Never preserve writable or setuid files in the shared sandbox root.
for directory, dirs, files in os.walk(root):
    os.chmod(directory,0o555)
    for name in files:
        path=pathlib.Path(directory)/name
        path.chmod(0o555 if path.parent == root/'bin' or '/lib/' in str(path) or '/lib64/' in str(path) else 0o444)
PY

FROM python:3.13-slim-bookworm@sha256:a1165e272e578941b84abc79e4ab38a0305cd12803a5c4247979ac7655f4d641
ARG APP_VERSION
LABEL org.opencontainers.image.version="${APP_VERSION}" \
      org.opencontainers.image.licenses="AGPL-3.0-or-later"
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    DATA_DIR=/data/service \
    HOME=/data
WORKDIR /app

# Anki wheels include the official Rust core; no Qt/desktop installation.
RUN apt-get update \
    && apt-get install -y --no-install-recommends libstdc++6 libgcc-s1 ca-certificates bubblewrap libseccomp2 \
    && rm -rf /var/lib/apt/lists/* \
    && groupadd --gid 10001 lernanker \
    && useradd --uid 10001 --gid 10001 --no-create-home --home-dir /data lernanker \
    && install -d -o 10001 -g 10001 -m 0700 /data /data/service

COPY --from=latex-tools /opt/latex-root/ /opt/latex-root/
COPY --from=latex-tools --chmod=0555 /build/latex-guard /usr/local/libexec/lernanker-latex
COPY --from=source-build /source/requirements-runtime.lock ./requirements-runtime.lock
RUN python -m pip install --disable-pip-version-check -r requirements-runtime.lock \
    && python -c "from importlib.metadata import version; assert version('anki') == '26.9.3', 'Anki version must be 26.9.3'"

# Runtime files come from exactly the snapshot packaged by the stdlib builder.
COPY --from=source-build /source/app/ ./app/
COPY --from=source-build /source/web/ ./web/
COPY --from=source-build /source/scripts/ ./scripts/
COPY --from=source-build /source/LICENSE /source/THIRD_PARTY.md ./
COPY --from=source-build /opt/lernanker-source/ /opt/lernanker-source/
RUN chmod 0555 /opt/lernanker-source \
    && chmod 0444 /opt/lernanker-source/*
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
    CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=4).read()"]
CMD ["uvicorn", "app.account_gateway:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--no-proxy-headers", "--no-access-log", "--timeout-graceful-shutdown", "120"]
