FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-amd64 AS dotnet-runtime

FROM node:22-bookworm-slim AS web-build
WORKDIR /source
COPY web/package*.json ./web/
WORKDIR /source/web
RUN npm ci
WORKDIR /source
COPY design-system/. ./design-system/
COPY web/. ./web/
WORKDIR /source/web
# THE VITEST SUITE IS NOT RUN IN THIS STAGE, AND MUST NOT BE RE-ADDED (ersatztv#887).
# This stage is gitless in both senses at once. It copies only `web/` and `design-system/`, so it
# holds no `.git` — a property of the STAGE, not of the context, which is the repository root
# (`context: .`) and whose `.dockerignore` does not exclude `.git`: the directory is there to be
# copied and simply is not. And `node:22-bookworm-slim` ships no git binary (`command -v git` -> not
# found), which is why a `COPY .git` would not help either. Members of the SPA suite need one or the
# other: `pageSizeCallSites.guard.test.ts` and `completeAnnotations.guard.test.ts` derive their file
# population from `git ls-files` and refuse to fall back to a directory walk
# (`testing.guard-derives-population-from-source`), and `trackedSourceFiles.realgit.test.ts` builds
# its own temp repository. (Not every `*.guard.test.ts` — `completeRequest.guard.test.ts` needs no
# git at all.)
#
# Running the suite here anyway therefore costs a hand-maintained list of the members that cannot
# run — and that list is a population nothing derives. It went stale the first time a guard was
# added without updating it, and because `Build & push image (amd64)` is `if: github.event_name !=
# 'pull_request'`, the resulting red is unreachable on a PR and lands on `main` and on the `v*` tag
# path: every image build failed and `:latest` stopped being republished. Excluding one more file
# re-arms that; the list is removed instead.
#
# WHAT STILL VALIDATES THE SUITE. `docker-build.yml`'s `test` job runs it UNFILTERED on a real
# checkout, and `build` — the job that invokes this Dockerfile — carries `needs: [test, migrations,
# scan]`, so an image cannot be published past a red suite. Two skips exist inside `test` and
# neither leaves an image unvalidated: `docs_only` also gates `Build and push`, so that arm ships
# nothing — and that arm IS asserted, by `test_the_DOCS_ONLY_arm_cannot_publish_an_image`. The #420
# revalidate skip fires only on a tree byte-identical to a head that already carried a green combined
# status; that arm is NOT asserted here — it rests on `scripts/ci-detect-already-validated.sh`, which
# carries no mutation proof of its own, so it is a dependency rather than a check. Held by
# `scripts/tests/test_image_build_delegates_the_spa_suite.py`. That guard PINS the command line
# below as text: any edit to it reddens, whatever the edit says. Update the pin in the same commit,
# and do not use it to re-add a suite run.
#
# `lint` and `typecheck` stay. They are gitless-safe with no member that is not, so they carry no
# list and no trap; whether the image build should run them at all is a separate question this
# does not answer.
RUN npm run lint && npm run typecheck && npm run build

FROM --platform=linux/amd64 192.168.1.95:3000/timothy/ersatztv-ffmpeg:8.1.2 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 python3-pip && \
    python3 -m pip install --target=/app/pythonlibs --no-cache-dir streamlink && \
    apt-get clean -y && \
    rm -rf /var/lib/apt/lists/*

# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64 AS build
RUN apt-get update && apt-get install -y ca-certificates gnupg default-jre-headless python3-pip
WORKDIR /source

# download the openapi-generator jar first so this layer is cached independently
# of the openapi spec dir below -- otherwise every spec change (e.g. v1.json)
# busts the layer and re-downloads the ~30MB jar (ersatztv#190)
RUN wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.15.0/openapi-generator-cli-7.15.0.jar

# generate openapi client
COPY ErsatzTV/wwwroot/openapi/. /app/ErsatzTV/wwwroot/openapi/
RUN java -jar openapi-generator-cli-7.15.0.jar generate -i /app/ErsatzTV/wwwroot/openapi/scripted-schedule.json -g python -o /app/etv-client --package-name etv_client
RUN rm -rf openapi-generator-cli-7.15.0.jar /app/ErsatzTV
RUN python3 -m pip install --target=/app/pythonlibs /app/etv-client
RUN rm -rf /app/etv-client
COPY scripts/scripted-schedules/. /app/scripted-schedules/

# copy csproj and restore as distinct layers
COPY *.sln .
# repo-wide build config (MSBuild props/targets incl. NuGet-audit warning
# exemptions, SDK pin, analyzer severities, and the Central Package Management
# version manifest) must be present before restore so the image build matches
# local/CI builds. Directory.Packages.props is REQUIRED here: with CPM the csproj
# carry no versions, so restore fails without the central manifest.
COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json .editorconfig ./
COPY eng/analyzers/sdk-all-suggestion.globalconfig ./eng/analyzers/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/*.csproj ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/*.csproj ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/*.csproj ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/*.csproj ./ErsatzTV.Scanner/

# Disable the persistent Roslyn/MSBuild compiler servers for the in-image build (ersatztv#406,
# server-management#604). The workflow sets these as env for the runner-side dotnet jobs, but this
# stage compiles inside `docker build`, so the workflow's env does NOT reach it — the restore and
# the two publishes below would otherwise spin up their own VBCSCompiler and hold its heap. This is
# the `build` job that server-management#570 measured pegging 5.999/6 GiB, so it is the one that
# most needs this.
#
# Placed here rather than at the top of the stage on purpose: an ENV invalidates every layer below
# it, and the wget of the ~30MB openapi-generator jar above is deliberately ordered early to stay
# cached (ersatztv#190). Nothing between that wget and this line compiles, so this is the earliest
# point where the ENV is free.
#
# Build-stage only: the final image is FROM runtime-base and only COPY --from=build /app (files,
# not ENV), so none of this lands in the shipped image or affects runtime.
ENV UseSharedCompilation=false \
    DOTNET_CLI_USE_MSBUILD_SERVER=0 \
    MSBUILDDISABLENODEREUSE=1

RUN dotnet restore -r linux-x64 ErsatzTV/ErsatzTV.csproj

# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY --from=web-build /source/ErsatzTV/wwwroot/app/. ./ErsatzTV/wwwroot/app/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/. ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/. ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/. ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/. ./ErsatzTV.Scanner/
ARG INFO_VERSION="unknown"
ARG BUILD_CONFIG="release"
WORKDIR /source/ErsatzTV.Scanner
RUN dotnet publish ErsatzTV.Scanner.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
WORKDIR /source/ErsatzTV
RUN sed -i '/Scanner/d' ErsatzTV.csproj
RUN dotnet publish ErsatzTV.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}

# final stage/image
FROM runtime-base
ENV FONTCONFIG_PATH=/etc/fonts
RUN fc-cache update
WORKDIR /app
COPY --from=build /app ./
ENV PYTHONPATH=/app/pythonlibs
ENV ETV_CONFIG_FOLDER=/config
ENV ETV_TRANSCODE_FOLDER=/transcode
ENV ETV_DISABLE_VULKAN=1
ENTRYPOINT ["./ErsatzTV"]
