# Multi-stage Dockerfile for Charon with integrated Caddy
# Single container deployment for simplified home user setup

# Build arguments for versioning
ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF

# Allow pinning Caddy version - Renovate will update this
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
# avoid accidentally pulling a v3 major release. Renovate can still update
# this ARG to a specific v2.x tag when desired.
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
## If the requested tag isn't available, fall back to a known-good v2.10.2 build.
ARG CADDY_VERSION=2.10.2
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
## upstream caddy image tags while still shipping a pinned caddy binary.
ARG CADDY_IMAGE=alpine:3.23

# ---- Cross-Compilation Helpers ----
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0 AS xx

# ---- Frontend Builder ----
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS frontend-builder
WORKDIR /app/frontend

# Copy frontend package files
COPY frontend/package*.json ./

# Build-time project version (propagated from top-level build-arg)
ARG VERSION=dev
# Make version available to Vite as VITE_APP_VERSION during the frontend build
ENV VITE_APP_VERSION=${VERSION}

# Set environment to bypass native binary requirement for cross-arch builds
ENV npm_config_rollup_skip_nodejs_native=1 \
    ROLLUP_SKIP_NODEJS_NATIVE=1

RUN npm ci

# Copy frontend source and build
COPY frontend/ ./
RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
    npm run build

# ---- Backend Builder ----
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS backend-builder
# Copy xx helpers for cross-compilation
COPY --from=xx / /

WORKDIR /app/backend

# Install build dependencies
# xx-apk installs packages for the TARGET architecture
ARG TARGETPLATFORM
# hadolint ignore=DL3018
RUN apk add --no-cache clang lld
# hadolint ignore=DL3018,DL3059
RUN xx-apk add --no-cache gcc musl-dev sqlite-dev

# Install Delve (cross-compile for target)
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
# hadolint ignore=DL3059,DL4006
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest && \
    DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
    if [ -n "$DLV_PATH" ] && [ "$DLV_PATH" != "/go/bin/dlv" ]; then \
        mv "$DLV_PATH" /go/bin/dlv; \
    fi && \
    xx-verify /go/bin/dlv

# Copy Go module files
COPY backend/go.mod backend/go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download

# Copy backend source
COPY backend/ ./

# Build arguments passed from main build context
ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE=unknown

# Build the Go binary with version information injected via ldflags
# xx-go handles CGO and cross-compilation flags automatically
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    CGO_ENABLED=1 xx-go build \
    -ldflags "-s -w -X github.com/Wikid82/charon/backend/internal/version.Version=${VERSION} \
              -X github.com/Wikid82/charon/backend/internal/version.GitCommit=${VCS_REF} \
              -X github.com/Wikid82/charon/backend/internal/version.BuildTime=${BUILD_DATE}" \
    -o charon ./cmd/api

# ---- Caddy Builder ----
# Build Caddy from source to ensure we use the latest Go version and dependencies
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION

# hadolint ignore=DL3018
RUN apk add --no-cache git
# hadolint ignore=DL3062
RUN --mount=type=cache,target=/go/pkg/mod \
    go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build Caddy for the target architecture with security plugins.
# We use XCADDY_SKIP_CLEANUP=1 to keep the build environment, then patch dependencies.
# hadolint ignore=SC2016
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    sh -c 'set -e; \
        export XCADDY_SKIP_CLEANUP=1; \
        # Run xcaddy build - it will fail at the end but create the go.mod
        GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
            --with github.com/greenpau/caddy-security \
            --with github.com/corazawaf/coraza-caddy/v2 \
            --with github.com/hslatman/caddy-crowdsec-bouncer \
            --with github.com/zhangjiayin/caddy-geoip2 \
            --with github.com/mholt/caddy-ratelimit \
            --output /tmp/caddy-temp || true; \
        # Find the build directory
        BUILDDIR=$(ls -td /tmp/buildenv_* 2>/dev/null | head -1); \
        if [ -d "$BUILDDIR" ] && [ -f "$BUILDDIR/go.mod" ]; then \
            echo "Patching dependencies in $BUILDDIR"; \
            cd "$BUILDDIR"; \
            # Upgrade transitive dependencies to pick up security fixes.
            # These are Caddy dependencies that lag behind upstream releases.
            # Renovate tracks these via regex manager in renovate.json
            # TODO: Remove this block once Caddy ships with fixed deps (check v2.10.3+)
            # renovate: datasource=go depName=github.com/expr-lang/expr
            go get github.com/expr-lang/expr@v1.17.6 || true; \
            # renovate: datasource=go depName=github.com/quic-go/quic-go
            go get github.com/quic-go/quic-go@v0.57.1 || true; \
            # renovate: datasource=go depName=github.com/smallstep/certificates
            go get github.com/smallstep/certificates@v0.29.0 || true; \
            go mod tidy || true; \
            # Rebuild with patched dependencies
            echo "Rebuilding Caddy with patched dependencies..."; \
            GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/bin/caddy \
                -ldflags "-w -s" -trimpath -tags "nobadger,nomysql,nopgx" . && \
            echo "Build successful"; \
        else \
            echo "Build directory not found, using standard xcaddy build"; \
            GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
                --with github.com/greenpau/caddy-security \
                --with github.com/corazawaf/coraza-caddy/v2 \
                --with github.com/hslatman/caddy-crowdsec-bouncer \
                --with github.com/zhangjiayin/caddy-geoip2 \
                --with github.com/mholt/caddy-ratelimit \
                --output /usr/bin/caddy; \
        fi; \
        rm -rf /tmp/buildenv_* /tmp/caddy-temp; \
        /usr/bin/caddy version'

# ---- CrowdSec Installer ----
# CrowdSec requires CGO (mattn/go-sqlite3), so we cannot build from source
# with CGO_ENABLED=0. Instead, we download prebuilt static binaries for amd64
# or install from packages. For other architectures, CrowdSec is skipped.
FROM alpine:3.23 AS crowdsec-installer

WORKDIR /tmp/crowdsec

ARG TARGETARCH
# CrowdSec version - Renovate can update this
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.7.4

# hadolint ignore=DL3018
RUN apk add --no-cache curl tar

# Download static binaries (only available for amd64)
# For other architectures, create empty placeholder files so COPY doesn't fail
# hadolint ignore=DL3059,SC2015
RUN set -eux; \
    mkdir -p /crowdsec-out/bin /crowdsec-out/config; \
    if [ "$TARGETARCH" = "amd64" ]; then \
        echo "Downloading CrowdSec binaries for amd64..."; \
        curl -fSL "https://github.com/crowdsecurity/crowdsec/releases/download/v${CROWDSEC_VERSION}/crowdsec-release.tgz" \
            -o /tmp/crowdsec.tar.gz && \
        tar -xzf /tmp/crowdsec.tar.gz -C /tmp && \
        # Binaries are in cmd/crowdsec-cli/cscli and cmd/crowdsec/crowdsec
        cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec-cli/cscli" /crowdsec-out/bin/ && \
        cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec/crowdsec" /crowdsec-out/bin/ && \
        chmod +x /crowdsec-out/bin/* && \
        # Copy config files from the release tarball
        if [ -d "/tmp/crowdsec-v${CROWDSEC_VERSION}/config" ]; then \
            cp -r "/tmp/crowdsec-v${CROWDSEC_VERSION}/config/"* /crowdsec-out/config/; \
        fi && \
        echo "CrowdSec binaries installed successfully"; \
    else \
        echo "CrowdSec binaries not available for $TARGETARCH - skipping"; \
        # Create empty placeholder so COPY doesn't fail
        touch /crowdsec-out/bin/.placeholder /crowdsec-out/config/.placeholder; \
    fi; \
    # Show what we have
    ls -la /crowdsec-out/bin/ /crowdsec-out/config/ || true

# ---- Final Runtime with Caddy ----
FROM ${CADDY_IMAGE}
WORKDIR /app

# Install runtime dependencies for Charon (no bash needed)
# hadolint ignore=DL3018
RUN apk --no-cache add ca-certificates sqlite-libs tzdata curl gettext \
    && apk --no-cache upgrade

# Download MaxMind GeoLite2 Country database
# Note: In production, users should provide their own MaxMind license key
# This uses the publicly available GeoLite2 database
RUN mkdir -p /app/data/geoip && \
    curl -L "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
    -o /app/data/geoip/GeoLite2-Country.mmdb

# Copy Caddy binary from caddy-builder (overwriting the one from base image)
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy

# Copy CrowdSec binaries from the crowdsec-installer stage (optional - only amd64)
# The installer creates placeholders for non-amd64 architectures
COPY --from=crowdsec-installer /crowdsec-out/bin/* /usr/local/bin/
COPY --from=crowdsec-installer /crowdsec-out/config /etc/crowdsec.dist

# Clean up placeholder files and verify CrowdSec (if available)
RUN rm -f /usr/local/bin/.placeholder /etc/crowdsec.dist/.placeholder 2>/dev/null || true; \
    if [ -x /usr/local/bin/cscli ]; then \
        echo "CrowdSec installed:"; \
        cscli version || echo "CrowdSec version check failed"; \
    else \
        echo "CrowdSec not available for this architecture - skipping verification"; \
    fi

# Create required CrowdSec directories in runtime image
RUN mkdir -p /etc/crowdsec /etc/crowdsec/acquis.d /etc/crowdsec/bouncers \
             /etc/crowdsec/hub /etc/crowdsec/notifications \
             /var/lib/crowdsec/data /var/log/crowdsec /var/log/caddy

# Copy CrowdSec configuration templates from source
COPY configs/crowdsec/acquis.yaml /etc/crowdsec.dist/acquis.yaml
COPY configs/crowdsec/install_hub_items.sh /usr/local/bin/install_hub_items.sh
COPY configs/crowdsec/register_bouncer.sh /usr/local/bin/register_bouncer.sh

# Make CrowdSec scripts executable
RUN chmod +x /usr/local/bin/install_hub_items.sh /usr/local/bin/register_bouncer.sh

# Copy Go binary from backend builder
COPY --from=backend-builder /app/backend/charon /app/charon
RUN ln -s /app/charon /app/cpmp || true
# Copy Delve debugger (xx-go install places it in /go/bin)
COPY --from=backend-builder /go/bin/dlv /usr/local/bin/dlv

# Copy frontend build from frontend builder
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist

# Copy startup script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

# Set default environment variables
ENV CHARON_ENV=production \
    CHARON_DB_PATH=/app/data/charon.db \
    CHARON_FRONTEND_DIR=/app/frontend/dist \
    CHARON_CADDY_ADMIN_API=http://localhost:2019 \
    CHARON_CADDY_CONFIG_DIR=/app/data/caddy \
    CHARON_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb \
    CHARON_HTTP_PORT=8080 \
    CHARON_CROWDSEC_CONFIG_DIR=/app/data/crowdsec
# Create necessary directories
RUN mkdir -p /app/data /app/data/caddy /config /app/data/crowdsec

# Re-declare build args for LABEL usage
ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF

# OCI image labels for version metadata
LABEL org.opencontainers.image.title="Charon (CPMP legacy)" \
      org.opencontainers.image.description="Web UI for managing Caddy reverse proxy configurations" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.created="${BUILD_DATE}" \
      org.opencontainers.image.revision="${VCS_REF}" \
    org.opencontainers.image.source="https://github.com/Wikid82/charon" \
    org.opencontainers.image.url="https://github.com/Wikid82/charon" \
    org.opencontainers.image.vendor="charon" \
      org.opencontainers.image.licenses="MIT"

# Expose ports
EXPOSE 80 443 443/udp 2019 8080

# Use custom entrypoint to start both Caddy and Charon
ENTRYPOINT ["/docker-entrypoint.sh"]
