# Сервис worker (#78): выполняет задачи из очереди в Google Chrome под Xvfb.
#
# builder: компилирует src/ (через entry_worker.py) в самодостаточный standalone-бинарник Nuitka —
# в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements.txt.
FROM python:3.12-slim-bookworm AS builder

ENV PYTHONDONTWRITEBYTECODE=1 \
	PIP_NO_CACHE_DIR=1 \
	PIP_DISABLE_PIP_VERSION_CHECK=1

# build-essential (gcc + libc6-dev — заголовки Python.h тянут stdlib.h/limits.h) — компилятор для
# сгенерированного Nuitka C-кода; patchelf — правит RPATH бинарника в standalone-режиме;
# ccache — ускоряет повторные сборки при пересборке образа
RUN apt-get update \
	&& apt-get install -y --no-install-recommends build-essential patchelf ccache \
	&& rm -rf /var/lib/apt/lists/*

# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md
ARG OFFLINE=0
WORKDIR /build
COPY requirements.txt requirements-build.txt ./
COPY wheels/ ./wheels/
RUN set -e; \
	if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=./wheels"; else PIP_OPTS=""; fi; \
	pip install $PIP_OPTS -r requirements.txt -r requirements-build.txt

COPY src/ ./src/

# standalone: entry_worker.dist/ — изолированная папка со своим Python-рантаймом и всеми пакетами, которые
# реально нужны воркеру (patchright, playwright, browserforge, dramatiq, db, tasks, engine, antibot, captcha,
# полный api/ — см. worker/windows.py). worker.actors попадает в сборку только из-за явного import в
# entry_worker.py: dramatiq грузит его по строке "worker.actors" динамически, статика Nuitka это не видит
# --include-package-data: Nuitka копирует только код, а patchright/browserforge везут с собой
# небольшой код + большие данные не-.py (драйвер patchright — Node.js бинарник и JS, browserforge и
# apify_fingerprint_datapoints — zip/json датасеты отпечатков), без них падают с FileNotFoundError в runtime
RUN python -m nuitka --mode=standalone \
	--include-package=worker --include-package=engine --include-package=antibot --include-package=captcha \
	--include-package=db --include-package=tasks --include-package=api \
	--include-package-data=patchright --include-package-data=browserforge \
	--include-package-data=apify_fingerprint_datapoints \
	--include-data-files=src/api/js/canvas_noise.js=api/js/canvas_noise.js \
	--output-dir=/build/dist \
	src/entry_worker.py

# runtime: Python тут остаётся — он нужен только на один шаг (установка Chrome через `patchright install`,
# тот же механизм, что и раньше), а дальше запускается готовый standalone-бинарник, а не исходники из
# requirements.txt. Полный интерпретатор здесь — осознанный компромисс: переносить apt-зависимости Chrome
# между стадиями вручную хрупко (patchright сам решает, что ставить под конкретную версию Chrome).
FROM python:3.12-slim-bookworm AS runtime

ENV PIP_NO_CACHE_DIR=1 \
	PIP_DISABLE_PIP_VERSION_CHECK=1

# Xvfb — браузер запускается в headful-режиме (headless=False), нужен виртуальный дисплей
RUN apt-get update \
	&& apt-get install -y --no-install-recommends tini xvfb xauth fonts-liberation fonts-noto-color-emoji ca-certificates \
	&& rm -rf /var/lib/apt/lists/*

# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями.
# Версия patchright — как в requirements.txt (#109): её CLI и сам пакет worker'у в runtime не нужны,
# только Chrome и его системные библиотеки, которые он поставит через apt
ARG OFFLINE=0
COPY wheels/ /tmp/wheels/
RUN set -e; \
	if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=/tmp/wheels"; else PIP_OPTS=""; fi; \
	pip install $PIP_OPTS patchright==1.63.0 \
	&& patchright install --with-deps chrome \
	&& pip uninstall -y patchright \
	&& rm -rf /tmp/wheels /var/lib/apt/lists/* /root/.cache

RUN useradd --create-home --uid 1000 app \
	&& mkdir -p /app/extra \
	&& chown -R app:app /app

WORKDIR /app
COPY --from=builder --chown=app:app /build/dist/entry_worker.dist/ ./

USER app

# профили браузера (extra/user_data_dir) и geoip-кэш — в volume
VOLUME ["/app/extra"]

# tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби)
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
# BROWSER_SLOTS/WORKER_THREADS читает сам entry_worker.bin (--processes 1, потоков — вдвое больше слотов)
CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' ./entry_worker.bin"]

# test (make test-browser; не публикуется, не используется по умолчанию — docker build должен указывать
# --target=runtime): entry_worker.bin — скомпилированный бинарник, pytest/tests/harness.py не может
# подключиться к его internals напрямую. Этой стадии нужны тот же Chrome/Xvfb из runtime и исходники
# как обычный интерпретируемый Python рядом с бинарником — они не конфликтуют (разные пути)
FROM runtime AS test
USER root
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app src/ ./src/
USER app
WORKDIR /app
