#!/usr/bin/env bash
#
# panama-server – the compose services a server machine runs.
#
# The repository carries the catalog: server/containers/<Name>/ holds a
# compose.yml, a podman-<name>.service user unit, and an .env.example naming
# what the service needs told. The machine carries the rest: ~/Server/<Name>/
# is a real directory owning the .env (secrets never sit inside the checkout,
# gitignored or not) and the bind-mounted data. Only the tracked files are
# symlinked, so a `git clean` in the repo can never reach a database.
#
#   list             every service in the catalog, with its state here
#   enable <Name>    link it into ~/Server, seed .env, enable the unit
#   disable <Name>   stop it and remove the unit; data and .env stay put
#   status [<Name>]  what is actually running
#   relink           refresh the symlinks for everything enabled here
#
# Verbs act on the unit and the links, never on data: there is deliberately no
# verb here that deletes ~/Server/<Name> or anything in it.

set -uo pipefail

PROGRAM="panama server"

SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)

# Overridable so the contract can point this at a fixture catalog and a
# throwaway HOME rather than the machine's real services.
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/containers}"
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"

if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
  BOLD=$(tput bold); RESET=$(tput sgr0)
  RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); BLUE=$(tput setaf 4)
else
  BOLD=""; RESET=""; RED=""; GREEN=""; YELLOW=""; BLUE=""
fi

info()  { printf '%s==>%s %s\n'  "${BLUE}${BOLD}"   "$RESET" "$*"; }
ok()    { printf '%s✓%s %s\n'    "${GREEN}${BOLD}"  "$RESET" "$*"; }
warn()  { printf '%s!%s %s\n'    "${YELLOW}${BOLD}" "$RESET" "$*"; }
err()   { printf '%s✗%s %s\n'    "${RED}${BOLD}"    "$RESET" "$*" >&2; }

usage() {
  cat <<EOF
${BOLD}$PROGRAM${RESET} – manage the compose services in server/containers/

${BOLD}Usage:${RESET}
  $PROGRAM list             Every service in the catalog, and its state here
  $PROGRAM enable <Name>    Link a service into ~/Server, seed its .env from
                            .env.example, and enable its unit. Stops short of
                            starting when the .env still holds CHANGE_ME
                            placeholders, and says which.
  $PROGRAM disable <Name>   Stop the unit and unlink it. ~/Server/<Name>,
                            its .env and its data are never touched.
  $PROGRAM status [<Name>]  What is actually running, per service
  $PROGRAM relink           Refresh every enabled service's symlinks after a
                            pull, and name the ones whose definitions changed
                            (nothing is restarted for you)
EOF
}

# ── The catalog ──────────────────────────────────────────────────────────────

services() {
  local dir
  for dir in "$SERVER_DIR"/*/; do
    [[ -d "$dir" ]] || continue
    basename "$dir"
  done
}

require_service() {
  local name="$1"
  if [[ ! -d "$SERVER_DIR/$name" ]]; then
    err "No such service: '$name'"
    printf 'The catalog has:\n' >&2
    services | sed 's/^/  /' >&2
    exit 1
  fi
}

# The one unit file a service directory carries. Its filename is its identity
# -- podman-<name>.service, exactly what lands in the systemd user directory --
# so nothing here invents a name that could drift from the file's.
unit_path_for() {
  local name="$1" unit
  for unit in "$SERVER_DIR/$name"/*.service; do
    [[ -e "$unit" ]] || continue
    printf '%s' "$unit"
    return 0
  done
  return 1
}

unit_name_for() {
  local unit
  unit="$(unit_path_for "$1")" || return 1
  basename "$unit"
}

# Installed means our symlink is in the systemd user directory. A regular file
# of the same name is a pre-Panama install done by hand -- reported, never
# silently replaced.
unit_installed() {
  local unit_name
  unit_name="$(unit_name_for "$1")" || return 1
  [[ -L "$UNIT_DIR/$unit_name" ]]
}

definition_hash() {
  local name="$1" unit
  unit="$(unit_path_for "$name")" || unit=/dev/null
  cat "$SERVER_DIR/$name/compose.yml" "$unit" 2>/dev/null | sha256sum | cut -d' ' -f1
}

recorded_hash() {
  [[ -r "$STATE_FILE" ]] || return 0
  awk -v name="$1" '$1 == name {print $2}' "$STATE_FILE"
}

record_hash() {
  local name="$1" hash="$2"
  mkdir -p "$(dirname "$STATE_FILE")"
  { [[ -r "$STATE_FILE" ]] && awk -v name="$name" '$1 != name' "$STATE_FILE"; \
    printf '%s %s\n' "$name" "$hash"; } >"$STATE_FILE.tmp"
  mv "$STATE_FILE.tmp" "$STATE_FILE"
}

# Replace path with a symlink to src. An existing regular file is moved aside
# with its name intact plus a suffix, because the only regular file that can be
# here is a pre-Panama one somebody wrote by hand -- worth keeping to diff.
link_file() {
  local src="$1" path="$2"
  if [[ -L "$path" ]]; then
    [[ "$(readlink -f "$path")" == "$(readlink -f "$src")" ]] && return 0
    rm "$path"
  elif [[ -e "$path" ]]; then
    mv "$path" "$path.pre-panama"
    warn "Kept the existing $(basename "$path") as $(basename "$path").pre-panama"
  fi
  ln -s "$src" "$path"
}

# ── Verbs ────────────────────────────────────────────────────────────────────

cmd_list() {
  local name unit_name state enabled
  for name in $(services); do
    unit_name="$(unit_name_for "$name")" || { printf '%-24s %s\n' "$name" "no unit file"; continue; }
    if unit_installed "$name"; then
      state="$(systemctl --user is-active "$unit_name" 2>/dev/null || true)"
      enabled="enabled"
      [[ "$state" == active ]] && state="${GREEN}active${RESET}" || state="${YELLOW}${state:-unknown}${RESET}"
      printf '%-24s %s, %b\n' "$name" "$enabled" "$state"
    elif [[ -e "$UNIT_DIR/$unit_name" ]]; then
      printf '%-24s %s\n' "$name" "installed by hand (not Panama's symlink)"
    else
      printf '%-24s %s\n' "$name" "-"
    fi
  done
}

cmd_enable() {
  local name="${1:-}"
  [[ -n "$name" ]] || { err "Which service? Usage: $PROGRAM enable <Name>"; exit 1; }
  require_service "$name"

  local repo_dir="$SERVER_DIR/$name" live_dir="$TARGET_DIR/$name"
  [[ -f "$repo_dir/compose.yml" ]] || { err "$name has no compose.yml in the catalog"; exit 1; }
  local unit_path unit_name
  unit_path="$(unit_path_for "$name")" || { err "$name has no unit file in the catalog"; exit 1; }
  unit_name="$(basename "$unit_path")"

  mkdir -p "$live_dir"
  link_file "$repo_dir/compose.yml" "$live_dir/compose.yml"
  ok "Linked compose.yml → $live_dir/compose.yml"

  # The .env lives with the machine, not the checkout. Seeded once from the
  # example and never overwritten -- it is where the person's secrets go.
  if [[ -f "$repo_dir/.env.example" && ! -e "$live_dir/.env" ]]; then
    cp "$repo_dir/.env.example" "$live_dir/.env"
    chmod 600 "$live_dir/.env"
    ok "Seeded $live_dir/.env from .env.example"
  fi

  # Refusing to start on placeholders, and saying which: a service brought up
  # with CHANGE_ME as its database password does not fail loudly, it runs --
  # and what it runs is a service whose password is CHANGE_ME.
  if [[ -e "$live_dir/.env" ]] && grep -q 'CHANGE_ME' "$live_dir/.env"; then
    warn "$live_dir/.env still has placeholder values:"
    grep -n 'CHANGE_ME' "$live_dir/.env" | sed 's/^/    /'
    printf 'Fill them in, then run: %s enable %s\n' "$PROGRAM" "$name"
    exit 1
  fi

  mkdir -p "$UNIT_DIR"
  link_file "$unit_path" "$UNIT_DIR/$unit_name"
  systemctl --user daemon-reload
  if systemctl --user enable --now "$unit_name"; then
    record_hash "$name" "$(definition_hash "$name")"
    ok "$name enabled and started ($unit_name)"
  else
    err "$unit_name did not start; see: systemctl --user status $unit_name"
    exit 1
  fi
}

cmd_disable() {
  local name="${1:-}"
  [[ -n "$name" ]] || { err "Which service? Usage: $PROGRAM disable <Name>"; exit 1; }
  require_service "$name"
  local unit_name
  unit_name="$(unit_name_for "$name")" || { err "$name has no unit file in the catalog"; exit 1; }

  systemctl --user disable --now "$unit_name" 2>/dev/null || true
  if [[ -L "$UNIT_DIR/$unit_name" ]]; then
    rm "$UNIT_DIR/$unit_name"
    systemctl --user daemon-reload
  fi
  ok "$name disabled. ~/Server/$name, its .env and its data were not touched."
}

cmd_status() {
  local name="${1:-}"
  if [[ -n "$name" ]]; then
    require_service "$name"
    local unit_name
    unit_name="$(unit_name_for "$name")" || { err "$name has no unit file"; exit 1; }
    systemctl --user status --no-pager "$unit_name" || true
    if [[ -d "$TARGET_DIR/$name" ]]; then
      ( cd "$TARGET_DIR/$name" && podman compose ps 2>/dev/null ) || true
    fi
    return 0
  fi

  # The role's health summary: what is enabled here, and is it actually up.
  local any=0 unit_name state
  for name in $(services); do
    unit_installed "$name" || continue
    any=1
    unit_name="$(unit_name_for "$name")"
    state="$(systemctl --user is-active "$unit_name" 2>/dev/null || true)"
    if [[ "$state" == active ]]; then
      ok "$name"
    else
      err "$name is ${state:-unknown}"
    fi
  done
  (( any )) || info "No services enabled on this machine yet. See: $PROGRAM list"
  return 0
}

cmd_relink() {
  local name unit_path unit_name changed=() unmanaged=() refreshed=0
  for name in $(services); do
    unit_path="$(unit_path_for "$name")" || continue
    unit_name="$(basename "$unit_path")"

    if [[ -e "$UNIT_DIR/$unit_name" && ! -L "$UNIT_DIR/$unit_name" ]]; then
      unmanaged+=("$name")
      continue
    fi
    unit_installed "$name" || continue

    link_file "$unit_path" "$UNIT_DIR/$unit_name"
    mkdir -p "$TARGET_DIR/$name"
    link_file "$SERVER_DIR/$name/compose.yml" "$TARGET_DIR/$name/compose.yml"
    refreshed=$((refreshed + 1))

    # Changed since the last time this machine looked, which is what makes a
    # pull actionable: the restart is deliberately yours to run, so the least
    # this can do is say which services are running a definition that is no
    # longer what the repository says.
    local now
    now="$(definition_hash "$name")"
    if [[ "$(recorded_hash "$name")" != "$now" ]]; then
      changed+=("$name")
      record_hash "$name" "$now"
    fi
  done

  systemctl --user daemon-reload 2>/dev/null || true
  info "Relinked $refreshed enabled service(s)"
  if (( ${#unmanaged[@]} > 0 )); then
    warn "Installed by hand, left alone: ${unmanaged[*]}"
  fi
  if (( ${#changed[@]} > 0 )); then
    warn "Definitions changed; restart each when ready:"
    for name in "${changed[@]}"; do
      printf '    %s:  systemctl --user restart %s\n' "$name" "$(unit_name_for "$name")"
    done
  fi
}

# ── Dispatcher ───────────────────────────────────────────────────────────────

main() {
  if [[ ! -d "$SERVER_DIR" ]]; then
    err "No service catalog at $SERVER_DIR"
    exit 1
  fi
  local cmd="${1:-}"
  case "$cmd" in
    list)    shift; cmd_list "$@" ;;
    enable)  shift; cmd_enable "$@" ;;
    disable) shift; cmd_disable "$@" ;;
    status)  shift; cmd_status "$@" ;;
    relink)  shift; cmd_relink "$@" ;;
    help|-h|--help|"") usage ;;
    *)
      err "Unknown command: '$cmd'"
      echo
      usage
      exit 1
      ;;
  esac
}

main "$@"
