#!/bin/bash
#
# Dieses Script installiert, deinstalliert, startet und stoppt podman-compose basierende Services.
#
# Beispiele:
#   oci enable caddy
#   oci disable caddy
#   oci start caddy
#   oci stop caddy
#   oci create caddy
#

set -euo pipefail

usage() {
  cat <<'USAGE'
Usage:
  oci <command> <service>

Commands:
  help                     Zeigt diese Hilfe
  create <service>         Legt /srv/<service> an, erstellt compose.yml und Unit-Datei
  enable <service>         Aktiviert den Dienst
  disable <service>        Deaktiviert den Dienst
  delete <service>         Deaktiviert den Dienst und loescht die Unit-Datei
  start <service>          Startet den Dienst
  stop <service>           Stoppt den Dienst
  status <service>         Zeigt den systemd Status
  log <service>            Folgt den Container-Logs
  pull <service>           Zieht die aktuellen Images (podman-compose pull)
  update <service>         Pullt Images und restartet bei Updates
  autoupdate <service>     Legt Update-Timer an und aktiviert ihn
  noautoupdate <service>   Deaktiviert Update-Timer und loescht ihn
  restart <service>        Startet den Dienst neu
  cat <service>            Gibt Service, Update-Timer und compose.yml aus
  info <service>           Zeigt Statusinformationen

Beispiele:
  oci create caddy
  oci enable caddy
  oci start caddy
  oci delete caddy
USAGE
}

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

require_root() {
  if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
    die "Dieses Kommando muss als root ausgefuehrt werden."
  fi
}

validate_service() {
  local name="$1"
  if [[ -z "$name" ]]; then
    die "Kein Service angegeben."
  fi
  if [[ ! "$name" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
    die "Ungueltiger Servicename: $name"
  fi
}

require_service_compose() {
  local name="$1"
  local service_dir="/srv/${name}"
  local compose_file="${service_dir}/compose.yml"

  if [[ ! -d "$service_dir" ]]; then
    die "Service-Verzeichnis fehlt: ${service_dir}"
  fi
  if [[ ! -f "$compose_file" ]]; then
    die "Compose-Datei fehlt: ${compose_file}"
  fi
}

create_service() {
  local name="$1"

  mkdir -p "/srv/${name}"
  if [[ ! -e "/srv/${name}/compose.yml" ]]; then
    cat <<'__COMPOSE__' > "/srv/${name}/compose.yml"
services:
  httpd:
    image: docker.io/library/httpd:latest
    restart: unless-stopped
    labels:
      - "io.containers.autoupdate=registry"
    network_mode: host
__COMPOSE__
  fi

  cat <<__UNIT__ > "/etc/systemd/system/${name}.service"
[Unit]
Description=podman-compose ${name} service
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/${name}
ExecStart=/usr/bin/podman-compose -f compose.yml up -d
ExecStop=/usr/bin/podman-compose -f compose.yml down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target
__UNIT__

  systemctl daemon-reload
}

get_compose_images() {
  local name="$1"
  local images=""

  images="$(cd "/srv/${name}" && /usr/bin/podman-compose -f compose.yml config --images 2>/dev/null || true)"
  if [[ -n "$images" ]]; then
    printf '%s\n' "$images" | awk 'NF {print $1}' | sort -u
    return 0
  fi

  images="$(
    cd "/srv/${name}" && /usr/bin/podman-compose -f compose.yml config 2>/dev/null \
      | awk '/^[[:space:]]*image:[[:space:]]*/{print}' \
      | sed -E "s/^[[:space:]]*image:[[:space:]]*//; s/[[:space:]]*#.*$//; s/^['\"]//; s/['\"]$//" \
      | awk 'NF' \
      | sort -u
  )" || true
  if [[ -n "$images" ]]; then
    printf '%s\n' "$images"
    return 0
  fi

  if [[ -f "/srv/${name}/compose.yml" ]]; then
    awk '/^[[:space:]]*image:[[:space:]]*/{print}' "/srv/${name}/compose.yml" \
      | sed -E "s/^[[:space:]]*image:[[:space:]]*//; s/[[:space:]]*#.*$//; s/^['\"]//; s/['\"]$//" \
      | awk 'NF' \
      | sort -u
  fi
}

get_image_id() {
  local image="$1"
  podman image inspect --format '{{.Id}}' "$image" 2>/dev/null || true
}

parse_image_reference() {
  local image="$1"
  local first registry remainder name reference last_segment

  if [[ "$image" == */* ]]; then
    first="${image%%/*}"
    if [[ "$first" == *.* || "$first" == *:* || "$first" == "localhost" ]]; then
      registry="$first"
      remainder="${image#*/}"
    else
      registry="docker.io"
      remainder="$image"
    fi
  else
    registry="docker.io"
    remainder="$image"
  fi

  if [[ "$registry" == "docker.io" && "$remainder" != */* ]]; then
    remainder="library/${remainder}"
  fi

  if [[ "$remainder" == *@* ]]; then
    name="${remainder%@*}"
    reference="${remainder#*@}"
  else
    name="$remainder"
    last_segment="${name##*/}"
    if [[ "$last_segment" == *:* ]]; then
      reference="${last_segment##*:}"
      name="${name%:*}"
    else
      reference="latest"
    fi
  fi

  echo "$registry $name $reference"
}

get_remote_manifest_digest_head() {
  local image="$1"
  local parsed registry repo reference manifest_url headers digest token token_json
  local token_service token_scope token_url status_line status_code www_auth realm scope service
  local accept_header

  parsed="$(parse_image_reference "$image")"
  registry="${parsed%% *}"
  parsed="${parsed#* }"
  repo="${parsed%% *}"
  reference="${parsed#* }"
  accept_header="application/vnd.oci.image.index.v1+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json"

  if [[ "$registry" == "docker.io" ]]; then
    token_url="https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull"
    token_json="$(curl -fsSL "$token_url" 2>/dev/null || true)"
    token="$(printf '%s' "$token_json" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
    if [[ -z "$token" ]]; then
      token="$(printf '%s' "$token_json" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
    fi
    [[ -z "$token" ]] && return 0

    manifest_url="https://registry-1.docker.io/v2/${repo}/manifests/${reference}"
    headers="$(curl -sSIL -H "Accept: ${accept_header}" -H "Authorization: Bearer ${token}" "$manifest_url" 2>/dev/null || true)"
  else
    manifest_url="https://${registry}/v2/${repo}/manifests/${reference}"
    headers="$(curl -sSIL -H "Accept: ${accept_header}" "$manifest_url" 2>/dev/null || true)"
    status_line="$(printf '%s\n' "$headers" | head -n1 | tr -d '\r')"
    status_code="$(printf '%s\n' "$status_line" | awk '{print $2}')"

    if [[ "$status_code" == "401" ]]; then
      www_auth="$(printf '%s\n' "$headers" | sed -n '/^[Ww][Ww][Ww]-[Aa][Uu][Tt][Hh][Ee][Nn][Tt][Ii][Cc][Aa][Tt][Ee]:/ {s/^[^:]*:[[:space:]]*//; p; q}' | tr -d '\r')"
      realm="$(printf '%s\n' "$www_auth" | sed -n 's/.*realm="\([^"]*\)".*/\1/p')"
      service="$(printf '%s\n' "$www_auth" | sed -n 's/.*service="\([^"]*\)".*/\1/p')"
      scope="$(printf '%s\n' "$www_auth" | sed -n 's/.*scope="\([^"]*\)".*/\1/p')"
      if [[ -z "$scope" ]]; then
        scope="repository:${repo}:pull"
      fi

      if [[ -n "$realm" ]]; then
        token_url="${realm}?scope=${scope}"
        if [[ -n "$service" ]]; then
          token_url="${token_url}&service=${service}"
        fi
        token_json="$(curl -fsSL "$token_url" 2>/dev/null || true)"
        token="$(printf '%s' "$token_json" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
        if [[ -z "$token" ]]; then
          token="$(printf '%s' "$token_json" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
        fi
        if [[ -n "$token" ]]; then
          headers="$(curl -sSIL -H "Accept: ${accept_header}" -H "Authorization: Bearer ${token}" "$manifest_url" 2>/dev/null || true)"
        fi
      fi
    fi
  fi

  digest="$(printf '%s\n' "$headers" | sed -n '/^[Dd][Oo][Cc][Kk][Ee][Rr]-[Cc][Oo][Nn][Tt][Ee][Nn][Tt]-[Dd][Ii][Gg][Ee][Ss][Tt]:/ {s/^[^:]*:[[:space:]]*//; p; q}' | tr -d '\r')"
  if [[ -n "$digest" ]]; then
    echo "$digest"
  fi
}

remote_digest_state_file() {
  local service="$1"
  echo "/var/lib/oci/digests/${service}.state"
}

create_update_timer() {
  local name="$1"
  local service_file="/etc/systemd/system/oci-update-${name}.service"
  local timer_file="/etc/systemd/system/oci-update-${name}.timer"

  cat <<__SERVICE__ > "$service_file"
[Unit]
Description=OCI update for ${name}

[Service]
Type=oneshot
ExecStart=/usr/bin/oci update ${name}
__SERVICE__

  cat <<__TIMER__ > "$timer_file"
[Unit]
Description=OCI update timer for ${name}

[Timer]
OnCalendar=*:0/15
RandomizedDelaySec=2m
Persistent=true

[Install]
WantedBy=timers.target
__TIMER__

  systemctl daemon-reload
  systemctl enable --now "oci-update-${name}.timer"
}

remove_update_timer() {
  local name="$1"
  local service_file="/etc/systemd/system/oci-update-${name}.service"
  local timer_file="/etc/systemd/system/oci-update-${name}.timer"

  systemctl stop "oci-update-${name}.timer" || true
  systemctl disable "oci-update-${name}.timer" || true
  rm -f "$service_file" "$timer_file"
  systemctl daemon-reload
}

is_enabled() {
  local unit="$1"
  if systemctl is-enabled --quiet "$unit" 2>/dev/null; then
    echo "true"
  else
    echo "false"
  fi
}

image_tag() {
  local image="$1"
  if [[ "$image" == *":"* ]]; then
    echo "${image##*:}"
  else
    echo "latest"
  fi
}

image_created() {
  local image="$1"
  podman image inspect --format '{{.Created}}' "$image" 2>/dev/null || true
}

pull_output_indicates_change() {
  local file="$1"

  if grep -Eq "Downloaded newer image" "$file"; then
    return 0
  fi

  return 1
}

CMD="${1:-}"
SERVICE="${2:-}"

case "$CMD" in
  help|-h|--help|"")
    usage
    exit 0
    ;;
  enable)
    require_root
    validate_service "$SERVICE"

    create_service "$SERVICE"
    systemctl enable "$SERVICE"
    ;;
  disable)
    require_root
    validate_service "$SERVICE"

    systemctl disable "$SERVICE" || true
    ;;
  start)
    require_root
    validate_service "$SERVICE"

    create_service "$SERVICE"
    systemctl start "$SERVICE"
    ;;
  stop)
    require_root
    validate_service "$SERVICE"

    systemctl stop "$SERVICE"
    ;;
  status)
    require_root
    validate_service "$SERVICE"

    systemctl status "$SERVICE" --no-pager
    ;;
  log|logs)
    require_root
    validate_service "$SERVICE"
    require_service_compose "$SERVICE"

    (cd "/srv/${SERVICE}" && /usr/bin/podman-compose logs -ft)
    ;;
  pull)
    require_root
    validate_service "$SERVICE"
    require_service_compose "$SERVICE"

    (cd "/srv/${SERVICE}" && /usr/bin/podman-compose -f compose.yml pull)
    ;;
  update)
    require_root
    validate_service "$SERVICE"
    require_service_compose "$SERVICE"

    changed=0
    used_image_id_compare=0
    was_active=0
    pull_output_file="$(mktemp)"
    if systemctl is-active --quiet "$SERVICE"; then
      was_active=1
    fi
    images="$(get_compose_images "$SERVICE")"
    if [[ -n "$images" ]]; then
      used_image_id_compare=1
      declare -A before_ids=()
      declare -A after_ids=()
      declare -A previous_remote_digests=()
      declare -A current_remote_digests=()
      digest_state_file="$(remote_digest_state_file "$SERVICE")"
      do_pull=1
      remote_update=0
      remote_check_inconclusive=0
      inconclusive_images=()

      if [[ -f "$digest_state_file" ]]; then
        while IFS=$'\t' read -r img digest; do
          [[ -z "$img" || -z "$digest" ]] && continue
          previous_remote_digests["$img"]="$digest"
        done < "$digest_state_file"
      fi

      while read -r img; do
        [[ -z "$img" ]] && continue
        remote_digest="$(get_remote_manifest_digest_head "$img" | head -n1)"
        if [[ -z "$remote_digest" ]]; then
          remote_check_inconclusive=1
          inconclusive_images+=("$img")
          continue
        fi
        current_remote_digests["$img"]="$remote_digest"
        if [[ -z "${previous_remote_digests[$img]:-}" || "${previous_remote_digests[$img]}" != "$remote_digest" ]]; then
          remote_update=1
        fi
      done <<< "$images"

      if [[ "$remote_update" -eq 1 ]]; then
        do_pull=1
      elif [[ "$remote_check_inconclusive" -eq 0 ]]; then
        do_pull=0
        echo "Kein Registry-Update erkannt, Pull wird uebersprungen."
      elif [[ "${OCI_PULL_ON_INCONCLUSIVE:-0}" == "1" ]]; then
        do_pull=1
        echo "HEAD-Pruefung nicht eindeutig (${inconclusive_images[*]}), nutze Pull-Fallback (OCI_PULL_ON_INCONCLUSIVE=1)."
      else
        do_pull=0
        echo "HEAD-Pruefung nicht eindeutig (${inconclusive_images[*]}), Pull wird uebersprungen."
      fi

      if [[ "$do_pull" -eq 1 ]]; then
        while read -r img; do
          [[ -z "$img" ]] && continue
          before_ids["$img"]="$(get_image_id "$img" | head -n1)"
        done <<< "$images"

        (cd "/srv/${SERVICE}" && /usr/bin/podman-compose -f compose.yml pull 2>&1 | tee "$pull_output_file")

        while read -r img; do
          [[ -z "$img" ]] && continue
          after_ids["$img"]="$(get_image_id "$img" | head -n1)"
        done <<< "$images"

        for img in "${!before_ids[@]}"; do
          before_id="${before_ids[$img]}"
          after_id="${after_ids[$img]:-}"
          if [[ -n "$after_id" && "$after_id" != "$before_id" ]]; then
            changed=1
            break
          fi
          if [[ -z "$before_id" && -n "$after_id" ]]; then
            changed=1
            break
          fi
        done
      fi

      if [[ "$remote_check_inconclusive" -eq 0 && "${#current_remote_digests[@]}" -gt 0 ]]; then
        mkdir -p "$(dirname "$digest_state_file")"
        digest_state_tmp="$(mktemp)"
        for img in "${!current_remote_digests[@]}"; do
          printf '%s\t%s\n' "$img" "${current_remote_digests[$img]}"
        done | sort > "$digest_state_tmp"
        mv "$digest_state_tmp" "$digest_state_file"
      fi
    else
      (cd "/srv/${SERVICE}" && /usr/bin/podman-compose -f compose.yml pull 2>&1 | tee "$pull_output_file")
      if pull_output_indicates_change "$pull_output_file"; then
        changed=1
      fi
    fi

    if [[ "$changed" -eq 0 && "$used_image_id_compare" -eq 0 ]]; then
      if pull_output_indicates_change "$pull_output_file"; then
        changed=1
      fi
    fi
    rm -f "$pull_output_file"

    if [[ "$changed" -eq 1 && "$was_active" -eq 1 ]]; then
      echo "Update erkannt, starte Service ${SERVICE} neu."
      systemctl restart "$SERVICE"
    elif [[ "$changed" -eq 1 ]]; then
      echo "Update erkannt, Service ${SERVICE} war nicht aktiv. Kein Restart."
    else
      echo "Kein Update erkannt, kein Restart erforderlich."
    fi

    if [[ "$changed" -eq 1 ]]; then
      # Nach jedem Image-Wechsel das alte, jetzt taglose Image entsorgen —
      # sonst sammeln sich pro Update ~Imagegroesse dangling Layer an, bis
      # die Platte voll ist (home-a lief so auf 96%). Nur dangling, keine
      # getaggten Images anderer Services.
      /usr/bin/podman image prune -f > /dev/null || true
    fi
    ;;
  autoupdate)
    require_root
    validate_service "$SERVICE"

    create_service "$SERVICE"
    create_update_timer "$SERVICE"
    ;;
  noautoupdate)
    require_root
    validate_service "$SERVICE"

    remove_update_timer "$SERVICE"
    ;;
  restart)
    require_root
    validate_service "$SERVICE"

    systemctl restart "$SERVICE"
    ;;
  cat)
    require_root
    validate_service "$SERVICE"

    if [[ -f "/etc/systemd/system/${SERVICE}.service" ]]; then
      echo "### /etc/systemd/system/${SERVICE}.service"
      cat "/etc/systemd/system/${SERVICE}.service"
      echo
    fi
    if [[ -f "/etc/systemd/system/oci-update-${SERVICE}.timer" ]]; then
      echo "### /etc/systemd/system/oci-update-${SERVICE}.timer"
      cat "/etc/systemd/system/oci-update-${SERVICE}.timer"
      echo
    fi
    if [[ -f "/etc/systemd/system/oci-update-${SERVICE}.service" ]]; then
      echo "### /etc/systemd/system/oci-update-${SERVICE}.service"
      cat "/etc/systemd/system/oci-update-${SERVICE}.service"
      echo
    fi
    if [[ -f "/srv/${SERVICE}/compose.yml" ]]; then
      echo "### /srv/${SERVICE}/compose.yml"
      cat "/srv/${SERVICE}/compose.yml"
    fi
    ;;
  info)
    require_root
    validate_service "$SERVICE"
    require_service_compose "$SERVICE"

    echo "service: ${SERVICE}"
    echo "path: /srv/${SERVICE}"
    echo "autoupdate: $(is_enabled "oci-update-${SERVICE}.timer")"
    images="$(get_compose_images "$SERVICE")"
    if [[ -n "$images" ]]; then
      while read -r img; do
        [[ -z "$img" ]] && continue
        tag="$(image_tag "$img")"
        created="$(image_created "$img")"
        if [[ -z "$tag" ]]; then
          tag="unknown"
        fi
        if [[ -z "$created" ]]; then
          created="unknown"
        fi
        echo "image version: ${tag}, from ${created}"
      done <<< "$images"
    else
      echo "image version: unknown, from unknown"
    fi
    echo "enabled: $(is_enabled "${SERVICE}")"
    ;;
  create)
    require_root
    validate_service "$SERVICE"

    create_service "$SERVICE"
    ;;
  delete)
    require_root
    validate_service "$SERVICE"

    systemctl stop "$SERVICE" || true
    systemctl disable "$SERVICE" || true
    remove_update_timer "$SERVICE"
    rm -f "/etc/systemd/system/${SERVICE}.service"
    systemctl daemon-reload
    ;;
  *)
    die "Unbekanntes Kommando: $CMD"
    ;;
esac
