# Multi-stage Dockerfile.
#
# How to build
# ────────────
# This Dockerfile needs SSH access to git.kadil.dev during the
# `npm install` step (the `goods-editor` dependency is a git+ssh:
# URL). We pass a base64-encoded SSH private key as a BuildKit
# secret named `ssh_key`, then decode it inside the build.
#
# Why base64-encoded rather than raw PEM
# ───────────────────────────────────────
# The primary deploy target is Dokploy, whose Build-time Secrets UI
# is a dotenv-parsed textarea (one KEY=VALUE per line). PEM keys are
# multi-line and break dotenv parsing. Base64 collapses the key to a
# single line of safe ASCII that any dotenv parser accepts. We use
# base64 everywhere (Dokploy and local builds) so the Dockerfile has
# exactly one input format — no branching logic for raw vs encoded.
#
# Register a deploy key first
# ─────────────────────────────
# Generate a read-only deploy key dedicated to this build and add
# its public half to Gitea (repo Settings → Deploy Keys):
#   ssh-keygen -t ed25519 -f ~/.ssh/goods-editor-deploy -N "" -C "deploy"
#   cat ~/.ssh/goods-editor-deploy.pub   # → paste into Gitea
#
# Then base64-encode the PRIVATE half for build-time use:
#   base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy
# The `tr -d '\n'` strips line wrapping so the output is a single
# physical line, which the Dokploy textarea (and dotenv parsers in
# general) require.
#
# Dokploy
# ───────
# Paste into the application's Environment → Build-time Secrets
# textarea as a single line:
#   ssh_key=<that base64 string>
# Then redeploy.
#
# Gitea SSH port note
# ───────────────────
# Our self-hosted Gitea is configured with SSH on port 222, not the
# default 22. Port 22 on git.kadil.dev hits the Docker host's
# OpenSSH (not Gitea), so deploy keys added through Gitea's UI
# wouldn't authorize anything if we connected on 22. The package.json
# git+ssh URL specifies `:222` in the host portion to route through
# Gitea's own SSH server, and the `ssh-keyscan -p 222` below scans
# the matching port so the known_hosts entry uses the same
# [host]:port form that the URL resolves to.
#
# Local build with the same approach
# ───────────────────────────────────
#   base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > /tmp/key.b64
#   docker build --secret id=ssh_key,src=/tmp/key.b64 -t apparel-designer .
#   rm /tmp/key.b64
#
# Or via env var:
#   SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \
#     docker build --secret id=ssh_key,env=SSH_KEY_B64 -t apparel-designer .
#
# Requires Docker 23+ (BuildKit is the default; older Docker needs
# `DOCKER_BUILDKIT=1` exported in the environment).
#
# Why two stages
# ──────────────
# `canvas` is a native-binding npm package. It needs Cairo/Pango/etc.
# at runtime, and python3+make+g++ at install-time to compile its
# native bindings (no prebuilt binary covers the alpine musl+napi+linux
# combination at this canvas version; prebuild-install falls back to
# node-gyp, which needs the compile toolchain).
#
# The naive single-stage approach would either:
#   (a) keep python3+make+g++ in the final image — inflates the image
#       by ~200MB of build tools nothing uses at runtime, or
#   (b) try to install --omit=dev in a runtime-only image — fails
#       because canvas tries to compile and has no python.
#
# Two-stage solves both: install + compile + prune dev deps in the
# builder (which has the toolchain), then COPY the resulting
# node_modules into a slim runtime that only carries the C runtime
# libs canvas needs at execution time.

# ─────────────────────────────────────────────────────────────────
# Stage 1: builder
# Installs all deps (including dev), compiles native bindings,
# builds the Vite bundle, then prunes dev deps so node_modules is
# production-ready before we copy it.
# ─────────────────────────────────────────────────────────────────
FROM node:20-alpine AS builder

# Build-time packages, grouped by why they're here:
#
#   Native-binding compile toolchain (for `canvas` + node-gyp):
#     python3, make, g++
#
#   Native-binding headers (canvas links against these at compile
#   time; the runtime stage carries the matching shared libs):
#     cairo-dev, pango-dev, libjpeg-turbo-dev, giflib-dev,
#     librsvg-dev, pixman-dev
#
#   Git over SSH (for the `goods-editor` git+ssh: dependency):
#     git, openssh-client
#
# git is npm's transport for git+ssh URLs; openssh-client provides
# the ssh binary that git invokes plus the ssh-keyscan utility we
# use below to populate known_hosts.
RUN apk add --no-cache \
    cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev pixman-dev \
    python3 make g++ \
    git openssh-client

WORKDIR /app

# Copy package manifests first so docker can cache the npm install
# layer when only application source changes.
COPY package*.json ./

# Install dependencies using an SSH key passed in as a build secret.
#
# `--mount=type=secret,id=ssh_key` is a BuildKit feature that exposes
# a host-provided secret at /run/secrets/ssh_key inside this single
# RUN step. The secret is NEVER copied into any image layer; once
# the RUN command finishes, the mount disappears and nothing about
# the key remains in the built image.
#
# Dokploy-specific note: Dokploy's Build-time Secrets UI is a single
# textarea that gets parsed as dotenv (one KEY=VALUE per line),
# which doesn't tolerate multi-line values. PEM-formatted SSH keys
# ARE multi-line. The workaround is to base64-encode the key before
# pasting into Dokploy and decode here at use-time. Base64 produces
# a single line of ASCII with no characters that confuse dotenv
# parsers.
#
# Encode locally:
#   base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy
# Then paste in Dokploy as:
#   ssh_key=<that base64 string>
#
# Steps in this RUN:
#   1. mkdir ~/.ssh with 0700 (ssh refuses world-readable config dirs)
#   2. ssh-keyscan git.kadil.dev so we don't hang on the
#      "continue connecting?" prompt
#   3. base64-decode /run/secrets/ssh_key into /tmp/ssh_key. The
#      decoded file holds the original PEM private key.
#   4. chmod 0600 the decoded key so ssh accepts it.
#   5. Run npm install with GIT_SSH_COMMAND pointing at the decoded
#      key. IdentitiesOnly=yes prevents ssh from trying any other
#      key it might find.
#   6. rm the decoded key. Since we created it in the same RUN, it
#      never gets committed to a layer.
#
# If the build aborts here:
#   • "Permission denied (publickey)" → the key in Dokploy doesn't
#     match the deploy key registered in Gitea. Re-base64-encode and
#     re-paste, OR re-check the Gitea deploy key.
#   • "Identity file /tmp/ssh_key not accessible: No such file or
#     directory" → the secret wasn't mounted. Verify the Dokploy
#     Build-time Secrets textarea contains exactly `ssh_key=...` as
#     a single line.
#   • "base64: invalid input" → the Dokploy textarea has trailing
#     whitespace, line breaks inside the base64, or extra content.
#     Re-paste from a fresh `base64 < key | tr -d '\n' | pbcopy`.
RUN --mount=type=secret,id=ssh_key \
    mkdir -p -m 0700 ~/.ssh && \
    ssh-keyscan -p 222 git.kadil.dev >> ~/.ssh/known_hosts 2>/dev/null && \
    base64 -d /run/secrets/ssh_key > /tmp/ssh_key && \
    chmod 0600 /tmp/ssh_key && \
    GIT_SSH_COMMAND="ssh -i /tmp/ssh_key -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" \
    npm install && \
    rm -f /tmp/ssh_key

COPY . .

RUN npm run build

# Drop dev dependencies (eslint, etc.) so the node_modules we copy
# to runtime is production-only. Without this step, the runtime
# stage would either ship dev deps it doesn't use, or re-run
# `npm install --omit=dev` in an image without the compile
# toolchain — which would fail when canvas tries to rebuild.
RUN npm prune --production

# ─────────────────────────────────────────────────────────────────
# Stage 2: runtime
# Carries only the runtime C libraries canvas links against plus
# system fonts. node_modules is copied whole from the builder;
# no `npm install` happens here, so no SSH is needed at runtime.
# ─────────────────────────────────────────────────────────────────
FROM node:20-alpine

# Runtime packages:
#
#   Native-binding runtime libs (matching the -dev headers in the
#   builder stage):
#     cairo, pango, libjpeg-turbo, giflib, librsvg, pixman
#
#   System fonts for the export pipeline. node-canvas's text rendering
#   uses fontconfig to resolve family names; these provide free
#   equivalents of the proprietary template families (Impact, Times,
#   Courier, Arial) plus emoji support for sticker exports. The
#   editor module's own bundled fonts (under node_modules/goods-editor/
#   fonts/) are registered explicitly via registerFont() — they don't
#   go through fontconfig and don't need to live in /usr/share/fonts.
RUN apk add --no-cache \
    cairo pango libjpeg-turbo giflib librsvg pixman \
    ttf-liberation ttf-dejavu font-noto font-noto-emoji \
    fontconfig

WORKDIR /app

# Bring the production node_modules over from the builder. This
# carries the already-compiled canvas .node binary AND the editor
# module's own fonts (under node_modules/goods-editor/fonts/), so
# no separate font-copy step is needed.
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./

# Application files. server.js is the entry point; dist/ is the
# Vite build output served as static.
COPY server.js ./
COPY --from=builder /app/dist ./dist
RUN mkdir -p /app/uploads /app/exports

# Refresh fontconfig's cache so the apk-installed system fonts are
# discoverable. The module's own fonts under node_modules are
# registered explicitly by the module's server code (via
# registerFont() — see the host's server.js comment about font
# registration moving to the module), so they don't need to be
# in fontconfig's index.
RUN fc-cache -f || true

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1

EXPOSE 3001
ENV NODE_ENV=production
CMD ["node", "server.js"]
