#!/bin/bash
# CoderFlow docs publish script.
#
# Runs inside a docs-publish task container (as the coder user) after the
# container bootstrap has installed git credentials and cloned/updated the
# environment's repositories with the docs branch checked out.
#
# Builds a static Docusaurus site from the configured docs folder and writes:
#   /task-output/site/               - the built static site
#   /task-output/docs-manifest.json  - { status, sha, ... } for the server
#
# Inputs (environment variables, set by the server):
#   DOCS_REPO_NAME                    short repo name (also the URL segment)
#   DOCS_REPO_DIR                     repo directory name under /workspace
#   DOCS_PATH                         repo-relative docs folder to publish
#   DOCS_BRANCH                       branch to publish from
#   DOCS_BASE_URL                     base URL of the hosted site (/help/{repo}/)
#   DOCS_SITE_TITLE                   site title (the repo name)
#   DOCS_SCAFFOLD_VERSION             version hash of the bundled scaffold
#   DOCS_PUBLISHED_SHA                sha of the currently published build ('' if none)
#   DOCS_PUBLISHED_SCAFFOLD_VERSION   scaffold version of the published build
#   DOCS_FORCE                        '1' to rebuild even when up to date
#
# Mounts (set by the server):
#   /coder-docs-publish   scaffold + this script (read-only)
#   /coder-docs-cache     named volume caching the Docusaurus toolchain
#   /task-output          task output directory

set -u

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}

die() {
    echo "" >&2
    echo "ERROR: $*" >&2
    exit 1
}

write_manifest() {
    # write_manifest <status> <sha>
    MANIFEST_STATUS="$1" MANIFEST_SHA="$2" node -e '
        const fs = require("fs");
        const manifest = {
            status: process.env.MANIFEST_STATUS,
            repoName: process.env.DOCS_REPO_NAME || null,
            branch: process.env.DOCS_BRANCH || null,
            sha: process.env.MANIFEST_SHA || null,
            docsPath: process.env.DOCS_PATH || null,
            scaffoldVersion: process.env.DOCS_SCAFFOLD_VERSION || null,
            finishedAt: new Date().toISOString()
        };
        fs.writeFileSync("/task-output/docs-manifest.json", JSON.stringify(manifest, null, 2));
    ' || die "Failed to write publish manifest"
}

: "${DOCS_REPO_NAME:?DOCS_REPO_NAME is required}"
: "${DOCS_REPO_DIR:?DOCS_REPO_DIR is required}"
: "${DOCS_PATH:?DOCS_PATH is required}"
: "${DOCS_BRANCH:?DOCS_BRANCH is required}"
: "${DOCS_BASE_URL:?DOCS_BASE_URL is required}"

WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
REPO_PATH="$WORKSPACE_DIR/$DOCS_REPO_DIR"
SCAFFOLD_SRC="${DOCS_SCAFFOLD_DIR:-/coder-docs-publish/scaffold}"
CACHE_ROOT="${DOCS_CACHE_DIR:-/coder-docs-cache}"

log "Publishing docs for $DOCS_REPO_NAME (branch: $DOCS_BRANCH, folder: $DOCS_PATH)"

# --- Resolve the commit to publish -----------------------------------------

[ -d "$REPO_PATH/.git" ] || die "Repository directory $REPO_PATH was not cloned. Check the repository configuration and git access for this environment."

cd "$REPO_PATH"

# Always fetch and hard-pin to the branch head. The container bootstrap
# usually leaves the clone there already, but a failed update on a clone
# baked into the environment image would otherwise silently publish (and
# record the SHA of) stale content. A fetch that cannot reach origin is a
# hard error, never a silent fallback.
git fetch origin "$DOCS_BRANCH" || die "Failed to fetch branch '$DOCS_BRANCH' from origin. Check git access for this environment."
git checkout -B "$DOCS_BRANCH" "origin/$DOCS_BRANCH" -- || die "Failed to check out branch '$DOCS_BRANCH'"

SHA="$(git rev-parse HEAD)" || die "Failed to resolve the current commit"
log "Publishing commit $SHA"

# --- Up-to-date short-circuit -----------------------------------------------

# Current only when the commit, scaffold version, docs folder, AND branch all
# match what is published: changing the docs path or branch without a new
# commit must still rebuild (the same SHA would otherwise serve stale docs).
if [ "${DOCS_FORCE:-0}" != "1" ] \
    && [ -n "${DOCS_PUBLISHED_SHA:-}" ] \
    && [ "$SHA" = "${DOCS_PUBLISHED_SHA:-}" ] \
    && [ "${DOCS_SCAFFOLD_VERSION:-}" = "${DOCS_PUBLISHED_SCAFFOLD_VERSION:-}" ] \
    && [ "${DOCS_PATH:-}" = "${DOCS_PUBLISHED_PATH:-}" ] \
    && [ "${DOCS_BRANCH:-}" = "${DOCS_PUBLISHED_BRANCH:-}" ]; then
    log "Published docs are already at commit $SHA (same folder and branch). Nothing to do."
    write_manifest "up-to-date" "$SHA"
    exit 0
fi

# --- Validate the docs folder ----------------------------------------------

[ -d "$REPO_PATH/$DOCS_PATH" ] || die "Docs folder '$DOCS_PATH' does not exist on branch '$DOCS_BRANCH' of $DOCS_REPO_NAME."

# Export ONLY git-tracked files from the published commit into a clean staging
# directory. `git archive` reads the commit tree, so untracked, gitignored, or
# stray files left in a clone (e.g. baked into the environment image) are never
# published - only what a reviewer would see at that commit.
DOCS_SOURCE="$(mktemp -d)"
if ! git archive --format=tar "HEAD:$DOCS_PATH" 2>/dev/null | tar -x -C "$DOCS_SOURCE"; then
    die "Docs folder '$DOCS_PATH' is not a tracked directory at commit ${SHA:0:12}. Commit your docs and publish again."
fi

if ! find "$DOCS_SOURCE" \( -name '*.md' -o -name '*.mdx' \) -type f | head -n 1 | grep -q .; then
    die "Docs folder '$DOCS_PATH' contains no tracked markdown files (.md or .mdx). Add documentation before publishing."
fi

# Symbolic links are rejected outright: the promise is that ONLY the
# configured folder is published, and a link could pull in content from
# anywhere in the repository or container. git archive preserves committed
# symlinks, so check the exported content.
SYMLINKS_FOUND="$(find "$DOCS_SOURCE" -type l | head -n 5)"
if [ -n "$SYMLINKS_FOUND" ]; then
    die "Docs folder '$DOCS_PATH' contains symbolic links, which cannot be published:
$SYMLINKS_FOUND
Replace them with regular files and publish again."
fi

# --- Drop git credentials before building repo-controlled content ----------

# The fetch/checkout above was the last git operation. The Docusaurus build
# compiles and executes doc content (.mdx) from the repository, so remove
# credential material from this process first. Defense in depth: repo
# writers already run code in task containers, but the build has no reason
# to see git credentials.
unset CODER_CONTAINER_TOKEN CODER_CREDENTIAL_SERVER CODER_MANAGED_REPOS CODER_GIT_CREDS GIT_ASKPASS SSH_ASKPASS 2>/dev/null || true
rm -f "$HOME/.git-credentials" 2>/dev/null || true
rm -rf "$HOME/.cache/coder-git-credentials" 2>/dev/null || true
git config --global --unset-all credential.helper 2>/dev/null || true

# --- Prepare the build directory in the toolchain cache ---------------------

[ -d "$SCAFFOLD_SRC" ] || die "Docs scaffold is not mounted at $SCAFFOLD_SRC"
mkdir -p "$CACHE_ROOT" || die "Docs build cache $CACHE_ROOT is not writable"

# Per-repo cache subdirectory keyed by the scaffold lockfile hash. The repo
# name is its own path segment (never a glob prefix), so pruning one repo's
# stale caches can never touch another repo's directory, and concurrent
# publishes of different repos stay fully isolated.
LOCK_HASH="$(sha256sum "$SCAFFOLD_SRC/package-lock.json" | cut -c1-16)"
REPO_CACHE_DIR="$CACHE_ROOT/repos/$DOCS_REPO_NAME"
BUILD_ROOT="$REPO_CACHE_DIR/$LOCK_HASH"
export npm_config_cache="$CACHE_ROOT/npm-cache"

# Drop this repo's cached build dirs from older scaffold versions.
mkdir -p "$REPO_CACHE_DIR"
for dir in "$REPO_CACHE_DIR"/*; do
    [ -d "$dir" ] || continue
    [ "$dir" = "$BUILD_ROOT" ] && continue
    log "Removing stale docs toolchain cache: $(basename "$dir")"
    rm -rf "$dir"
done

mkdir -p "$BUILD_ROOT"
cp -a "$SCAFFOLD_SRC/." "$BUILD_ROOT/"
rm -rf "$BUILD_ROOT/docs" "$BUILD_ROOT/build" "$BUILD_ROOT/.docusaurus"

cd "$BUILD_ROOT"

if [ ! -f "$BUILD_ROOT/node_modules/.coderflow-install-ok" ]; then
    log "Installing the Docusaurus build toolchain (first publish only; later publishes reuse the cache)..."
    npm ci --no-audit --no-fund || die "Failed to install the Docusaurus build toolchain. Check network access from task containers."
    touch "$BUILD_ROOT/node_modules/.coderflow-install-ok"
    log "Toolchain installed."
else
    log "Reusing cached Docusaurus toolchain."
fi

# --- Stage the docs content -------------------------------------------------

mkdir -p "$BUILD_ROOT/docs"
cp -a "$DOCS_SOURCE/." "$BUILD_ROOT/docs/"

# Docusaurus needs a doc at the site root; generate a landing page ONLY when
# the docs folder provides none of the recognized root docs.
HAS_ROOT_DOC=""
for candidate in index.md index.mdx README.md README.mdx; do
    if [ -f "$BUILD_ROOT/docs/$candidate" ]; then
        HAS_ROOT_DOC=1
        break
    fi
done
if [ -z "$HAS_ROOT_DOC" ]; then
    log "No index.md or README.md at the docs root; generating a landing page."
    cat > "$BUILD_ROOT/docs/index.md" <<EOF
---
title: "${DOCS_SITE_TITLE:-$DOCS_REPO_NAME}"
slug: /
---

Welcome to the ${DOCS_SITE_TITLE:-$DOCS_REPO_NAME} documentation. Use the sidebar to browse pages, or the search box to find a topic.
EOF
fi

# --- Build ------------------------------------------------------------------

log "Building the docs site (base URL: $DOCS_BASE_URL)..."
rm -rf /task-output/site
export DOCS_SITE_TITLE="${DOCS_SITE_TITLE:-$DOCS_REPO_NAME}"
export DOCS_BASE_URL
if ! ./node_modules/.bin/docusaurus build --out-dir /task-output/site; then
    die "Docusaurus build failed. Review the log above for the first error; it usually points at a specific markdown file."
fi

[ -f /task-output/site/index.html ] || die "Build finished but produced no site output."

write_manifest "built" "$SHA"
log "Docs site built successfully for $DOCS_REPO_NAME@${SHA:0:12}."
exit 0
