#!/usr/bin/env bash
# check-containers
#
# save to
# /usr/local/bin/check-containers
#
# bash/zsh
# alias containers='CHECK_CONTAINERS_QUIET=1 CHECK_CONTAINERS_IGNORE_ERRORS=1 bash /usr/local/bin/check-containers'
# alias containers-watch='CHECK_CONTAINERS_QUIET=1 CHECK_CONTAINERS_IGNORE_ERRORS=1 bash /usr/local/bin/check-containers monitor'
#
# statuses:
#   online   = http response exactly 200
#   error    = reachable but non-200 response
#   offline  = dns/tls/connectivity/timeout failure
#   skipped  = unlabeled / wildcard / intentionally not checked
#
# exit codes:
#   0 = all checked domains online, or domains-only output
#   1 = at least one error
#   2 = at least one offline
#   3 = runtime/config/dependency error
#
# ================================================================================


set -u
set -o pipefail

if [ -n "${ZSH_VERSION:-}" ]; then
    emulate -L sh
fi

IFS='
    '

PROGRAM="check-containers"
VERSION="1.1.1"

CONFIG_FILE="${CHECK_CONTAINERS_CONFIG:-/etc/check-containers.conf}"

USE_COLOR="${CHECK_CONTAINERS_COLOR:-auto}"
DOMAINS_ONLY=0
LABEL_KEY="${CHECK_CONTAINERS_LABEL_KEY:-}"
AUTO_DISCOVER="${CHECK_CONTAINERS_AUTO_DISCOVER:-1}"
NO_HEADER=0
TIMEOUT="${CHECK_CONTAINERS_TIMEOUT:-8}"
FORCE_SCHEME="${CHECK_CONTAINERS_SCHEME:-}"
SHOW_ALL="${CHECK_CONTAINERS_SHOW_ALL:-0}"
MONITOR=0
REFRESH="${CHECK_CONTAINERS_REFRESH:-10}"
OUTPUT_FORMAT="${CHECK_CONTAINERS_OUTPUT:-table}"
STRICT_TLS="${CHECK_CONTAINERS_STRICT_TLS:-1}"
EDIT_CONFIG=0
INTERACTIVE=0
EXIT_MODE="${CHECK_CONTAINERS_EXIT_MODE:-default}"
OS_FAMILY="unknown"
OS_NAME="unknown"

RESET=''
BOLD=''
DIM=''
RED=''
GREEN=''
YELLOW=''
BLUE=''
MAGENTA=''
CYAN=''
GRAY=''

usage() {
    cat <<'USAGE'
usage:
  check-containers [options]
  check-containers monitor [options]

options:
  -c, --color                 force color output
  -n, --no-color              disable color output
  -d, --domains               print domains only
      --label key             docker label key to use
      --no-discover           disable auto-detection of domain label key
      --no-header             do not print table header
      --timeout seconds       http timeout per domain (default: 8)
      --http                  check http only
      --https                 check https only
      --all                   include containers without valid domain labels
      --refresh seconds       monitor refresh interval (default: 10)
      --json                  output json
      --table                 output table
      --strict-tls            verify tls certificates
      --insecure              ignore tls certificate validation
      --config file           use alternate config file
      --edit-config           edit config interactively with prompts
      --interactive           prompt for missing config values before run
      --exit-default          0 all online, 1 any error, 2 any offline, 3 runtime failure
  -h, --help                  show help

commands:
  monitor                     live refreshing view

config precedence:
  cli > environment > config file > built-in defaults

environment:
  check_containers_config
  check_containers_label_key
  check_containers_auto_discover
  check_containers_timeout
  check_containers_scheme
  check_containers_show_all
  check_containers_refresh
  check_containers_output
  check_containers_strict_tls
  check_containers_color
  check_containers_exit_mode

examples:
  check-containers
  check-containers --label caddy
  check-containers --domains
  check-containers --json
  check-containers monitor --refresh 5
  check-containers --edit-config
USAGE
}

die() {
    printf 'error: %s\n' "$*" >&2
    exit 3
}

warn() {
    printf 'warning: %s\n' "$*" >&2
}

# a dog wearing shoes is a top 5 out of all the phenomenons

need() {
    command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

trim() {
    local value
    value="${1:-}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    printf '%s' "$value"
}

lower() {
    printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]'
}

load_config_file() {
    [ -f "$CONFIG_FILE" ] || return 0
    # shellcheck disable=SC1090
    . "$CONFIG_FILE"
    LABEL_KEY="${CHECK_CONTAINERS_LABEL_KEY:-$LABEL_KEY}"
    AUTO_DISCOVER="${CHECK_CONTAINERS_AUTO_DISCOVER:-$AUTO_DISCOVER}"
    TIMEOUT="${CHECK_CONTAINERS_TIMEOUT:-$TIMEOUT}"
    FORCE_SCHEME="${CHECK_CONTAINERS_SCHEME:-$FORCE_SCHEME}"
    SHOW_ALL="${CHECK_CONTAINERS_SHOW_ALL:-$SHOW_ALL}"
    REFRESH="${CHECK_CONTAINERS_REFRESH:-$REFRESH}"
    OUTPUT_FORMAT="${CHECK_CONTAINERS_OUTPUT:-$OUTPUT_FORMAT}"
    STRICT_TLS="${CHECK_CONTAINERS_STRICT_TLS:-$STRICT_TLS}"
    USE_COLOR="${CHECK_CONTAINERS_COLOR:-$USE_COLOR}"
    EXIT_MODE="${CHECK_CONTAINERS_EXIT_MODE:-$EXIT_MODE}"
}

detect_os() {
    if [ -r /etc/os-release ]; then
        OS_NAME="$(awk -F= '/^id=/{gsub(/"/,"",$2); print $2}' /etc/os-release 2>/dev/null | head -n1)"
        [ -n "$OS_NAME" ] || OS_NAME="unknown"
        case "$OS_NAME" in
            alpine) OS_FAMILY="alpine" ;;
            arch|endeavouros|manjaro) OS_FAMILY="arch" ;;
            ubuntu) OS_FAMILY="ubuntu" ;;
            debian|raspbian) OS_FAMILY="debian" ;;
            *) OS_FAMILY="$OS_NAME" ;;
        esac
        return 0
    fi
    if command -v apk >/dev/null 2>&1; then
        OS_FAMILY="alpine"
        OS_NAME="alpine"
    elif command -v pacman >/dev/null 2>&1; then
        OS_FAMILY="arch"
        OS_NAME="arch"
    elif command -v apt-get >/dev/null 2>&1; then
        OS_FAMILY="debian"
        OS_NAME="debian"
    fi
}

pick_random_theme() {
    local idx
    idx=$(( ( $(date +%s) + $$ ) % 5 ))

    case "$idx" in
        0)
            RED=$'\033[0;31m'
            GREEN=$'\033[0;32m'
            YELLOW=$'\033[1;33m'
            BLUE=$'\033[0;34m'
            MAGENTA=$'\033[0;35m'
            CYAN=$'\033[0;36m'
            GRAY=$'\033[0;90m'
            ;;
        1)
            RED=$'\033[1;31m'
            GREEN=$'\033[1;32m'
            YELLOW=$'\033[0;33m'
            BLUE=$'\033[1;34m'
            MAGENTA=$'\033[1;35m'
            CYAN=$'\033[1;36m'
            GRAY=$'\033[0;37m'
            ;;
        2)
            RED=$'\033[38;5;196m'
            GREEN=$'\033[38;5;46m'
            YELLOW=$'\033[38;5;226m'
            BLUE=$'\033[38;5;39m'
            MAGENTA=$'\033[38;5;201m'
            CYAN=$'\033[38;5;51m'
            GRAY=$'\033[38;5;245m'
            ;;
        3)
            RED=$'\033[38;5;160m'
            GREEN=$'\033[38;5;34m'
            YELLOW=$'\033[38;5;178m'
            BLUE=$'\033[38;5;27m'
            MAGENTA=$'\033[38;5;129m'
            CYAN=$'\033[38;5;44m'
            GRAY=$'\033[38;5;240m'
            ;;
        *)
            RED=$'\033[0;31m'
            GREEN=$'\033[38;5;70m'
            YELLOW=$'\033[38;5;214m'
            BLUE=$'\033[38;5;33m'
            MAGENTA=$'\033[38;5;141m'
            CYAN=$'\033[38;5;45m'
            GRAY=$'\033[38;5;243m'
            ;;
    esac
}

setup_colors() {
    local tty=0
    [ -t 1 ] && tty=1

    case "$USE_COLOR" in
        1|yes|true|always|force) USE_COLOR="force" ;;
        0|no|false|never) USE_COLOR="never" ;;
        auto|*) USE_COLOR="auto" ;;
    esac

    if [ "$USE_COLOR" = "never" ]; then
        RESET='' BOLD='' DIM=''
        RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' GRAY=''
        return 0
    fi

    if [ "$USE_COLOR" = "force" ] || [ "$tty" -eq 1 ]; then
        RESET=$'\033[0m'
        BOLD=$'\033[1m'
        DIM=$'\033[2m'
        pick_random_theme
    else
        RESET='' BOLD='' DIM=''
        RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' GRAY=''
    fi
}

prompt_default() {
    local prompt default answer
    prompt="${1:-}"
    default="${2:-}"
    printf '%s [%s]: ' "$prompt" "$default" >&2
    IFS= read -r answer || true
    answer="$(trim "$answer")"
    [ -n "$answer" ] || answer="$default"
    printf '%s' "$answer"
}

write_config_file() {
    local dir
    dir=$(dirname "$CONFIG_FILE")
    [ -d "$dir" ] || mkdir -p "$dir" || die "cannot create config directory: $dir"

    cat > "$CONFIG_FILE" <<EOF
# $PROGRAM configuration
CHECK_CONTAINERS_LABEL_KEY="${LABEL_KEY}"
CHECK_CONTAINERS_AUTO_DISCOVER="${AUTO_DISCOVER}"
CHECK_CONTAINERS_TIMEOUT="${TIMEOUT}"
CHECK_CONTAINERS_SCHEME="${FORCE_SCHEME}"
CHECK_CONTAINERS_SHOW_ALL="${SHOW_ALL}"
CHECK_CONTAINERS_REFRESH="${REFRESH}"
CHECK_CONTAINERS_OUTPUT="${OUTPUT_FORMAT}"
CHECK_CONTAINERS_STRICT_TLS="${STRICT_TLS}"
CHECK_CONTAINERS_COLOR="${USE_COLOR}"
CHECK_CONTAINERS_EXIT_MODE="${EXIT_MODE}"
EOF
}

interactive_edit_config() {
    printf '%sinteractive configuration for %s%s\n' "$BOLD" "$PROGRAM" "$RESET" >&2
    printf '%sdetected os:%s %s (%s)\n' "$CYAN" "$RESET" "$OS_NAME" "$OS_FAMILY" >&2

    LABEL_KEY="$(prompt_default "docker label key" "${LABEL_KEY:-caddy}")"
    AUTO_DISCOVER="$(prompt_default "auto-discover label key (1/0)" "${AUTO_DISCOVER:-1}")"
    TIMEOUT="$(prompt_default "http timeout seconds" "${TIMEOUT:-8}")"
    FORCE_SCHEME="$(prompt_default "force scheme: empty/http/https" "${FORCE_SCHEME:-}")"
    SHOW_ALL="$(prompt_default "include unlabeled containers (1/0)" "${SHOW_ALL:-0}")"
    REFRESH="$(prompt_default "monitor refresh seconds" "${REFRESH:-10}")"
    OUTPUT_FORMAT="$(prompt_default "output format: table/json" "${OUTPUT_FORMAT:-table}")"
    STRICT_TLS="$(prompt_default "strict tls verify (1/0)" "${STRICT_TLS:-1}")"
    USE_COLOR="$(prompt_default "color mode: auto/force/never" "${USE_COLOR:-auto}")"
    EXIT_MODE="$(prompt_default "exit mode" "${EXIT_MODE:-default}")"

    write_config_file
    printf '%ssaved config:%s %s\n' "$GREEN" "$RESET" "$CONFIG_FILE" >&2
}

interactive_fill_missing() {
    [ -n "$LABEL_KEY" ] || LABEL_KEY="$(prompt_default "docker label key" "caddy")"
}

json_escape() {
    printf '%s' "${1:-}" | sed \
        -e 's/\\/\\\\/g' \
        -e 's/"/\\"/g' \
        -e 's/  /\\t/g' \
        -e 's/
/\\r/g' \
        -e ':a;N;$!ba;s/\n/\\n/g'
}

strip_domain() {
    local domain
    domain="$(trim "${1:-}")"
    domain="${domain#http://}"
    domain="${domain#https://}"
    domain="${domain%%/*}"
    domain="${domain%%\?*}"
    domain="${domain%%\#*}"
    domain="${domain%%:*}"
    printf '%s' "$domain"
}

valid_domain() {
    local domain
    domain="$(strip_domain "${1:-}")"
    [ -n "$domain" ] || return 1
    printf '%s' "$domain" | grep -Eq '^(\*\.)?[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
}

split_domains() {
    local raw
    raw="${1:-}"
    raw="${raw//$'\r'/}"
    raw="${raw//$'\n'/,}"
    raw="${raw//;/,}"
    printf '%s\n' "$raw" |
        tr ',' '\n' |
        sed 's/^[[:space:]]*//;s/[[:space:]]*$//' |
        sed '/^$/d'
}

docker_state() {
    case "${1:-}" in
        running) printf 'running' ;;
        restarting) printf 'restarting' ;;
        exited|created|paused|dead) printf 'stopped' ;;
        *) printf '%s' "${1:-unknown}" ;;
    esac
}

extract_domains_from_value() {
    local value line
    value="${1:-}"

    if printf '%s' "$value" | grep -q 'Host('; then
        printf '%s' "$value" |
            grep -oE '`[^`]+`' |
            tr -d '`' |
            while IFS= read -r line; do
                [ -n "$line" ] && printf '%s\n' "$line"
            done
        return 0
    fi

    split_domains "$value"
}

discover_label_key() {
    local candidate found
    for candidate in \
        "${LABEL_KEY:-}" \
        caddy \
        CADDY \
        VIRTUAL_HOST \
        virtual.host \
        domain \
        domains \
        traefik.domain \
        nginxproxymanager.domain
    do
        [ -n "$candidate" ] || continue
        found="$(
            docker ps -a --format "{{.Label \"$candidate\"}}" 2>/dev/null |
            grep -E '([A-Za-z0-9.-]+\.[A-Za-z]{2,}|Host\()' | head -n 1 || true
        )"
        if [ -n "$found" ]; then
            printf '%s' "$candidate"
            return 0
        fi
    done
    return 1
}

check_url() {
    local url curl_args code rc
    url="$1"

    curl_args="--silent --show-error --location --output /dev/null --write-out %{http_code} --connect-timeout $TIMEOUT --max-time $TIMEOUT --user-agent $PROGRAM/$VERSION"

    if [ "$STRICT_TLS" = "0" ]; then
        curl_args="$curl_args --insecure"
    fi

    # shellcheck disable=SC2086
    code="$(curl $curl_args "$url" 2>/dev/null)"
    rc=$?

    if [ "$rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
        return 1
    fi

    printf '%s' "$code"
    return 0
}

http_check() {
    local domain code
    domain="$1"
    code=""

    case "$domain" in
        \*.*)
            printf 'skipped|wildcard|--'
            return 0
            ;;
    esac

    if [ "$FORCE_SCHEME" = "http" ]; then
        if code="$(check_url "http://$domain")"; then
            [ "$code" = "200" ] &&
                printf 'online|http 200|http' ||
                printf 'error|http %s|http' "$code"
        else
            printf 'offline|no connection|http'
        fi
        return 0
    fi

    if [ "$FORCE_SCHEME" = "https" ]; then
        if code="$(check_url "https://$domain")"; then
            [ "$code" = "200" ] &&
                printf 'online|http 200|https' ||
                printf 'error|http %s|https' "$code"
        else
            printf 'offline|no connection|https'
        fi
        return 0
    fi

    if code="$(check_url "https://$domain")"; then
        [ "$code" = "200" ] &&
            printf 'online|http 200|https' ||
            printf 'error|http %s|https' "$code"
        return 0
    fi

    if code="$(check_url "http://$domain")"; then
        [ "$code" = "200" ] &&
            printf 'online|http 200|http' ||
            printf 'error|http %s|http' "$code"
        return 0
    fi

    printf 'offline|no connection|--'
}

print_solid_line() {
    local width char i
    width="${1:-80}"
    char="${2:-─}"
    i=0
    while [ "$i" -lt "$width" ]; do
        printf '%s' "$char"
        i=$((i + 1))
    done
    printf '\n'
}

collect_rows() {
    local rows_tmp label raw found name state uptime created_at domains domain
    local container_state result check_status check_detail check_scheme domain_found
    rows_tmp="$1"
    found=0

    label="$LABEL_KEY"
    if [ -z "$label" ] && [ "$AUTO_DISCOVER" = "1" ]; then
        label="$(discover_label_key || true)"
    fi
    [ -n "$label" ] || label="caddy"

    raw="$(
        docker ps -a \
            --format "{{.Names}}|{{.State}}|{{.RunningFor}}|{{.CreatedAt}}|{{.Label \"$label\"}}" \
            2>/dev/null || true
    )"

    if [ -z "$raw" ]; then
        warn "no docker containers found, or docker is not accessible"
        printf '%s' "$label" > "${rows_tmp}.label"
        return 0
    fi

    printf '%s' "$label" > "${rows_tmp}.label"

    while IFS='|' read -r name state uptime created_at domains; do
        [ -n "${name:-}" ] || continue
        container_state="$(docker_state "$state")"
        domain_found=0

        if [ -n "${domains:-}" ]; then
            while IFS= read -r domain; do
                domain="$(strip_domain "$domain")"
                valid_domain "$domain" || continue
                domain_found=1
                found=1

                if [ "$DOMAINS_ONLY" = "1" ]; then
                    printf '%s\t%s\n' "$name" "$domain" >> "$rows_tmp"
                    continue
                fi

                result="$(http_check "$domain")"
                IFS='|' read -r check_status check_detail check_scheme <<EOF
$result
EOF
                printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
                    "$name" \
                    "$container_state" \
                    "$uptime" \
                    "$domain" \
                    "$check_status" \
                    "$check_detail" \
                    "$check_scheme" >> "$rows_tmp"
            done <<EOF
$(extract_domains_from_value "$domains")
EOF
        fi

        if [ "$SHOW_ALL" = "1" ] && [ "$domain_found" = "0" ] && [ "$DOMAINS_ONLY" = "0" ]; then
            found=1
            printf '%s\t%s\t%s\t-\tskipped\tno domain label\t--\n' \
                "$name" \
                "$container_state" \
                "$uptime" >> "$rows_tmp"
        fi
    done <<EOF
$raw
EOF

    if [ "$found" = "0" ]; then
        warn "no valid domain labels found using label key: $label"
        warn "try: $PROGRAM --label your_label_key"
    fi
}

render_domains_only() {
    local rows_tmp
    rows_tmp="$1"
    cut -f2 "$rows_tmp" 2>/dev/null | sort -u
}

render_json() {
    local rows_tmp label first
    rows_tmp="$1"
    label="$(cat "${rows_tmp}.label" 2>/dev/null || printf '%s' "${LABEL_KEY:-caddy}")"

#    printf '{\n'
#    printf '  "program": "%s",\n' "$(json_escape "$PROGRAM")"
#    printf '  "version": "%s",\n' "$(json_escape "$VERSION")"
#    printf '  "os_family": "%s",\n' "$(json_escape "$OS_FAMILY")"
#    printf '  "os_name": "%s",\n' "$(json_escape "$OS_NAME")"
    printf '  "label": "%s",\n' "$(json_escape "$label")"
    printf '  "containers": [\n'

    first=1
    while IFS=$'\t' read -r name state uptime domain check_status check_detail check_scheme; do
        [ -n "${name:-}" ] || continue
        [ "$first" -eq 1 ] || printf ',\n'
        first=0
        printf '    {"container":"%s","docker_state":"%s","running_for":"%s","domain":"%s","status":"%s","result":"%s","scheme":"%s"}' \
            "$(json_escape "$name")" \
            "$(json_escape "$state")" \
            "$(json_escape "$uptime")" \
            "$(json_escape "$domain")" \
            "$(json_escape "$check_status")" \
            "$(json_escape "$check_detail")" \
            "$(json_escape "$check_scheme")"
    done < "$rows_tmp"

    printf '\n  ]\n'
    printf '}\n'
}

render_table() {
    local rows_tmp label
    local container_width state_width uptime_width domain_width status_width detail_width scheme_width total_width
    local name state uptime domain check_status check_detail check_scheme
    local online error offline skipped total
    local domain_count matched_count container_count
    local state_color status_color

    rows_tmp="$1"
    label="$(cat "${rows_tmp}.label" 2>/dev/null || printf '%s' "${LABEL_KEY:-caddy}")"

    container_width=12
    state_width=12
    uptime_width=14
    domain_width=24
    status_width=8
    detail_width=14
    scheme_width=6

    while IFS=$'\t' read -r name state uptime domain check_status check_detail check_scheme; do
        [ "${#name}" -gt "$container_width" ] && container_width="${#name}"
        [ "${#state}" -gt "$state_width" ] && state_width="${#state}"
        [ "${#uptime}" -gt "$uptime_width" ] && uptime_width="${#uptime}"
        [ "${#domain}" -gt "$domain_width" ] && domain_width="${#domain}"
        [ "${#check_status}" -gt "$status_width" ] && status_width="${#check_status}"
        [ "${#check_detail}" -gt "$detail_width" ] && detail_width="${#check_detail}"
        [ "${#check_scheme}" -gt "$scheme_width" ] && scheme_width="${#check_scheme}"
    done < "$rows_tmp"

    total_width=$((container_width + state_width + uptime_width + domain_width + status_width + detail_width + scheme_width + 18))

    if [ "$NO_HEADER" = "0" ]; then
        printf '\n'
        print_solid_line "$total_width"
        printf "${BOLD}${BLUE}%-${container_width}s %-${state_width}s %-${uptime_width}s %-${domain_width}s %-${status_width}s %-${detail_width}s %-${scheme_width}s${RESET}\n" \
            "container" "docker_state" "running_for" "domain" "status" "result" "scheme"
        print_solid_line "$total_width"
    fi

    online=0
    error=0
    offline=0
    skipped=0

    while IFS=$'\t' read -r name state uptime domain check_status check_detail check_scheme; do
        state_color="$GRAY"
        status_color="$RED"

        case "$state" in
            running) state_color="$GREEN" ;;
            restarting) state_color="$YELLOW" ;;
            stopped) state_color="$RED" ;;
        esac

        case "$check_status" in
            online)
                status_color="$GREEN"
                online=$((online + 1))
                ;;
            error)
                status_color="$YELLOW"
                error=$((error + 1))
                ;;
            offline)
                status_color="$RED"
                offline=$((offline + 1))
                ;;
            skipped)
                status_color="$GRAY"
                skipped=$((skipped + 1))
                ;;
        esac

        printf "%-${container_width}s %s%-${state_width}s${RESET} %-${uptime_width}s %-${domain_width}s %s%-${status_width}s${RESET} %-${detail_width}s %-${scheme_width}s\n" \
            "$name" \
            "$state_color" "$state" \
            "$uptime" \
            "$domain" \
            "$status_color" "$check_status" \
            "$check_detail" \
            "$check_scheme"
    done < "$rows_tmp"

    total=$((online + error + offline + skipped))
    domain_count="$(cut -f4 "$rows_tmp" 2>/dev/null | grep -v '^-$' | sort -u | wc -l | tr -d ' ')"
    matched_count="$(cut -f1 "$rows_tmp" 2>/dev/null | sort -u | wc -l | tr -d ' ')"
    container_count="$(docker ps -aq 2>/dev/null | wc -l | tr -d ' ')"

    if [ "$NO_HEADER" = "0" ]; then
        print_solid_line "$total_width"
    fi

    printf "${GREEN}${BOLD}online${RESET}: %d " "$online"
    printf "${YELLOW}${BOLD}error${RESET}: %d " "$error"
    printf "${RED}${BOLD}offline${RESET}: %d " "$offline"
    printf "${GRAY}${BOLD}skipped${RESET}: %d " "$skipped"
    printf "${BLUE}${BOLD}total${RESET}: %d\n" "$total"

    printf "${CYAN}domains${RESET}: %s " "$domain_count"
    printf "${CYAN}matched_containers${RESET}: %s " "$matched_count"
    printf "${CYAN}all_containers${RESET}: %s " "$container_count"
    printf "${CYAN}label${RESET}: %s " "$label"
    printf "${CYAN}os${RESET}: %s\n" "$OS_FAMILY"
}

determine_exit_code() {
    local rows_tmp status rc
    rows_tmp="$1"
    rc=0

    while IFS=$'\t' read -r _ _ _ _ status _ _; do
        case "$status" in
            offline) rc=2 ;;
            error)
                [ "$rc" -lt 2 ] && rc=1
                ;;
        esac
    done < "$rows_tmp"

    return "$rc"
}

run_check() {
    local rows_tmp=""
    rows_tmp="$(mktemp "${TMPDIR:-/tmp}/check-containers.XXXXXX")" || die "failed to create temp file"
    trap '[ -n "${rows_tmp:-}" ] && rm -f "${rows_tmp}" "${rows_tmp}.label"' RETURN INT TERM EXIT

    collect_rows "$rows_tmp"

    if [ "$DOMAINS_ONLY" = "1" ]; then
        render_domains_only "$rows_tmp"
        return 0
    fi

    case "$OUTPUT_FORMAT" in
        json) render_json "$rows_tmp" ;;
        *) render_table "$rows_tmp" ;;
    esac

    determine_exit_code "$rows_tmp"
    return $?
}

parse_args() {
    if [ "${1:-}" = "monitor" ]; then
        MONITOR=1
        shift
    fi

    while [ "$#" -gt 0 ]; do
        case "$1" in
            -c|--color)
                USE_COLOR="force"
                ;;
            -n|--no-color)
                USE_COLOR="never"
                ;;
            -d|--domains)
                DOMAINS_ONLY=1
                ;;
            --label)
                shift
                [ "$#" -gt 0 ] || die "--label requires a value"
                LABEL_KEY="$1"
                AUTO_DISCOVER=0
                ;;
            --no-discover)
                AUTO_DISCOVER=0
                ;;
            --no-header)
                NO_HEADER=1
                ;;
            --timeout)
                shift
                [ "$#" -gt 0 ] || die "--timeout requires whole seconds"
                printf '%s' "$1" | grep -Eq '^[0-9]+$' || die "--timeout requires whole seconds"
                TIMEOUT="$1"
                ;;
            --refresh)
                shift
                [ "$#" -gt 0 ] || die "--refresh requires whole seconds"
                printf '%s' "$1" | grep -Eq '^[0-9]+$' || die "--refresh requires whole seconds"
                REFRESH="$1"
                ;;
            --http)
                FORCE_SCHEME="http"
                ;;
            --https)
                FORCE_SCHEME="https"
                ;;
            --all)
                SHOW_ALL=1
                ;;
            --json)
                OUTPUT_FORMAT="json"
                ;;
            --table)
                OUTPUT_FORMAT="table"
                ;;
            --strict-tls)
                STRICT_TLS=1
                ;;
            --insecure)
                STRICT_TLS=0
                ;;
            --config)
                shift
                [ "$#" -gt 0 ] || die "--config requires a file path"
                CONFIG_FILE="$1"
                ;;
            --edit-config)
                EDIT_CONFIG=1
                ;;
            --interactive)
                INTERACTIVE=1
                ;;
            --exit-default)
                EXIT_MODE="default"
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            *)
                die "unknown option: $1"
                ;;
        esac
        shift
    done
}

main() {
    parse_args "$@"
    detect_os
    load_config_file
    setup_colors

    need docker
    need curl
    need awk
    need grep
    need sed
    need tr
    need cut
    need sort
    need wc
    need mktemp
    need date

    if [ "$EDIT_CONFIG" = "1" ]; then
        interactive_edit_config
        exit 0
    fi

    if [ "$INTERACTIVE" = "1" ]; then
        interactive_fill_missing
    fi

    if [ "$MONITOR" = "1" ]; then
        trap 'printf "\n"; exit 0' INT TERM
        while true; do
            if [ -t 1 ]; then
                printf '\033[H\033[2J'
            fi
#            printf "${BOLD}${CYAN}%s monitor${RESET} " "$PROGRAM"
#            printf "${DIM}%s | refresh: %ss | os: %s | ctrl+c to exit${RESET}\n" \
             printf "$(date '+%Y-%m-%d %H:%M:%S')" \
                "$REFRESH" \
                "$OS_FAMILY"
            run_check || true
            sleep "$REFRESH"
        done
    else
        run_check
        exit $?
    fi
}

main "$@"