# Stage 1: Build frontend with Node
FROM node:22-slim AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npx vite build --outDir /app/static_dist

# Stage 1b: AGPL §13 source archiver (v8.13.0)
#
# Bakes the Corresponding Source of THIS build into the image, served at
# /source.tar.gz. Railway's build context is a clean git clone of the
# deployed commit (no .git dir is available, so `git archive` can't run
# here) — tarring the context reproduces `git archive <deployed-sha>`
# exactly, because .dockerignore mirrors the .gitattributes export-ignore
# set (backups/, PRIVATISATION_REPORT.md) and everything else in the
# context IS the tracked tree. Generated per-build, so the archive can
# never go stale relative to the running version — including env-var
# redeploys that rebuild without a new tag.
#
# The tar excludes below are belt-and-braces for LOCAL builds, where the
# context may contain untracked files (.env, venvs, runtime data) that
# .dockerignore should already drop.
FROM python:3.11-slim AS source-archiver
ARG RAILWAY_GIT_COMMIT_SHA=unknown
WORKDIR /src
COPY . .
RUN mkdir -p /out && \
    tar --sort=name \
        --exclude='./.git' \
        --exclude='./.env' \
        --exclude='./.env.local' \
        --exclude='./.env.development' \
        --exclude='./.env.test' \
        --exclude='./.env.production' \
        --exclude='./backups' \
        --exclude='./PRIVATISATION_REPORT.md' \
        --exclude='./node_modules' \
        --exclude='./frontend/node_modules' \
        --exclude='./frontend/dist' \
        --exclude='./backend/static_dist' \
        --exclude='./backend/uploads' \
        --exclude='./backend/data' \
        --exclude='./backend/logs' \
        --exclude='./backend/venv' \
        --exclude='./.venv' \
        --exclude='./venv' \
        --exclude='__pycache__' \
        --exclude='*.pyc' \
        --exclude='*.log' \
        --exclude='*.db' \
        --exclude='.DS_Store' \
        -czf /out/mirofish-source.tar.gz . && \
    printf '{"commit":"%s","built_at":"%s","size_bytes":%s}\n' \
        "$RAILWAY_GIT_COMMIT_SHA" \
        "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        "$(stat -c%s /out/mirofish-source.tar.gz)" > /out/meta.json && \
    cat /out/meta.json

# Stage 2: Python backend + built frontend
FROM python:3.11-slim

WORKDIR /app

# v7.7.1 Resource Control: Cap BLAS/OpenMP thread pools BEFORE any Python
# import happens. NumPy/SciPy/sklearn read these envs at C-extension load
# time, so they MUST be set before `import numpy` runs in any worker.
# Without these caps, each worker spins up N_CPU threads per BLAS call,
# leading to thread storms under concurrent simulations and the
# OpenBLAS `blas_thread_init` exit-2 we saw in production logs.
# v8.24 — carry the build's commit into the runtime so /health can answer
# "which commit is live?". APP_VERSION is a hand-maintained constant and had
# not moved in 72 commits, so during an incident the only version signal was
# stale by an entire programme of work.
ARG RAILWAY_GIT_COMMIT_SHA=unknown
ENV MIROFISH_COMMIT_SHA=$RAILWAY_GIT_COMMIT_SHA

ENV OPENBLAS_NUM_THREADS=2 \
    OMP_NUM_THREADS=2 \
    MKL_NUM_THREADS=2 \
    NUMEXPR_NUM_THREADS=2 \
    VECLIB_MAXIMUM_THREADS=2 \
    BLIS_NUM_THREADS=2 \
    TOKENIZERS_PARALLELISM=false

# Install Python dependencies using pip directly (no uv needed)
COPY backend/requirements.txt ./backend/requirements.txt
RUN pip install --no-cache-dir -r backend/requirements.txt gunicorn PyJWT stripe redis

# Copy backend source
COPY backend/ ./backend/

# Copy built frontend from stage 1
COPY --from=frontend-builder /app/static_dist ./backend/static_dist

# v8.13.0 — AGPL §13 Corresponding Source archive (served at /source.tar.gz)
COPY --from=source-archiver /out /app/source_offer

# v8.13.1 — build-time compliance assertion: a missing or truncated source
# archive must FAIL the build, so Railway can never deploy an image that
# silently breaks the §13 offer. 1 MB floor: the real archive is ~7.5 MB;
# anything under 1 MB means the archiver stage produced garbage.
RUN size=$(stat -c%s /app/source_offer/mirofish-source.tar.gz 2>/dev/null || echo 0); \
    if [ "$size" -le 1048576 ]; then \
        echo "FATAL: AGPL s13 source archive missing or truncated (size=${size} bytes) - refusing to build a non-compliant image"; \
        exit 1; \
    fi; \
    echo "AGPL s13 source archive OK (${size} bytes)"

# Create persistent data directories with full permissions
RUN mkdir -p /app/backend/data /app/backend/uploads && chmod -R 777 /app/backend/data /app/backend/uploads

EXPOSE 5001

WORKDIR /app/backend
# SECURITY: gunicorn's default access-log format is "%(r)s" — the full request
# target, query string included — and "%(f)s", the referrer. Both routinely
# carry single-factor secrets in this application: password-reset tokens
# (api/password_reset.py), email-verification tokens (api/auth_routes.py),
# service-order access tokens (api/service_orders.py) and unsubscribe tokens
# (utils/email.py). Anything logged there is readable by everyone with
# deployment-log access, which turns a log reader into an account-takeover
# path. The referrer is dropped too: a browser sends the FULL previous URL,
# so a user who follows any link from a tokenised page leaks that token into
# the next request's Referer.
#
# This format keeps everything operationally useful — client address,
# timestamp, method, path, status, response size, user agent — and records
# neither query strings nor referrers. The application logger already logs
# request.path only; this closes the WSGI layer beneath it.
CMD ["gunicorn", "--bind", "0.0.0.0:5001", "--workers", "2", "--threads", "2", "--timeout", "300", "--preload", "--access-logfile", "-", "--access-logformat", "%(h)s %(l)s %(u)s %(t)s \"%(m)s %(U)s\" %(s)s %(b)s \"%(a)s\"", "run:app"]
