#!/usr/bin/env bash
set -Eeuo pipefail

kit_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly kit_dir
readonly env_file="${ENV_FILE:-$kit_dir/.env}"
readonly stack_root="/srv/gitea-security"
readonly gitea_root="$stack_root/gitea"
readonly openresty_root="$stack_root/openresty"
readonly openappsec_root="$stack_root/openappsec"
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
readonly timestamp
readonly backup_dir="$stack_root/backups/prepare-$timestamp"
rendered_dir="$(mktemp -d)"
readonly rendered_dir
bouncer_tmp="$(mktemp -d)"
readonly bouncer_tmp
success=false
gitea_started=false
appsec_started=false
openresty_started=false

fail() {
  printf 'error=%s\n' "$1" >&2
  exit 1
}

cleanup() {
  local exit_code=$?
  rm -rf "$rendered_dir" "$bouncer_tmp"
  if [[ "$success" != true ]]; then
    printf 'prepare=failed backup=%s\n' "$backup_dir" >&2
    if [[ "$openresty_started" == true ]]; then
      systemctl stop openresty >/dev/null 2>&1 || true
    fi
    if [[ "$appsec_started" == true ]]; then
      docker compose -f "$openappsec_root/compose.yaml" stop >/dev/null 2>&1 || true
    fi
    if [[ "$gitea_started" == true ]]; then
      docker compose -f "$gitea_root/compose.yaml" stop >/dev/null 2>&1 || true
    fi
  fi
  exit "$exit_code"
}
trap cleanup EXIT

wait_for() {
  local label=$1
  local timeout=$2
  shift 2
  local deadline=$((SECONDS + timeout))
  until "$@"; do
    (( SECONDS < deadline )) || fail "timeout waiting for $label"
    sleep 2
  done
}

container_healthy() {
  [[ "$(docker inspect --format '{{.State.Health.Status}}' "$1" 2>/dev/null)" == healthy ]]
}

crowdsec_ready() {
  systemctl is-active --quiet crowdsec \
    && curl -fsS --max-time 3 http://127.0.0.1:8080/health >/dev/null \
    && ss -lntH 'sport = :7422' | grep -F '127.0.0.1' >/dev/null
}

appsec_core_ready() {
  docker exec openappsec-agent sh -ceu '
    test -s /etc/cp/conf/waap/waap.policy
    pidof cp-nano-orchestration >/dev/null
    pidof cp-nano-attachment-registrator >/dev/null
    pidof cp-nano-agent-cache >/dev/null
  ' >/dev/null 2>&1
}

detect_policy_active() {
  local actual_version
  actual_version="$(sha256sum "$openappsec_root/local-policy/detect-learn.yaml" | awk '{ print $1 }')"
  [[ "$actual_version" == "$OPENAPPSEC_DETECT_POLICY_SHA256" ]] \
    && [[ -s "$openappsec_root/config/policy.json" ]] \
    && jq -e --arg expected "$OPENAPPSEC_DETECT_POLICY_SHA256" '
    .version == $expected and
    .waap.WAAP.WebApplicationSecurity[0].webAttackMitigationMode == "Learn" and
    .waap.WAAP.WebApplicationSecurity[0].webAttackMitigationSeverity == "Transparent" and
    .waap.WAAP.WebApplicationSecurity[0].webAttackMitigationAction == "Transparent" and
    .triggers.rulebase.log[0].logToAgent == true
  ' "$openappsec_root/config/policy.json" >/dev/null
}

[[ $(id -u) -eq 0 ]] || fail "run as root"
[[ -f "$env_file" ]] || fail "missing environment file: $env_file"
[[ ! -e "$stack_root/.prepared" ]] || fail "deployment already prepared"

set -a
# shellcheck source=/dev/null
source "$kit_dir/VERSION.env"
# shellcheck source=/dev/null
source "$env_file"
set +a

if grep -q 'CHANGE_ME' "$env_file"; then
  fail "replace every CHANGE_ME value in $env_file"
fi

for command in curl crowdsec cscli docker envsubst git jq openresty patch python3 realpath sha256sum ss systemctl tar; do
  command -v "$command" >/dev/null || fail "missing command: $command"
done
docker compose version >/dev/null
[[ "$GITEA_UID" =~ ^[0-9]+$ ]] || fail "GITEA_UID must be numeric"
[[ "$GITEA_GID" =~ ^[0-9]+$ ]] || fail "GITEA_GID must be numeric"
[[ "$GITEA_PROXY_CIDR" == 172.28.0.1/32 ]] || fail "GITEA_PROXY_CIDR must match the fixed Compose gateway"
[[ "$OPENRESTY_LOG_DIR" == /srv/gitea-security/openresty/logs ]] || fail "OPENRESTY_LOG_DIR must match the acquisition path"
[[ "$PUBLIC_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ && "$PUBLIC_REPOSITORY" != *.git ]] || fail "PUBLIC_REPOSITORY must be org/repository without .git"
BOUNCER_API_KEY=preflight-only ENV_FILE="$env_file" OUTPUT_DIR="$rendered_dir" "$kit_dir/scripts/render.sh" >/dev/null

# shellcheck source=/dev/null
source /etc/os-release
if [[ "${ID:-}" != ubuntu || "${VERSION_ID:-}" != 24.04 ]]; then
  [[ "${ALLOW_UNSUPPORTED_OS:-0}" == 1 ]] || fail "reference target is Ubuntu 24.04; set ALLOW_UNSUPPORTED_OS=1 only after compatibility testing"
fi
[[ "$(uname -m)" == aarch64 || "$(uname -m)" == arm64 ]] || fail "target must be ARM64"
[[ "$(nproc)" -ge 4 ]] || fail "at least four CPU cores are required"
awk '/MemTotal:/ { exit !($2 >= 1800000) }' /proc/meminfo || fail "at least 1.8 GiB RAM is required"
awk '/SwapTotal:/ { exit !($2 >= 1048576) }' /proc/meminfo || fail "at least 1 GiB swap is required"
[[ "$(df -Pk /srv | awk 'NR == 2 { print $4 }')" -ge 1048576 ]] || fail "at least 1 GiB free space is required before Git data"

openresty -v 2>&1 | grep -F 'openresty/1.31.1.1' >/dev/null || fail "OpenResty 1.31.1.1 is required"
crowdsec -version 2>&1 | grep -F 'v1.7.8' >/dev/null || fail "CrowdSec 1.7.8 is required"

for path in "$CERT_FULLCHAIN_FILE" "$CERT_PRIVATE_KEY_FILE" "$ADVANCED_MODEL_FILE" "$OPENAPPSEC_AGENT_ARCHIVE" "$OPENAPPSEC_ATTACHMENT_ARCHIVE"; do
  [[ -s "$path" ]] || fail "missing required artifact: $path"
done
printf '%s  %s\n' "$ADVANCED_MODEL_SHA256" "$ADVANCED_MODEL_FILE" | sha256sum -c -
printf '%s  %s\n' "$OPENAPPSEC_AGENT_ARCHIVE_SHA256" "$OPENAPPSEC_AGENT_ARCHIVE" | sha256sum -c -
printf '%s  %s\n' "$OPENAPPSEC_ATTACHMENT_ARCHIVE_SHA256" "$OPENAPPSEC_ATTACHMENT_ARCHIVE" | sha256sum -c -
tar -tzf "$ADVANCED_MODEL_FILE" | grep -F 'waap.data' >/dev/null || fail "Advanced Model archive does not contain waap.data"

install -d -m 0700 "$stack_root" "$backup_dir" "$stack_root/bin" "$stack_root/deploy-kit"
cp -a /etc/crowdsec "$backup_dir/crowdsec"
if [[ "$(realpath "$kit_dir")" != "$(realpath -m "$stack_root/deploy-kit")" ]]; then
  cp -a "$kit_dir/." "$stack_root/deploy-kit/"
fi
install -m 0600 "$env_file" "$stack_root/.env"
install -m 0644 "$kit_dir/VERSION.env" "$stack_root/VERSION.env"
install -m 0750 "$kit_dir/scripts/"*.sh "$stack_root/bin/"

printf 'phase=load-images\n'
docker load -i "$OPENAPPSEC_AGENT_ARCHIVE" >/dev/null
docker load -i "$OPENAPPSEC_ATTACHMENT_ARCHIVE" >/dev/null
for image in local/openappsec-agent:1.1.34-arm64 local/openappsec-nginx-attachment:1.1.34-arm64; do
  [[ "$(docker image inspect "$image" --format '{{.Architecture}}')" == arm64 ]] || fail "wrong architecture: $image"
done
[[ "$(docker image inspect local/openappsec-agent:1.1.34-arm64 --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" == "$OPENAPPSEC_COMMIT" ]] || fail "Agent source commit mismatch"
[[ "$(docker image inspect local/openappsec-nginx-attachment:1.1.34-arm64 --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" == "$ATTACHMENT_COMMIT" ]] || fail "Attachment source commit mismatch"

printf 'phase=stage-files\n'
install -d -m 0750 "$gitea_root/data" "$openresty_root/conf.d" "$openresty_root/certs" "$openresty_root/logs"
install -d -m 0750 "$openappsec_root/config" "$openappsec_root/data" "$openappsec_root/logs" "$openappsec_root/nginx" "$openappsec_root/nginx-logs" "$openappsec_root/local-policy" "$openappsec_root/model"
chown -R "$GITEA_UID:$GITEA_GID" "$gitea_root/data"
install -m 0644 "$kit_dir/gitea/compose.yaml" "$gitea_root/compose.yaml"
install -m 0644 "$kit_dir/openresty/nginx.conf" "$openresty_root/nginx.conf"
install -m 0644 "$kit_dir/openresty/conf.d/proxy-common.inc" "$openresty_root/conf.d/proxy-common.inc"
install -m 0644 "$CERT_FULLCHAIN_FILE" "$openresty_root/certs/fullchain.pem"
install -m 0600 "$CERT_PRIVATE_KEY_FILE" "$openresty_root/certs/private-key.pem"
cp -a "$kit_dir/openappsec/." "$openappsec_root/"
install -m 0644 "$kit_dir/openappsec/local-policy/detect-learn.yaml" "$openappsec_root/local-policy/local_policy.yaml"
install -m 0644 "$ADVANCED_MODEL_FILE" "$openappsec_root/model/open-appsec-advanced-model.tgz"

printf 'phase=configure-crowdsec\n'
cscli hub update >>"$stack_root/crowdsec-hub-install.log" 2>&1
for collection in crowdsecurity/nginx crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules; do
  cscli collections install "$collection" >>"$stack_root/crowdsec-hub-install.log" 2>&1
done
bouncer_name=gitea-openresty
if cscli bouncers list -o json | jq -e --arg name "$bouncer_name" 'any(.[]; (.name // .Name) == $name)' >/dev/null; then
  fail "CrowdSec bouncer already exists: $bouncer_name"
fi
BOUNCER_API_KEY="$(cscli bouncers add "$bouncer_name" -o raw)"
export BOUNCER_API_KEY
ENV_FILE="$env_file" OUTPUT_DIR="$rendered_dir" "$kit_dir/scripts/render.sh"

bouncer_url="https://github.com/crowdsecurity/cs-openresty-bouncer/releases/download/v${CROWDSEC_BOUNCER_VERSION}/crowdsec-openresty-bouncer.tgz"
curl --proto '=https' --tlsv1.2 -fsSL --retry 3 "$bouncer_url" -o "$bouncer_tmp/bouncer.tgz"
printf '%s  %s\n' "$CROWDSEC_BOUNCER_ARCHIVE_SHA256" "$bouncer_tmp/bouncer.tgz" | sha256sum -c -
tar -xzf "$bouncer_tmp/bouncer.tgz" -C "$bouncer_tmp"
bouncer_source="$bouncer_tmp/crowdsec-openresty-bouncer-v${CROWDSEC_BOUNCER_VERSION}"
install -d -m 0750 /etc/crowdsec/bouncers "$bouncer_tmp/nginx-conf"
install -m 0600 "$rendered_dir/crowdsec/bouncers/crowdsec-openresty-bouncer.conf" /etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf
(
  cd "$bouncer_source"
  ./install.sh -y \
    --NGINX_CONF_DIR="$bouncer_tmp/nginx-conf" \
    --LIB_PATH=/usr/local/openresty/lualib \
    --CONFIG_PATH=/etc/crowdsec/bouncers \
    --DATA_PATH=/var/lib/crowdsec/lua
)
patch --batch --forward /usr/local/openresty/lualib/crowdsec.lua < "$kit_dir/crowdsec/empty-appsec-error.patch"
install -m 0644 "$kit_dir/crowdsec/bouncers/crowdsec-openresty-bouncer.conf.local" /etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf.local
install -m 0644 "$rendered_dir/crowdsec/acquis.d/gitea-nginx.yaml" /etc/crowdsec/acquis.d/gitea-nginx.yaml
install -m 0644 "$kit_dir/crowdsec/acquis.d/gitea-appsec.yaml" /etc/crowdsec/acquis.d/gitea-appsec.yaml
install -d -m 0755 /etc/crowdsec/appsec-configs /etc/systemd/system/crowdsec.service.d
install -m 0644 "$kit_dir/crowdsec/appsec-configs/gitea.yaml" /etc/crowdsec/appsec-configs/gitea.yaml
install -m 0644 "$kit_dir/crowdsec/profiles.yaml" /etc/crowdsec/profiles.yaml
install -m 0644 "$kit_dir/crowdsec/systemd/resources.conf" /etc/systemd/system/crowdsec.service.d/resources.conf
crowdsec -t
systemctl daemon-reload
systemctl restart crowdsec
wait_for CrowdSec 180 crowdsec_ready

printf 'phase=start-gitea\n'
(
  cd "$gitea_root"
  docker compose up -d
)
gitea_started=true
wait_for Gitea 180 container_healthy gitea

printf 'phase=start-openappsec-core\n'
(
  cd "$openappsec_root"
  docker compose up -d appsec-agent
)
appsec_started=true
wait_for OpenAppSec-core 300 appsec_core_ready
wait_for OpenAppSec-detect-policy 240 detect_policy_active

printf 'phase=start-openappsec-attachment\n'
(
  cd "$openappsec_root"
  docker compose up -d appsec-nginx
)
wait_for OpenAppSec-agent 240 container_healthy openappsec-agent
wait_for OpenAppSec-nginx 120 container_healthy openappsec-nginx
agent_restarts="$(docker inspect --format '{{.RestartCount}}' openappsec-agent)"
nginx_restarts="$(docker inspect --format '{{.RestartCount}}' openappsec-nginx)"
sleep 30
[[ "$(docker inspect --format '{{.RestartCount}}' openappsec-agent)" == "$agent_restarts" ]] || fail "OpenAppSec Agent restarted during stability gate"
[[ "$(docker inspect --format '{{.RestartCount}}' openappsec-nginx)" == "$nginx_restarts" ]] || fail "OpenAppSec NGINX restarted during stability gate"

printf 'phase=start-openresty-direct\n'
install -m 0644 "$rendered_dir/openresty/conf.d/00-gitea-security.conf" "$openresty_root/conf.d/00-gitea-security.conf"
install -m 0644 "$rendered_dir/openresty/conf.d/01-crowdsec.conf" "$openresty_root/conf.d/01-crowdsec.conf"
install -m 0644 "$rendered_dir/openresty/conf.d/gitea.conf" "$openresty_root/conf.d/gitea.conf"
install -m 0644 "$kit_dir/openresty/systemd/openresty.service" /etc/systemd/system/openresty.service
install -m 0644 "$kit_dir/openresty/logrotate/openresty" /etc/logrotate.d/gitea-openresty
systemctl stop openresty >/dev/null 2>&1 || true
systemctl daemon-reload
openresty -t -c "$openresty_root/nginx.conf"
systemctl enable --now openresty
openresty_started=true
wait_for OpenResty 60 curl -fsS http://127.0.0.1:18081/healthz
curl --insecure --fail --silent --show-error --resolve "$GIT_DOMAIN:443:127.0.0.1" "https://$GIT_DOMAIN/" >/dev/null

touch "$stack_root/.prepared"
printf '%s\n' "$backup_dir" > "$stack_root/last-prepare-backup"
success=true
printf 'prepare=pass mode=detect-learn upstream=127.0.0.1:3000 backup=%s\n' "$backup_dir"
