# Multi-stage build for the FileServer pipeline.
#
# The runtime image carries only what serving needs: the Julia runtime, a depot
# that is already instantiated *and* precompiled (so startup is load-only, no
# compilation), exiftool, and optionally github-linguist. Nothing from the build
# survives into it: no compilers, no package registry, no gem toolchain.
#
#   docker build -t file-server .
#   docker build --build-arg WITH_LINGUIST=false -t file-server .   # ~200MB smaller
#
# github-linguist (stage-4 programming-language detection) is the one heavy
# optional dependency: it needs a Ruby toolchain to build rugged. It degrades
# gracefully: without it, text files still get natural-language enrichment and
# the server logs a warning at startup, so it can be built out.

ARG JULIA_VERSION=1.12.6
ARG DEBIAN_RELEASE=bookworm
ARG WITH_LINGUIST=true

# ---------------------------------------------------------------------------
# Stage 1: instantiate and precompile the Julia depot.
# ---------------------------------------------------------------------------
FROM julia:${JULIA_VERSION}-${DEBIAN_RELEASE} AS julia-depot

ENV JULIA_DEPOT_PATH=/opt/julia-depot \
    JULIA_PKG_PRECOMPILE_AUTO=0

WORKDIR /app

# Manifest-first so a source-only edit doesn't re-resolve or rebuild the whole
# dependency tree. FileServer is its own project's root package, so Pkg insists
# on a module file being there; a stub satisfies it while the (slow, rarely
# invalidated) dependency layer is built; the real source lands below.
COPY Project.toml Manifest.toml ./
RUN mkdir -p src \
 && printf 'module FileServer\nend\n' > src/FileServer.jl \
 && julia --project=/app -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'

# Precompiling FileServer itself means the runtime image loads cached code
# instead of compiling Lux/Zygote on first request.
COPY src/ ./src/
RUN julia --project=/app -e 'using Pkg; Pkg.precompile()' \
 && julia --project=/app -e 'using FileServer'

# Everything the depot keeps for *resolving* packages is dead weight once the
# Manifest is pinned: the registry, git clones, scratch and log dirs.
RUN rm -rf /opt/julia-depot/registries \
           /opt/julia-depot/clones \
           /opt/julia-depot/scratchspaces \
           /opt/julia-depot/logs \
 && find /opt/julia-depot/packages -type d \
        \( -name test -o -name docs -o -name .git \) -prune -exec rm -rf {} + \
 && find /opt/julia-depot/artifacts -type d -name doc -prune -exec rm -rf {} +

# ---------------------------------------------------------------------------
# Stage 2: build github-linguist into a relocatable GEM_HOME (optional).
#
# Built against Debian's system Ruby, the same package the runtime installs,
# so rugged's native extension is ABI-compatible with the interpreter there.
# ---------------------------------------------------------------------------
FROM debian:${DEBIAN_RELEASE}-slim AS linguist
ARG WITH_LINGUIST

ENV GEM_HOME=/opt/gems

RUN set -eux; \
    mkdir -p "$GEM_HOME"; \
    if [ "$WITH_LINGUIST" = "true" ]; then \
      apt-get update; \
      apt-get install -y --no-install-recommends \
          ruby ruby-dev build-essential cmake pkg-config \
          libicu-dev zlib1g-dev libssl-dev; \
      gem install --no-document github-linguist; \
      # Drop sources, docs and test fixtures from the installed gems; only the
      # built .so and the .rb load path matter at runtime.
      rm -rf "$GEM_HOME"/cache "$GEM_HOME"/doc; \
      find "$GEM_HOME"/gems -type d \( -name test -o -name spec -o -name samples \) \
          -prune -exec rm -rf {} +; \
      find "$GEM_HOME"/extensions -type d -name tmp -prune -exec rm -rf {} +; \
      rm -rf /var/lib/apt/lists/*; \
    fi

# ---------------------------------------------------------------------------
# Stage 3: runtime.
# ---------------------------------------------------------------------------
FROM debian:${DEBIAN_RELEASE}-slim AS runtime
ARG WITH_LINGUIST

# exiftool is a hard startup prerequisite (stage-2 enrichment fails fast without
# it). Ruby + libicu are only needed to *run* the linguist gem built above.
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        libimage-exiftool-perl ca-certificates curl; \
    if [ "$WITH_LINGUIST" = "true" ]; then \
      apt-get install -y --no-install-recommends ruby libicu72; \
    fi; \
    rm -rf /var/lib/apt/lists/*

RUN useradd --system --uid 10001 --create-home --home-dir /app app

# Depot ownership matters: Julia rewrites a precompile cache it considers stale,
# and a read-only depot would turn that into a startup crash instead of a
# recompile. --chown does it in the copy, so it costs no extra layer.
COPY --from=julia-depot /usr/local/julia /usr/local/julia
COPY --from=julia-depot --chown=app:app /opt/julia-depot /opt/julia-depot
COPY --from=linguist /opt/gems /opt/gems

ENV PATH=/usr/local/julia/bin:/opt/gems/bin:$PATH \
    GEM_HOME=/opt/gems \
    GEM_PATH=/opt/gems \
    JULIA_DEPOT_PATH=/opt/julia-depot \
    JULIA_NUM_THREADS=auto \
    JULIA_PKG_OFFLINE=true

WORKDIR /app
COPY Project.toml Manifest.toml ./
COPY src/ ./src/
COPY bin/server.jl bin/cluster_sweep.jl ./bin/
COPY model/classifier.jld2 ./model/

# Every stage directory hangs off /data so one volume covers the whole pipeline.
# 0.0.0.0 rather than the 127.0.0.1 default: the port is only reachable from
# outside if it is published.
ENV FS_HOST=0.0.0.0 \
    FS_PORT=8080 \
    FS_SPOOL_DIR=/data/spool \
    FS_BINARY_DIR=/data/binary \
    FS_DONE_DIR=/data/done \
    FS_TEXT_DONE_DIR=/data/text_done \
    FS_FAILED_DIR=/data/failed \
    FS_CLUSTER_DIR=/data/binary \
    FS_CLUSTER_CATALOG=/data/catalog.json \
    FS_NOMINATED_DIR=/data/nominated \
    FS_MODEL_PATH=/app/model/classifier.jld2

RUN mkdir -p /data && chown app:app /data
USER app
VOLUME ["/data"]

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
    CMD curl -fsS "http://127.0.0.1:${FS_PORT}/health" || exit 1

ENTRYPOINT ["julia", "--project=/app", "-t", "auto"]
CMD ["bin/server.jl"]
