#!/usr/bin/env bash
# Read-only health report for the Plex Media Server.
#
# Exit status: 0 = all checks passed, 1 = at least one WARN, 2 = at least one FAIL.
# Safe to run from cron; makes no changes.
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly COLLECTION_DIR="$(dirname "$SCRIPT_DIR")"

# shellcheck source=../lib/plex-api.sh
. "$COLLECTION_DIR/lib/plex-api.sh"
# shellcheck source=../lib/posters.sh
. "$COLLECTION_DIR/lib/posters.sh"

WARNS=0
FAILS=0
CHECK_POSTERS=0

# Global, not local: an EXIT trap fires after the owning function has returned,
# by which point a local would be out of scope and `set -u` would abort cleanup.
WORKDIR=""
cleanup() { [[ -n "$WORKDIR" ]] && rm -rf "$WORKDIR"; return 0; }
trap cleanup EXIT

usage() {
  cat <<'EOF'
Usage: plex-health [--posters] [--help]

  --posters   Also run a full poster-integrity check on every show/movie
              library. Costs one HTTP request per item, so it is off by
              default; without it the report notes that it was skipped.
  --help

Exit status: 0 ok, 1 warnings, 2 failures.
EOF
}

ok()   { printf '  [ ok ] %s\n' "$*"; }
warn() { printf '  [warn] %s\n' "$*"; WARNS=$((WARNS + 1)); }
fail() { printf '  [FAIL] %s\n' "$*"; FAILS=$((FAILS + 1)); }
info() { printf '  [info] %s\n' "$*"; }
section() { printf '\n%s\n' "$*"; }

# ── checks ───────────────────────────────────────────────────────────────────

check_server() {
  section "Server"
  local root
  root="$(plex_get /)" || { fail "cannot read server root"; return; }

  local version platform pv user signin sub
  version="$(jq -r '.MediaContainer.version // "?"' <<<"$root")"
  platform="$(jq -r '.MediaContainer.platform // "?"' <<<"$root")"
  pv="$(jq -r '.MediaContainer.platformVersion // "?"' <<<"$root")"
  user="$(jq -r '.MediaContainer.myPlexUsername // "?"' <<<"$root")"
  signin="$(jq -r '.MediaContainer.myPlexSigninState // "?"' <<<"$root")"
  sub="$(jq -r '.MediaContainer.myPlexSubscription // false' <<<"$root")"

  ok "Plex ${version} on ${platform} ${pv}"
  [[ "$signin" == "ok" ]] && ok "myPlex sign-in: ${user}" || fail "myPlex sign-in state: ${signin}"
  [[ "$sub" == "true" ]] && ok "Plex Pass subscription active" || info "no active Plex Pass subscription"
}

check_updates() {
  section "Updates"
  local st
  st="$(plex_get /updater/status)" || { warn "updater status unavailable"; return; }
  local size can
  size="$(jq -r '.MediaContainer.size // 0' <<<"$st")"
  can="$(jq -r '.MediaContainer.canInstall // false' <<<"$st")"
  if [[ "$size" == "0" ]]; then
    ok "no server update pending"
  else
    info "update available (canInstall=${can}) — this server updates via its Docker image, not in-app"
  fi
}

check_remote_access() {
  section "Remote access"
  local acct
  acct="$(plex_get /myplex/account)" || { warn "cannot read myPlex account"; return; }
  local mstate merr
  mstate="$(jq -r '.MyPlex.mappingState // "?"' <<<"$acct")"
  merr="$(jq -r '.MyPlex.mappingError // ""' <<<"$acct")"
  if [[ -z "$merr" ]]; then
    ok "port mapping: ${mstate}"
  else
    # odin sits behind a Pangolin/Newt tunnel with no open inbound ports, so an
    # "unreachable" mapping is the expected steady state, not a fault.
    info "port mapping ${mstate}/${merr} — expected: odin has no open inbound ports (tunnel ingress)"
  fi
}

check_libraries() {
  section "Libraries"
  local sections
  sections="$(plex_get /library/sections)" || { fail "cannot list library sections"; return; }

  local key title type refreshing code count
  while IFS=$'\t' read -r key title type refreshing; do
    # Top-level item count: movies for a movie library, shows for a show
    # library. Music sections are counted by artist.
    case "$type" in
      movie)  code=1 ;;
      show)   code=2 ;;
      artist) code=8 ;;
      *)      code=1 ;;
    esac
    count="$(plex_get "/library/sections/${key}/all" --get --data-urlencode "type=${code}" \
             | jq -r '.MediaContainer.totalSize // .MediaContainer.size // "?"' 2>/dev/null || echo '?')"
    if [[ "$refreshing" == "true" ]]; then
      info "${title} (id ${key}, ${type}): ${count} items — currently refreshing"
    else
      ok "${title} (id ${key}, ${type}): ${count} items"
    fi
  done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)\t\(.refreshing)"' <<<"$sections")
}

check_unmatched() {
  section "Unmatched media"
  local sections
  sections="$(plex_get /library/sections)"
  local key title type code items unmatched
  while IFS=$'\t' read -r key title type; do
    case "$type" in
      movie) code=1 ;;
      show)  code=2 ;;
      *)     continue ;;
    esac
    items="$(plex_get "/library/sections/${key}/all" --get --data-urlencode "type=${code}")" || continue
    # An unmatched item keeps a local:// guid instead of a real agent guid.
    unmatched="$(jq -r '[.MediaContainer.Metadata[]?
                        |select((.guid//"")|test("^(local://|com\\.plexapp\\.agents\\.none)"))]|length' <<<"$items")"
    if [[ "$unmatched" == "0" ]]; then
      ok "${title}: all items matched to an agent"
    else
      warn "${title}: ${unmatched} unmatched item(s) — they will never get metadata or artwork"
    fi
  done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)"' <<<"$sections")
}

check_posters() {
  section "Poster integrity"
  if (( ! CHECK_POSTERS )); then
    info "skipped (one request per item) — re-run with --posters, or use bin/poster-audit"
    return
  fi
  local sections
  sections="$(plex_get /library/sections)"
  local key title type code bad total
  WORKDIR="$(mktemp -d)"
  while IFS=$'\t' read -r key title type; do
    case "$type" in
      movie) code=1 ;;
      show)  code=2 ;;
      *)     continue ;;
    esac
    posters_fetch_items "$key" "$code" "$WORKDIR/items.json"
    posters_scan "$WORKDIR/items.json" > "$WORKDIR/scan.tsv"
    total="$(wc -l < "$WORKDIR/scan.tsv" | tr -d ' ')"
    bad="$(awk -F'\t' '$2!="OK"' "$WORKDIR/scan.tsv" | wc -l | tr -d ' ')"
    if [[ "$bad" == "0" ]]; then
      ok "${title}: ${total}/${total} posters resolve"
    else
      warn "${title}: ${bad}/${total} posters missing or broken — run bin/poster-repair --section '${title}'"
    fi
  done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)"' <<<"$sections")
}

check_butler() {
  section "Scheduled maintenance (Butler)"
  local b
  b="$(plex_get /butler)" || { warn "cannot read butler tasks"; return; }

  local backup optimize
  backup="$(jq -r '.ButlerTasks.ButlerTask[]|select(.name=="BackupDatabase")|.enabled' <<<"$b")"
  optimize="$(jq -r '.ButlerTasks.ButlerTask[]|select(.name=="OptimizeDatabase")|.enabled' <<<"$b")"

  [[ "$backup" == "true" ]]   && ok "database backup enabled"   || warn "database backup DISABLED — no automatic recovery point"
  [[ "$optimize" == "true" ]] && ok "database optimize enabled" || warn "database optimize disabled"

  local disabled
  disabled="$(jq -r '[.ButlerTasks.ButlerTask[]|select(.enabled==false)|.name]|join(", ")' <<<"$b")"
  [[ -n "$disabled" ]] && info "disabled tasks: ${disabled}"
}

check_activity() {
  section "Current activity"
  local s a
  s="$(plex_get /status/sessions)" || { warn "cannot read sessions"; return; }
  local streams transcodes
  streams="$(jq -r '.MediaContainer.size // 0' <<<"$s")"
  transcodes="$(jq -r '[.MediaContainer.Metadata[]?|select((.TranscodeSession//empty)!=empty)]|length' <<<"$s")"
  ok "${streams} active stream(s), ${transcodes} transcoding"

  a="$(plex_get /activities)" || return
  local acts
  acts="$(jq -r '.MediaContainer.size // 0' <<<"$a")"
  if [[ "$acts" == "0" ]]; then
    ok "no background activities running"
  else
    info "${acts} background activity/activities: $(jq -r '[.MediaContainer.Activity[]?|.title]|join(", ")' <<<"$a")"
  fi
}

main() {
  while (( $# )); do
    case "$1" in
      --posters) CHECK_POSTERS=1 ;;
      --help|-h) usage; exit 0 ;;
      *) plex_die "unknown argument: $1 (try --help)" ;;
    esac
    shift
  done

  plex_require_deps
  plex_load_env "$COLLECTION_DIR/.env"

  printf 'Plex health check — %s\n' "${PLEX_URL:-unset}"
  plex_check_auth
  printf '  [ ok ] reachable and authenticated\n'

  check_server
  check_updates
  check_remote_access
  check_libraries
  check_unmatched
  check_posters
  check_butler
  check_activity

  section "Result"
  if (( FAILS > 0 )); then
    printf '  %d failure(s), %d warning(s)\n' "$FAILS" "$WARNS"; exit 2
  elif (( WARNS > 0 )); then
    printf '  %d warning(s)\n' "$WARNS"; exit 1
  fi
  printf '  all checks passed\n'
}

main "$@"
