#!/usr/bin/env bash

set -uo pipefail

usage() {
  cat <<'EOF'
Usage: docker_restart_policy_report.sh [--tsv]

Print restart and exit information for every Docker container.

Options:
  --tsv   Print tab-separated output instead of an aligned table.
  -h      Show this help.
EOF
}

output_tsv=false

case "${1:-}" in
  --tsv)
    output_tsv=true
    ;;
  -h|--help)
    usage
    exit 0
    ;;
  "")
    ;;
  *)
    usage >&2
    exit 2
    ;;
esac

for command_name in docker jq; do
  if ! command -v "$command_name" >/dev/null 2>&1; then
    printf 'Error: required command not found: %s\n' "$command_name" >&2
    exit 1
  fi
done

if ! docker ps -a >/dev/null 2>&1; then
  printf 'Error: cannot access the Docker daemon. Check Docker or run with sufficient privileges.\n' >&2
  exit 1
fi

docker_root=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)
docker_root=${docker_root:-/var/lib/docker}

tmp_file=$(mktemp)
trap 'rm -f "$tmp_file"' EXIT

printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
  'CONTAINER_ID' \
  'NAME' \
  'IMAGE' \
  'STATUS' \
  'COMPOSE_RESTART' \
  '.HostConfig.RestartPolicy.Name' \
  'HasBeenManuallyStopped' \
  'EXIT_CODE' \
  'COMPOSE_FILE' >"$tmp_file"

extract_restart_from_yaml() {
  local compose_file=$1
  local service=$2

  awk -v target="$service" '
    function indentation(line, copy) {
      copy = line
      sub(/[^ ].*$/, "", copy)
      return length(copy)
    }

    /^[[:space:]]*#/ || /^[[:space:]]*$/ { next }

    /^[[:space:]]*services:[[:space:]]*(#.*)?$/ {
      in_services = 1
      services_indent = indentation($0)
      next
    }

    in_services {
      current_indent = indentation($0)
      content = $0
      sub(/^[ ]+/, "", content)

      if (current_indent <= services_indent) {
        exit
      }

      if (!service_level_known && content ~ /^[^:]+:[[:space:]]*(#.*)?$/) {
        service_indent = current_indent
        service_level_known = 1
      }

      if (!in_target && service_level_known && current_indent == service_indent && \
          content ~ /^[^:]+:[[:space:]]*(#.*)?$/) {
        key = content
        sub(/:[[:space:]]*(#.*)?$/, "", key)
        gsub(/^[[:space:]"\047]+|[[:space:]"\047]+$/, "", key)

        if (key == target) {
          in_target = 1
          target_indent = current_indent
        }
        next
      }

      if (in_target && current_indent <= target_indent) {
        exit
      }

      if (in_target && content ~ /^restart[[:space:]]*:/) {
        sub(/^restart[[:space:]]*:[[:space:]]*/, "", content)
        sub(/[[:space:]]+#.*$/, "", content)
        gsub(/^[[:space:]"\047]+|[[:space:]"\047]+$/, "", content)
        print content
        exit
      }
    }
  ' "$compose_file"
}

COMPOSE_FILES=()
COMPOSE_FILE_DISPLAY='-'
COMPOSE_RESTART='not_compose'

resolve_compose_details() {
  local config_files=$1
  local working_dir=$2
  local project=$3
  local service=$4
  local candidate
  local raw_files=()
  local existing_files=()
  local compose_args=()
  local compose_json
  local policy
  local i

  COMPOSE_FILES=()
  COMPOSE_FILE_DISPLAY='-'
  COMPOSE_RESTART='not_compose'

  if [[ -z "$service" ]]; then
    return
  fi

  COMPOSE_RESTART='not_set'

  if [[ -n "$config_files" ]]; then
    IFS=',' read -r -a raw_files <<<"$config_files"
    for candidate in "${raw_files[@]}"; do
      candidate=${candidate#"${candidate%%[![:space:]]*}"}
      candidate=${candidate%"${candidate##*[![:space:]]}"}
      if [[ -n "$candidate" && "$candidate" != /* && -n "$working_dir" ]]; then
        candidate="$working_dir/$candidate"
      fi
      [[ -n "$candidate" ]] && COMPOSE_FILES+=("$candidate")
    done
  fi

  if [[ ${#COMPOSE_FILES[@]} -eq 0 && -n "$working_dir" ]]; then
    for candidate in \
      "$working_dir/compose.yaml" \
      "$working_dir/compose.yml" \
      "$working_dir/docker-compose.yaml" \
      "$working_dir/docker-compose.yml"; do
      if [[ -e "$candidate" ]]; then
        COMPOSE_FILES+=("$candidate")
        break
      fi
    done
  fi

  if [[ ${#COMPOSE_FILES[@]} -eq 0 && -n "$project" ]]; then
    candidate="/var/lib/casaos/apps/$project/docker-compose.yml"
    [[ -e "$candidate" ]] && COMPOSE_FILES+=("$candidate")
  fi

  if [[ ${#COMPOSE_FILES[@]} -eq 0 ]]; then
    COMPOSE_FILE_DISPLAY='not_found'
    COMPOSE_RESTART='unknown'
    return
  fi

  COMPOSE_FILE_DISPLAY=$(IFS=','; printf '%s' "${COMPOSE_FILES[*]}")

  for candidate in "${COMPOSE_FILES[@]}"; do
    if [[ -r "$candidate" ]]; then
      existing_files+=("$candidate")
      compose_args+=(-f "$candidate")
    fi
  done

  if [[ ${#existing_files[@]} -ne ${#COMPOSE_FILES[@]} ]]; then
    COMPOSE_RESTART='unreadable'
    return
  fi

  # Compose resolves overrides and environment interpolation more accurately than raw YAML parsing.
  if docker compose version >/dev/null 2>&1; then
    if [[ -n "$working_dir" ]]; then
      compose_args+=(--project-directory "$working_dir")
    fi
    compose_json=$(docker compose "${compose_args[@]}" config --format json 2>/dev/null || true)
    if [[ -n "$compose_json" ]]; then
      policy=$(printf '%s' "$compose_json" | jq -er --arg service "$service" \
        '.services[$service].restart // "not_set" | tostring' 2>/dev/null || true)
      if [[ -n "$policy" ]]; then
        COMPOSE_RESTART=$policy
        return
      fi
    fi
  fi

  # Fall back to the last override file that explicitly sets restart for this service.
  for ((i = ${#existing_files[@]} - 1; i >= 0; i--)); do
    policy=$(extract_restart_from_yaml "${existing_files[$i]}" "$service")
    if [[ -n "$policy" ]]; then
      COMPOSE_RESTART=$policy
      return
    fi
  done
}

get_manual_stop_state() {
  local full_id=$1
  local config_file="$docker_root/containers/$full_id/config.v2.json"

  if [[ -r "$config_file" ]]; then
    jq -r '
      if has("HasBeenManuallyStopped")
      then (.HasBeenManuallyStopped | tostring)
      else "unknown"
      end
    ' "$config_file" 2>/dev/null || printf '%s' 'read_error'
  elif [[ -e "$config_file" ]]; then
    printf '%s' 'permission_denied'
  elif [[ $EUID -ne 0 && "$docker_root" == /var/lib/docker* ]]; then
    printf '%s' 'requires_root'
  else
    printf '%s' 'unavailable'
  fi
}

container_count=0

while IFS=$'\t' read -r short_id ps_name ps_image ps_status; do
  [[ -z "$short_id" ]] && continue

  inspect_json=$(docker inspect "$short_id" 2>/dev/null || true)
  if [[ -z "$inspect_json" ]]; then
    continue
  fi

  inspect_fields=$(printf '%s' "$inspect_json" | jq -r '
    .[0] |
    [
      .Id,
      (.Name | ltrimstr("/")),
      (.State.ExitCode | tostring),
      (.HostConfig.RestartPolicy.Name // "no"),
      (.Config.Labels["com.docker.compose.project"] // ""),
      (.Config.Labels["com.docker.compose.service"] // ""),
      (.Config.Labels["com.docker.compose.project.working_dir"] // ""),
      (.Config.Labels["com.docker.compose.project.config_files"] // "")
    ] | join("\u001f")
  ')

  IFS=$'\x1f' read -r \
    full_id \
    inspected_name \
    exit_code \
    hostconfig_restart_policy_name \
    compose_project \
    compose_service \
    compose_working_dir \
    compose_config_files <<<"$inspect_fields"

  name=${inspected_name:-$ps_name}
  manual_stop=$(get_manual_stop_state "$full_id")

  resolve_compose_details \
    "$compose_config_files" \
    "$compose_working_dir" \
    "$compose_project" \
    "$compose_service"

  printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
    "$short_id" \
    "$name" \
    "$ps_image" \
    "$ps_status" \
    "$COMPOSE_RESTART" \
    "$hostconfig_restart_policy_name" \
    "$manual_stop" \
    "$exit_code" \
    "$COMPOSE_FILE_DISPLAY" >>"$tmp_file"

  container_count=$((container_count + 1))
done < <(docker ps -a --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}')

if [[ "$output_tsv" == true ]] || ! command -v column >/dev/null 2>&1; then
  cat "$tmp_file"
else
  column -t -s $'\t' "$tmp_file"
fi

if [[ $container_count -eq 0 ]]; then
  printf '\nNo containers found.\n'
fi
