#!/usr/bin/env bash
# scripts/deploy/deploy-service.sh — deploy exactly ONE service on the local node.
#
# Extracted from deploy-node.sh so that the human whole-node deploy and the
# agent-driven single-service redeploy (services/deploy-runner/) share ONE
# compose invocation. That sharing is the whole point: docker compose derives
# the PROJECT NAME from the directory of the first -f file (verified:
# `docker compose -f services/node-agent/docker-compose.yml config` → name
# "node-agent", independent of cwd; adding --project-directory changes it).
# A caller that assembles the -f arguments differently lands in a DIFFERENT
# project, where `--remove-orphans` tears down the already-running stack —
# exactly what wiped the control-plane on VPS on 2026-06-25. Every caller goes
# through here.
#
# Usage:
#   deploy-service.sh --repo <repo-path> --host-dir <hosts/<node> path>
#                     --service <name>
#                     [--build-if-needed] [--force-recreate] [--remove-orphans]
#
# Flags:
#   --build-if-needed  add --build when the service ships its own Dockerfile
#                      (whole-node deploy path — see the rationale below)
#   --force-recreate   recreate containers even when config is unchanged
#                      (remediation path: plain `up -d` is a no-op for an
#                      unhealthy-but-unchanged service, i.e. it would "succeed"
#                      without doing anything)
#   --remove-orphans   human whole-node deploy only; never used by the agent
#                      path, where a project-name mismatch must not be able to
#                      delete containers
#
# Exit codes:
#   0  deployed
#   2  usage / validation error
#   3  service owns its deploy path (services/<svc>/deploy-local.sh) — skipped
#   *  docker compose exit code

set -euo pipefail

REPO_PATH=""
HOST_DIR=""
SERVICE=""
BUILD_IF_NEEDED=false
FORCE_RECREATE=false
REMOVE_ORPHANS=false

usage() {
    sed -n '2,30p' "${BASH_SOURCE[0]}" >&2
    exit 2
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --repo)            REPO_PATH="${2:-}"; shift 2 ;;
        --host-dir)        HOST_DIR="${2:-}"; shift 2 ;;
        --service)         SERVICE="${2:-}"; shift 2 ;;
        --build-if-needed) BUILD_IF_NEEDED=true; shift ;;
        --force-recreate)  FORCE_RECREATE=true; shift ;;
        --remove-orphans)  REMOVE_ORPHANS=true; shift ;;
        -h|--help)         usage ;;
        *) echo "deploy-service.sh: unknown argument: $1" >&2; usage ;;
    esac
done

[[ -n "$REPO_PATH" && -n "$HOST_DIR" && -n "$SERVICE" ]] || {
    echo "deploy-service.sh: --repo, --host-dir and --service are required" >&2
    usage
}

# Service names are used to build filesystem paths. Reject anything that is not
# a plain kebab-case service name so a malformed (or maliciously crafted)
# dispatched action can never escape services/ via ../.
if [[ ! "$SERVICE" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]]; then
    echo "deploy-service.sh: invalid service name: '${SERVICE}'" >&2
    exit 2
fi

[[ -d "$REPO_PATH" ]] || { echo "deploy-service.sh: repo not found: ${REPO_PATH}" >&2; exit 2; }
[[ -d "$HOST_DIR" ]]  || { echo "deploy-service.sh: host dir not found: ${HOST_DIR}" >&2; exit 2; }

SERVICE_DIR="${REPO_PATH}/services/${SERVICE}"
COMPOSE_FILE="${SERVICE_DIR}/docker-compose.yml"

if [[ ! -f "$COMPOSE_FILE" ]]; then
    echo "deploy-service.sh: compose file not found for ${SERVICE} at ${COMPOSE_FILE}" >&2
    exit 2
fi

# Services that ship their own deploy-local.sh have a dedicated, orchestrated
# deploy path (e.g. control-plane via deploy-control-plane.sh → deploy-local.sh)
# which does host-level preparation (dirs, ownership, sudo) this script cannot
# do. Deploying them from here would also run `up` against a stack whose own
# path expects to own it. Their own deploy path owns them; skip.
if [[ -f "${SERVICE_DIR}/deploy-local.sh" ]]; then
    echo "Skipping ${SERVICE}: ma własną ścieżkę deployu (services/${SERVICE}/deploy-local.sh), pomijam"
    exit 3
fi

COMPOSE_ARGS=(docker compose -f "$COMPOSE_FILE")

OVERRIDE_FILE="${HOST_DIR}/runtime/${SERVICE}/docker-compose.override.yml"
if [[ -f "$OVERRIDE_FILE" ]]; then
    echo "Using override file for ${SERVICE}"
    COMPOSE_ARGS+=(-f "$OVERRIDE_FILE")
fi

ENV_FILE="${SERVICE_DIR}/.env"
if [[ -f "$ENV_FILE" ]]; then
    COMPOSE_ARGS+=(--env-file "$ENV_FILE")
fi

COMPOSE_ARGS+=(up -d)

# Services with their own Dockerfile need a rebuild on a whole-node deploy —
# otherwise `up -d` sees "container running, image tag unchanged" and skips
# rebuilding even when src/ changed, silently leaving the old code running
# (backlog c858dbc). The agent redeploy path deliberately does NOT pass
# --build-if-needed: a redeploy is a reconcile, not a code ship, and building
# on the 4 GiB VPS mid-incident is an OOM risk.
if [[ "$BUILD_IF_NEEDED" == "true" && -f "${SERVICE_DIR}/Dockerfile" ]]; then
    echo "Building ${SERVICE} (has Dockerfile)"
    COMPOSE_ARGS+=(--build)
fi

if [[ "$FORCE_RECREATE" == "true" ]]; then
    COMPOSE_ARGS+=(--force-recreate)
fi

if [[ "$REMOVE_ORPHANS" == "true" ]]; then
    COMPOSE_ARGS+=(--remove-orphans)
fi

echo "Deploying service: ${SERVICE}..."
echo "+ ${COMPOSE_ARGS[*]}"
"${COMPOSE_ARGS[@]}"
