#!/usr/bin/env bash
# ============================================================================
# CakePHP container entrypoint — fail-fast preflight, then Apache.
#
# Every check here exists because the corresponding failure is SILENT in this
# application. The app is built to fail open (RAG degrades quietly, python
# errors are swallowed by `2>/dev/null`, paths.php falls through to a default
# database rather than erroring). That is the right behaviour for a request,
# and the wrong behaviour for a container start: a container that boots into a
# degraded state looks healthy and serves wrong answers. So the degraded states
# are detected once, here, loudly.
# ============================================================================
set -euo pipefail

APP_ROOT=/var/www/html/trakop-web/code
log()  { printf '[entrypoint] %s\n' "$*"; }
fail() { printf '[entrypoint] FATAL: %s\n' "$*" >&2; exit 1; }

# ---------------------------------------------------------------------------
# 1. Mount path. paths.php resolves the environment from getcwd(); the trailing
#    slash in "trakop-web/code/" only appears when the CWD is .../code/webroot.
#    A mount at any other path silently selects DBNAME=27215_vendor.
# ---------------------------------------------------------------------------
[ -d "$APP_ROOT/webroot" ] || fail "source not mounted at $APP_ROOT (webroot/ missing).
  The mount path is load-bearing: config/paths.php derives the environment from
  getcwd() and any other path selects the wrong database."
[ -f "$APP_ROOT/config/paths.php" ] || fail "config/paths.php missing from the mount.
  It is not baked into the image on purpose (it holds every credential); it must
  arrive through the bind mount."
[ -d "$APP_ROOT/vendor/cakephp/cakephp" ] || fail "vendor/ missing from the mount.
  vendor/ is COMMITTED at 3.5.17 and must not be reinstalled: composer.lock pins
  3.4.6 and installing it downgrades the framework so every page 500s."

# ---------------------------------------------------------------------------
# 2. THE 172.31 COLLISION GUARD — the most dangerous failure in this migration.
#
#    config/paths.php and nodeServer/server.js both select the PRODUCTION
#    database by matching the server's own IP against these addresses. Docker's
#    default bridge address pool is 172.17.0.0/16 .. 172.31.0.0/16, which
#    OVERLAPS them. A container that happens to draw 172.31.43.197 connects a
#    developer's laptop to production MySQL and flips CRON_URL to the live cron
#    host — with no error and no log line.
#
#    Refusing to start is the only safe response. If you are legitimately
#    deploying ON one of these hosts with host networking, the container shares
#    the host's address and this check must be waived explicitly, which is why
#    the override is a named variable and not a silent default.
#
#    RFC1918 ONLY — do not add public addresses such as the Contabo hosts. The
#    hazard being guarded is Docker handing a container a bridge address that
#    collides with a production host, and Docker can never hand out a public
#    address. This service is network_mode:host on Contabo, so `hostname -I`
#    there DOES return the public IP: listing it would make the container
#    refuse to start, and TRAKOP_ALLOW_PRODUCTION_IP is declared on the flask
#    service only, so there would be no way to disarm it.
# ---------------------------------------------------------------------------
PROD_IPS="172.31.43.197 172.31.42.66 172.31.33.124 172.31.12.223 172.31.15.66 172.31.5.127"
MY_IPS="$(hostname -I 2>/dev/null || true)"
for prod in $PROD_IPS; do
    for mine in $MY_IPS; do
        if [ "$prod" = "$mine" ]; then
            if [ "${TRAKOP_ALLOW_PRODUCTION_IP:-0}" = "1" ]; then
                log "WARNING: this container holds production address $mine and TRAKOP_ALLOW_PRODUCTION_IP=1 was set. paths.php will select the PRODUCTION database."
            else
                fail "this container holds the production address $mine.
  config/paths.php maps that address to a PRODUCTION database. If this is not a
  production host, the container drew the address from Docker's default bridge
  pool (172.17-172.31) — pin default-address-pools in /etc/docker/daemon.json to
  a range outside 172.16/12 and recreate the network.
  If this genuinely IS the production host, set TRAKOP_ALLOW_PRODUCTION_IP=1."
            fi
        fi
    done
done

# ---------------------------------------------------------------------------
# 3. STUB-GATEWAY ISOLATION. A local LLM test double once reached real users
#    because a php-fpm pool exported TRAKOPLENS_AI_ENDPOINT at 127.0.0.1:8899.
#    The application-side guard makes that override CLI-only, but an env var
#    pointing the live model at a stub has no legitimate use in a server
#    container, so it is refused rather than ignored.
# ---------------------------------------------------------------------------
if [ -n "${TRAKOPLENS_AI_ENDPOINT:-}" ]; then
    fail "TRAKOPLENS_AI_ENDPOINT is set in a server container ('${TRAKOPLENS_AI_ENDPOINT}').
  That variable exists only to point the CLI test harness at a local stub. A
  stub answering real users is exactly the incident this guard prevents. Remove
  it from the compose environment / fpm pool."
fi

# ---------------------------------------------------------------------------
# 4. Python runtime. exec('python3 ... 2>/dev/null') swallows ImportError, so a
#    missing module surfaces only as `tool_failed` with no message. Assert the
#    imports once at boot instead of discovering it from a user's question.
# ---------------------------------------------------------------------------
python3 -c "import pymysql, pandas, openpyxl, xlsxwriter, requests, \
googleapiclient.discovery, googleapiclient.http, google_auth_oauthlib.flow, \
google.oauth2.credentials, google.auth.transport.requests" 2>/dev/null \
    || fail "python3 dependency import failed. Delivery tools shell out with
  2>/dev/null, so this would appear only as tool_failed. Rebuild the image."

# ---------------------------------------------------------------------------
# 5. Writable runtime directories. These are bind-mounted from the host, where
#    the uid is a domain account (777201176) and in-container Apache is
#    www-data (33). Missing/unwritable dirs break sessions, logs and the
#    per-vendor file caches (masterConfig_*.txt, timezone_*.txt).
#    tmp_img, fastpayfiles and invoices are written by the app but created by
#    nothing (no mkdir anywhere) and tracked by nothing, so a fresh checkout
#    silently loses those writes until they are made here.
# ---------------------------------------------------------------------------
for d in "$APP_ROOT/tmp" "$APP_ROOT/tmp/cache" "$APP_ROOT/tmp/cache/models" \
         "$APP_ROOT/tmp/cache/persistent" "$APP_ROOT/tmp/sessions" \
         "$APP_ROOT/logs" "$APP_ROOT/webroot/cache_files" \
         "$APP_ROOT/webroot/tmp_img" "$APP_ROOT/webroot/fastpayfiles" \
         "$APP_ROOT/webroot/invoices"; do
    mkdir -p "$d" 2>/dev/null || true
    if [ ! -w "$d" ]; then
        fail "$d is not writable by uid $(id -u).
  Set the compose service's user: to the host owner's uid:gid, or chmod the
  directory on the host. Sessions, logs and the per-vendor file caches all
  write here."
    fi
done

# A WARNING, never fail: a broken renderer breaks receipts and invoice PDFs, and
# must not take the whole site down with it. Mirrors the probe order in
# config/app_identity.php — if that finds nothing the app falls back to the
# historical AWS path and every render dies with exit 127.
if ! command -v wkhtmltoimage >/dev/null 2>&1 && [ ! -x /opt/wkhtmltox/bin/wkhtmltoimage ]; then
    log "WARNING: no wkhtmltoimage on PATH or at /opt/wkhtmltox/bin — delivery-receipt
  PNGs and invoice PDFs will fail. The image installs it; this means the layer is
  missing or an /opt bind mount is shadowing it. See config/app_identity.php."
fi

# ---------------------------------------------------------------------------
# 5b. APACHE LISTEN PORT, applied at RUNTIME rather than baked at build time.
#
#     The port was originally a build ARG, which meant switching between
#     "alongside host Apache on 8081" and "owning :80" required rebuilding the
#     image. That coupling is wrong: the same artefact must be able to serve
#     either mode, or a cutover becomes a rebuild and a rollback becomes another
#     one. Rewritten here from $APACHE_PORT so one image covers both.
#
#     Note there is NO `ports:` mapping in docker-compose.yml for this service —
#     it uses network_mode: host, so Apache binds the host's port directly and
#     this variable IS the published port. `docker port` will therefore show
#     nothing for this container; that is expected, not a fault.
# ---------------------------------------------------------------------------
APACHE_PORT="${APACHE_PORT:-8081}"
case "$APACHE_PORT" in
    ''|*[!0-9]*) fail "APACHE_PORT must be numeric, got '${APACHE_PORT}'" ;;
esac
sed -ri "s/^Listen [0-9]+$/Listen ${APACHE_PORT}/" /etc/apache2/ports.conf
sed -ri "s/<VirtualHost \*:[0-9]+>/<VirtualHost *:${APACHE_PORT}>/" \
    /etc/apache2/sites-available/000-default.conf
log "apache will listen on :${APACHE_PORT} (host network — this is the published port)"

# Stamp the container id into the X-Trakop-Container response header by
# SUBSTITUTION, not Apache ${} interpolation — see the note in the vhost.
# `|| true` is REQUIRED, not defensive noise. This script runs under
# `set -euo pipefail`, and on cgroup v2 /proc/self/cgroup is just "0::/" with no
# 64-hex container id — so grep exits 1, pipefail propagates it, and the
# assignment kills the entrypoint. It did: the container restart-looped having
# logged only the listen port, with NO error message anywhere, because `set -e`
# exits silently. Under host networking there was no port-binding symptom either.
# Source the id from /proc/self/mountinfo, not /proc/self/cgroup: under cgroup v2
# the cgroup file is just "0::/" with no id, and `hostname` is the HOST's name
# because network_mode: host shares the UTS namespace. mountinfo still carries
# /var/lib/docker/containers/<64hex>/ from the /etc/hosts bind, and that id
# matches `docker inspect -f '{{.Id}}'` — verified.
TRAKOP_CONTAINER_ID="$( { grep -oE '/containers/[0-9a-f]{64}' /proc/self/mountinfo 2>/dev/null || true; } \
    | head -1 | grep -oE '[0-9a-f]{64}' | cut -c1-12)"
[ -n "$TRAKOP_CONTAINER_ID" ] || TRAKOP_CONTAINER_ID="unknown-$(hostname)"
# Replace the whole directive line, so this is idempotent no matter whether the
# file currently holds the build-time placeholder or a previous container's id.
sed -ri "s|^([[:space:]]*Header always set X-Trakop-Container ).*|\\1\"${TRAKOP_CONTAINER_ID}\"|" \
    /etc/apache2/sites-available/000-default.conf
log "runtime identity header: X-Trakop-Runtime: docker / X-Trakop-Container: ${TRAKOP_CONTAINER_ID}"

# Refuse to start if something else already holds the port, rather than letting
# Apache fail with a bare "Address already in use" buried in its own log. The
# usual cause during a cutover is host Apache still running on :80.
if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE "[^0-9]${APACHE_PORT}\$|:${APACHE_PORT} "; then
    fail "port ${APACHE_PORT} is already in use on this host.
  During a cutover to :80 this almost always means host Apache is still running:
      sudo systemctl stop apache2
  Use docker/scripts/start-docker.sh, which handles the handover in order."
fi

# ---------------------------------------------------------------------------
# 5c. URL ALIASES. The mount target is fixed at /var/www/html/trakop-web because
#     check 1 above makes it load-bearing, so the folder name on the host —
#     trakop-web-docker, trakop-web-native — never appears in the URL. In host
#     mode Apache serves the real /var/www/html and the folder name IS the URL,
#     so the same checkout answers on two different paths depending on which
#     runtime owns :80, and the path a developer types from memory 404s. Both
#     runtimes report `Server: Apache/2.4.54 (Debian)`, so that 404 page gives no
#     hint which one produced it.
#
#     A 301 REDIRECT, NOT A SECOND WAY TO SERVE THE SAME TREE. The first version
#     of this used a symlink so both paths served the app directly. That produced
#     an INFINITE LOGIN LOOP, and it is worth recording exactly why, because a
#     symlink looks harmless:
#
#       * The app builds absolute URLs from HTTP_ROOT, which comes from getcwd().
#         getcwd() never returns a path containing a symlink, so it always read
#         "trakop-web/code/" no matter which URL the browser used.
#       * So a page fetched at /trakop-web-docker/code/ carried a login form
#         posting to /trakop-web/code/users/login.
#       * That POST succeeded and set the CAKEPHP session cookie with
#         path=/trakop-web/code/ — cookie paths are exact prefixes, and
#         "/trakop-web/code/" is NOT a prefix of "/trakop-web-docker/code/".
#       * Auth then redirected back to the Referer, /trakop-web-docker/code/,
#         where the browser correctly declined to send that cookie.
#       * Arrive unauthenticated → login form → repeat, with correct credentials
#         and no error message anywhere. Two URLs meant two sessions.
#
#     A redirect removes the second session instead of trying to synchronise it:
#     the browser is moved onto the canonical path BEFORE it is issued a cookie,
#     so exactly one origin path exists for the whole session. The convenience
#     (typing the folder name works) is kept; the split is not.
#
#     RedirectMatch, not Alias: it resolves during URL mapping, so code/.htaccess
#     — which rewrites with a relative substitution (`webroot/$1`) and no
#     RewriteBase — never sees a prefix it cannot map back, and no tracked
#     application file has to change for host mode's sake.
# ---------------------------------------------------------------------------
VHOST=/etc/apache2/sites-available/000-default.conf
# Idempotent: strip anything a previous container start added before re-adding,
# so restarts cannot accumulate duplicate directives. Two patterns because the
# marker has to sit on its OWN line — Apache has no trailing-comment syntax, so
# `RedirectMatch ... # marker` is parsed as a fourth argument and configtest
# fails with "takes two or three arguments" (observed: restart loop, :80 down).
sed -ri '/# TRAKOP_URL_ALIAS/d' "$VHOST"
sed -ri '/RedirectMatch 301 \^\/[a-zA-Z0-9._-]+\(\/\.\*\)\?\$ \/trakop-web\$1/d' "$VHOST"
for url_alias in ${TRAKOP_URL_ALIASES:-}; do
    case "$url_alias" in
        trakop-web) fail "TRAKOP_URL_ALIASES may not contain 'trakop-web' — that is the canonical path, not an alias." ;;
        *[!a-zA-Z0-9._-]*|''|.|..) fail "TRAKOP_URL_ALIASES entry '${url_alias}' is not a bare directory name." ;;
    esac
    # Remove a symlink left behind by the earlier symlink-based implementation:
    # DocumentRoot lookup would otherwise still serve it for paths the redirect
    # does not cover, resurrecting the two-session split.
    [ -L "/var/www/html/${url_alias}" ] && rm -f "/var/www/html/${url_alias}"
    # Insert BEFORE the closing tag with sed's `i` command. Not the `e` flag,
    # which executes the pattern space as a shell command — that was tried, and
    # it corrupted the vhost into a restart loop that took :80 down.
    sed -i "\|</VirtualHost>|i \\    RedirectMatch 301 ^/${url_alias}(/.*)?\$ /trakop-web\$1" "$VHOST"
    sed -i "\|</VirtualHost>|i \\    # TRAKOP_URL_ALIAS ${url_alias}" "$VHOST"
    log "url alias: /${url_alias}/... -> 301 -> /trakop-web/... (one canonical path, one session)"
done
# Refuse to hand a broken vhost to Apache: a config error here restart-loops the
# container, and with host networking there is no port symptom to notice it by.
apache2ctl configtest >/dev/null 2>&1 \
    || fail "the alias redirect produced an invalid Apache config:
$(apache2ctl configtest 2>&1 | head -5)"

# ---------------------------------------------------------------------------
# 6. RAG configuration sanity. Every retriever fails OPEN and silently: with a
#    missing password or an unreachable store, KB grounding, tool narrowing and
#    walkthrough routing all disappear with no error. Report the resolved state
#    at boot so "RAG has been dead for a week" is not a discovery.
#    NOT fatal — an intentionally RAG-less server is a valid deployment.
# ---------------------------------------------------------------------------
php -r '
$_SERVER["REQUEST_URI"]="/"; $_SERVER["HTTP_HOST"]="localhost";
$_SERVER["SERVER_ADDR"]="127.0.0.1"; $_SERVER["SERVER_NAME"]="localhost";
chdir("/var/www/html/trakop-web/code/webroot");
require "/var/www/html/trakop-web/code/config/paths.php";
printf("[entrypoint] db=%s@%s  rag=%s  embed=%s/%dd  pg=%s  cache=%s  corpus=%s%s\n",
    DBNAME, DBHOST, TRAKOPLENS_RAG_MODE, TRAKOPLENS_EMBED_MODEL, TRAKOPLENS_EMBED_DIM,
    preg_replace("/^pgsql:/","",TRAKOPLENS_PG_DSN),
    TRAKOPLENS_CACHE_ENGINE === "redis" && extension_loaded("redis") ? "redis" : "file",
    TRAKOPLENS_KB_CORPUS_VERSION,
    is_file(TRAKOPLENS_KB_SNAPSHOT) ? "" : "  [WARNING: KB snapshot missing -> faq corpus will be empty]");
if (TRAKOPLENS_RAG_MODE === "active" && !TRAKOPLENS_PG_PASS) {
    fwrite(STDERR, "[entrypoint] WARNING: RAG_MODE=active but TRAKOPLENS_PG_PASS is empty -> RAG is off in practice.\n");
}
' 2>&1 | grep -v '^PHP \(Notice\|Warning\|Deprecated\)' || true

log "preflight passed; starting: $*"
exec "$@"
