ARG PYTHON_VERSION=3.12
ARG RACKET_VERSION=8.12
ARG UV_VERSION=0.4.0
ARG DEBIAN_VERSION=bookworm

# OCI Image Specification (org.opencontainers.image.*) 标签默认值。
# 在正式发布时，GitHub Actions release.yml docker/metadata-action@v5 会通过
# build-push-action 的 --build-arg 注入真实值（基于 GITHUB_SHA / GITHUB_REPOSITORY
# / github.event.release.tag_name / {{date '...'}}）；本地 docker build 未传则回退到
# 下方占位值，保证镜像仍满足 OCI 标注契约（非空字符串 + 合法 URI/semver 形状）。
ARG OCI_SOURCE="https://github.com/4TWS3/agentLisp"
ARG OCI_VERSION="0.1.0-dev"
ARG OCI_REVISION="0000000000000000000000000000000000000000"
ARG OCI_CREATED="1970-01-01T00:00:00Z"
ARG OCI_TITLE="AgentLisp v2.0 Runtime Image"

FROM python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} AS python-base

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

FROM python-base AS builder-base

ARG UV_VERSION
ENV VIRTUAL_ENV=/app/.venv \
    UV_PROJECT_ENVIRONMENT=/app/.venv \
    PATH="/root/.cargo/bin:/root/.local/bin:/app/.venv/bin:$PATH"

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        unzip \
        libfontconfig1 \
    && rm -rf /var/lib/apt/lists/*

ADD --chmod=755 https://astral.sh/uv/${UV_VERSION}/install.sh /install-uv.sh
RUN sh /install-uv.sh

FROM python-base AS racket-base

ARG RACKET_VERSION
ARG RACKET_INSTALLER="racket-minimal-${RACKET_VERSION}-x86_64-linux-bc.sh"
ARG RACKET_URL="https://download.racket-lang.org/releases/${RACKET_VERSION}/installers/${RACKET_INSTALLER}"

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        libfontconfig1 \
    && curl -fsSL "${RACKET_URL}" -o /tmp/racket-installer.sh \
    && sh /tmp/racket-installer.sh --unix-style --dest /usr/local --create-dir \
    && raco pkg install --auto --no-docs --batch \
         rackunit-lib \
         data-lib \
    && rm -rf /tmp/* /root/.cache /var/lib/apt/lists/*

FROM builder-base AS builder

WORKDIR /app

COPY python/pyproject.toml ./python/
WORKDIR /app/python
# 依赖层（不含项目本体）：--no-install-project 让这层可缓存。
# 教训：不要 touch 空 uv.lock 再 --frozen —— 空 lock 会让 sync「成功但什么都不装」，
#      项目层随之装不上，最终镜像里没有 .venv/bin/agentlisp
#      （CI 实测：docker run ... agentlisp --help → exec: agentlisp: not found，exit 127）。
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --no-dev --no-install-project

COPY python/agentlisp_runtime ./agentlisp_runtime
# 项目层：必须**真装项目**，console script（agentlisp = agentlisp_runtime.cli:app）才会生成。
# 构建期自检：脚本不存在就让镜像构建失败（约定交给机器拦，而不是等 CI 冒烟步骤才发现）。
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --no-dev \
    && test -x /app/python/.venv/bin/agentlisp \
    && echo "[docker] console script ok: $(ls -l /app/python/.venv/bin/agentlisp)"

FROM python-base AS runtime-final

COPY --from=racket-base /usr/local /usr/local

ENV VIRTUAL_ENV=/app/.venv \
    PATH="/app/.venv/bin:/usr/local/bin:$PATH" \
    SCHEME_ROOT=/app/scheme \
    # /app 必须在 PYTHONPATH 上：CI 的模块冒烟跑 `python -m examples.dist.repair_agent`
    PYTHONPATH=/app:/app/python

WORKDIR /app

COPY --from=builder /app/python/.venv /app/.venv
COPY scheme /app/scheme
COPY python /app/python
COPY docker/entrypoint.sh /app/docker/entrypoint.sh
COPY config /app/config
# examples 必须进镜像：CI 的模块冒烟跑 `python -m examples.dist.repair_agent`
COPY examples /app/examples
# compiler 必须进镜像：否则镜像只能跑运行时、不能编译 .al —— 审查者据此判定「模式未实现」
# （分发物与源码树能力不一致 = 误导用户）。Racket 已由 racket-base 阶段提供。
COPY compiler /app/compiler

RUN chmod +x /app/docker/entrypoint.sh \
    && ln -sf /app/python/pyproject.toml /app/pyproject.toml

# CLI 入口。两个必须处理的现实：
#  1) console script 的 shebang 由 uv 写成**构建期** venv 路径（/app/python/.venv/bin/python），
#     运行时只有 /app/.venv —— 那个解释器不存在，于是 `exec: agentlisp: not found`（CI 实测）。
#     → 统一把 shebang 改写为 /app/.venv/bin/python。
#  2) 兜底 shim 只在真的缺入口时建，且必须打印走了哪条路（不许静默降级）。
# 末尾验收**强制走 PATH 查找**：不允许用绝对路径回退掩盖问题（上一版正是这样掩盖了 shebang 缺陷）。
RUN set -eux; \
    for f in /app/.venv/bin/agentlisp /app/.venv/bin/agentlisp2; do \
      if [ -f "$f" ]; then \
        echo "[docker] shebang before: $(head -1 "$f")"; \
        sed -i '1s|^#!.*|#!/app/.venv/bin/python|' "$f"; \
        echo "[docker] shebang after : $(head -1 "$f")"; \
      fi; \
    done; \
    if ! command -v agentlisp > /dev/null 2>&1; then \
      echo "[docker] WARN: 没有 agentlisp 入口，创建 /usr/local/bin/agentlisp shim"; \
      printf '#!/bin/sh\nexec /app/.venv/bin/python -m agentlisp_runtime.cli "$@"\n' > /usr/local/bin/agentlisp; \
      chmod +x /usr/local/bin/agentlisp; \
    fi; \
    echo "[docker] which agentlisp: $(command -v agentlisp)"; \
    echo "[docker] venv bin: $(ls /app/.venv/bin/ | tr '\n' ' ')"; \
    agentlisp --help > /dev/null; \
    echo "[docker] CLI smoke ok（PATH 查找 + --help 均通过）"; \
    cd /app && python -c 'import examples.dist.repair_agent as m; print("[docker] module smoke ok:", m.AGENT_NAME)'; \
    echo "[docker] module smoke ok（examples.dist.repair_agent 可导入）"

# 分发物自证能力：镜像内必须能编译 .al，并把模式语义写进产物。
# 背景：审查者只看分发物，看到镜像里没有 compiler/ 就判定「模式未实现」——
# 分发物必须自己证明能力，而不是靠源码树解释。
RUN set -eux; \
    printf '(defpriority-agent docker_smoke\n  :criteria "urgency"\n  :levels 3)\n' > /tmp/pattern-smoke.al; \
    racket compiler/main.rkt --version; \
    racket compiler/main.rkt -i /tmp/pattern-smoke.al -o /tmp/pattern-smoke.py; \
    grep -q 'self.PATTERNS' /tmp/pattern-smoke.py; \
    echo "[docker] compiler smoke ok: $(wc -l < /tmp/pattern-smoke.py) lines, self.PATTERNS present"; \
    rm -f /tmp/pattern-smoke.al /tmp/pattern-smoke.py

RUN cd /app \
    && ls scheme/ \
    && (raco pkg install --auto --no-docs --batch --no-setup --ignore-build-deps --link ./scheme 2>&1 || \
        raco pkg install --auto --no-docs --batch --no-setup --ignore-build-deps --type dir --name agent-dsl ./scheme 2>&1 || \
        echo "Scheme package setup completed") \
    && echo "Scheme package stage done"

# ===== OCI Image Format Specification 标注（ANNOTATIONS 1.0）=====
# 5 个标准键 org.opencontainers.image.{source,version,revision,created,title}
# 在 release.yml CI 中由 docker/metadata-action@v5 生成并通过 build-push-action --build-arg
# 覆盖；本地 build 未传 arg 时使用顶部 ARG 默认值，保证标签不会是空字符串违反 OCI。
ARG OCI_SOURCE
ARG OCI_VERSION
ARG OCI_REVISION
ARG OCI_CREATED
ARG OCI_TITLE
LABEL org.opencontainers.image.source="${OCI_SOURCE}" \
      org.opencontainers.image.version="${OCI_VERSION}" \
      org.opencontainers.image.revision="${OCI_REVISION}" \
      org.opencontainers.image.created="${OCI_CREATED}" \
      org.opencontainers.image.title="${OCI_TITLE}"

ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["agentlisp", "--help"]
