# syntax=docker/dockerfile:1.5
# =============================================================================
# Three-stage build: frontend → python-build → runtime
#   1. frontend:     Bun (deps) + Node.js (vite build)
#   2. python-build: Python + build tools — pip install (compilation)
#   3. runtime:      Python slim — copies from both, no Node, no gcc (~1.5GB)
# =============================================================================

# Build args
ARG USE_CUDA=false
ARG USE_OLLAMA=false
ARG USE_CUDA_VER=cu121
ARG USE_EMBEDDING_MODEL=intfloat/multilingual-e5-large
ARG USE_RERANKING_MODEL=""
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
ARG BUILD_HASH=dev-build
ARG UID=0
ARG GID=0

# =============================================================================
# Stage 1: FRONTEND — Bun for deps, Node.js for vite build
# =============================================================================
FROM node:22-bookworm AS frontend
ARG BUILD_HASH

# Install bun (fast dependency management; vite build stays on Node.js for memory)
RUN npm install -g bun

WORKDIR /app

# Install dependencies via bun (cache layer)
COPY app/package.json /app/package.json
COPY app/bun.lock /app/bun.lock
RUN bun install --frozen-lockfile

# Setup Pyodide (cache layer)
COPY app/scripts/prepare-pyodide.js /app/scripts/prepare-pyodide.js
RUN mkdir -p /app/static/pyodide && \
    NODE_OPTIONS="--max-old-space-size=4096" node scripts/prepare-pyodide.js

# Copy files needed for frontend build
COPY app/postcss.config.js /app/postcss.config.js
COPY app/pyproject.toml /app/pyproject.toml
COPY app/svelte.config.js /app/svelte.config.js
COPY app/tailwind.config.js /app/tailwind.config.js
COPY app/tsconfig.json /app/tsconfig.json
COPY app/vite.config.ts /app/vite.config.ts

# Copy static files into build dir for vite
COPY app/static/ /app/build/
COPY app/src /app/src

# Build frontend (Node.js runtime — bun's JSC OOMs on large Svelte builds)
RUN NODE_OPTIONS="--max-old-space-size=4096" npx vite build

# Copy custom.css after vite build (SvelteKit clears output dir during build)
COPY app/static/assets/custom.css /app/build/assets/custom.css


# =============================================================================
# Stage 2: PYTHON-BUILD — compile Python packages with build tools
# =============================================================================
FROM python:3.11-bookworm AS python-build

# Build tools needed for native extensions (chromadb/hnswlib, psycopg2, etc.)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential gcc g++ python3-dev && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Install core Python dependencies (ML packages installed at runtime via wizard)
COPY app/backend/requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

# Pre-download tiktoken encoding (~1MB)
RUN mkdir -p /app/backend/tiktoken_cache && \
    TIKTOKEN_CACHE_DIR=/app/backend/tiktoken_cache \
    python -c "import tiktoken; tiktoken.get_encoding('o200k_base')"


# =============================================================================
# Stage 3: RUNTIME — slim image, no Node.js, no build tools
# =============================================================================
FROM python:3.11-slim-bookworm AS runtime

ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
ARG UID=0
ARG GID=0
ARG BUILD_HASH

# Runtime-only system packages
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    ca-certificates curl jq \
    ffmpeg \
    cron rclone && \
    apt-get clean && rm -rf /var/lib/apt/lists/* && \
    # Symlink python3 for scripts that reference it
    ln -sf /usr/local/bin/python /usr/local/bin/python3

# Copy Python packages from python-build stage (paths match: both use site-packages)
COPY --from=python-build /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
COPY --from=python-build /usr/local/bin/ /usr/local/bin/

# ml_packages (data-volume install via wizard) loads AFTER site-packages so the
# system bcrypt/uvicorn/click/anyio/pydantic always win. Transitional: the 2.4
# bundle replaces this with a signed tarball pulled into the data volume.
RUN printf 'import sys, os\n_ml = "/app/backend/data/ml_packages"\nif os.path.isdir(_ml) and _ml not in sys.path:\n    sys.path.append(_ml)\n' \
    > /usr/local/lib/python3.11/sitecustomize.py

# Install `uv` for the runtime ML wizard install. Pinned so a 0.x breaking
# change cannot ship via the base image. The release artifact name uses the
# same arch tokens as `uname -m` on Linux (x86_64, aarch64) — both Docker
# buildx targets are covered. Bump UV_VERSION to the latest release that has
# both x86_64 and aarch64 Linux gnu artifacts at edit time:
#   curl -s https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r .tag_name
ARG UV_VERSION=0.5.18
RUN ARCH=$(uname -m) && \
    curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${ARCH}-unknown-linux-gnu.tar.gz" \
      | tar -xz -C /usr/local/bin --strip-components=1 "uv-${ARCH}-unknown-linux-gnu/uv" && \
    uv --version

# Copy tiktoken cache
COPY --from=python-build /app/backend/tiktoken_cache/ /app/backend/tiktoken_cache/

# Copy bun binary + node_modules for dev mode (hot reload via `make dev_run`)
COPY --from=frontend /usr/local/bin/bun /usr/local/bin/bun
COPY --from=frontend /app/node_modules/ /app/node_modules/

# Copy vite build output (frontend)
COPY --from=frontend /app/build/ /app/build/

# Copy static files (single copy — config.py syncs from /app/build/static at startup)
COPY app/static/ /app/static/

# Copy backend source
COPY app/backend/ /app/backend/

# Copy changelog and package.json (version string)
COPY CHANGELOG.md /app/CHANGELOG.md
COPY app/package.json /app/package.json

WORKDIR /app/backend

## Environment variables ##
ENV ENV=prod \
    PORT=8080 \
    STATIC_DIR=/app/static \
    USE_OLLAMA_DOCKER=${USE_OLLAMA} \
    USE_CUDA_DOCKER=${USE_CUDA} \
    USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
    USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
    USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}

ENV OLLAMA_BASE_URL="/ollama" \
    OPENAI_API_BASE_URL=""

ENV OPENAI_API_KEY="" \
    WEBUI_SECRET_KEY="" \
    DO_NOT_TRACK=true \
    ANONYMIZED_TELEMETRY=false \
    CHROMA_TELEMETRY=false \
    USER_AGENT="Sage-is-AI/2.0" \
    ORT_LOG_LEVEL=3

ENV WHISPER_MODEL="base" \
    WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"

ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
    RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
    SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"

ENV USE_TIKTOKEN_ENCODING_NAME="o200k_base"
ENV TIKTOKEN_ENCODING_NAME="$USE_TIKTOKEN_ENCODING_NAME" \
    TIKTOKEN_CACHE_DIR="/app/backend/tiktoken_cache"

ENV HF_HOME="/app/backend/data/cache/embedding/models"

ENV BACKUP_PATH="" \
    BACKUP_CRON="0 2 *"

ENV HOME=/root

# Create user if not root
RUN if [ $UID -ne 0 ]; then \
    if [ $GID -ne 0 ]; then addgroup --gid $GID app; fi; \
    adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
    fi

# Persist chroma's local cache (telemetry id + ONNX embedding model bundle)
# in the data volume. chromadb constructs paths via `Path.home() / ".cache"
# / "chroma" / ...` — Python's `Path.home()` reads the HOME env var, not
# XDG_CACHE_HOME, so the only reliable redirect is at the filesystem layer.
# Symlinking $HOME/.cache/chroma into the persisted /app/backend/data/cache
# tree matches the existing convention for libraries that don't expose a
# cache-path env var (sentence-transformers, HF, whisper, tiktoken all
# point at /app/backend/data/cache via their respective env vars). Survives
# fresh container starts AND CapRover redeploys as long as the volume is
# mounted at /app/backend/data — which is the standard mount path in both
# the Makefile workflow and the docs/try-sage-deployment.md CapRover guide.
RUN mkdir -p /app/backend/data/cache/chroma && \
    mkdir -p $HOME/.cache && \
    ln -s /app/backend/data/cache/chroma $HOME/.cache/chroma && \
    echo -n 00000000-0000-0000-0000-000000000000 > /app/backend/data/cache/chroma/telemetry_user_id

# Fix ownership if not root
RUN if [ $UID -ne 0 ]; then \
    chown -R $UID:$GID /app $HOME; \
    fi

# Conditional Ollama install
RUN if [ "$USE_OLLAMA" = "true" ]; then \
    curl -fsSL https://ollama.com/install.sh | sh; \
    fi

# Create data directory
RUN mkdir -p /app/backend/data && \
    if [ $UID -ne 0 ]; then chown -R $UID:$GID /app/backend/data/ /app/backend/tiktoken_cache/; fi

EXPOSE 8080

HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1

USER $UID:$GID

ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
ENV DOCKER=true

CMD [ "bash", "restore_backup_start.sh", "server" ]
