#!/bin/bash
# ==============================================================================================
# ============================= Media Shares Permissions =======================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Apply nobody:users ownership and correct permissions to all media shares.
# Runs daily as the first job in the maintenance window — arr cleanup depends
# on correct ownership to rename and delete files.
#
# Now a proper daily failsafe: files arrive with wrong ownership from rsync
# without --chown, manual admin copies, containers with unconfigured PUID/PGID,
# or unRAID environment resets after updates.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each share in MEDIA_PERMISSION_SHARES:
#
#   1. Path safety and existence
#        → unsafe or missing paths are refused or skipped, never scanned
#
#   2. Count wrong ownership (diagnostic)
#        → find ! -user / ! -group — the number reported as "corrected"
#
#   3. Ownership pass — only if the count is non-zero
#        → chown PERMISSIONS_OWNER on non-matching entries only
#
#   4. Directory mode pass
#        → chmod PERMISSIONS_DIR_MODE on directories not already at that mode
#
#   5. File mode pass
#        → chmod PERMISSIONS_FILE_MODE on files not already at that mode
#        → "No such file" errors ignored: volatile dirs (Emby transcodes) race
#
# Every pass is conditional by design — see Conditional Passes below.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Daily Failsafe, Not Enforcer
#   Wrong ownership is a symptom of something else — a misconfigured container,
#   a manual copy, an rsync without --chown. This script corrects the symptom
#   daily rather than hunting the root cause. A persistent high correction count
#   is the signal to investigate the source.
#
# Runs First in the Window
#   Arr cleanup scripts depend on correct ownership to rename and delete files.
#   Permissions must be correct before cleanup runs — ordering is not optional.
#
# Separate Passes for Directories and Files
#   Directories need execute permission for traversal; files do not. Applying
#   the same mode to both is a common mistake this script avoids by design.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   chown to an arbitrary owner requires root.
#
# Lock Acquisition
#   acquire_lock "wait" — waits rather than skipping. Share scans are long, and
#   this runs first in the daily window; skipping it would let arr cleanup run
#   against uncorrected ownership.
#
# Host Detection
#   detect_hosts() aliases HOST*_MEDIA_PERMISSION_SHARES to this host's shares.
#
# Empty Array Guard
#   Exits cleanly if no shares are configured for this host.
#
# Share Path Depth Guard
#   Every share must be an absolute path at least three levels deep before it is
#   scanned. A truncated entry like /mnt/user passes an existence check and would
#   chown and chmod every share on the array — which, because chown/chmod restamp
#   ctime, would erase the age signal the arr cleanups depend on across the whole
#   library in a single run.
#
# Folder Existence
#   Missing shares are skipped with a warning; remaining shares still process.
#
# Separate Passes
#   Directories and files are chmod'd in separate passes — directories need the
#   execute bit for traversal, media files must not have it.
#
# Conditional Passes
#   Only entries whose owner or mode is actually wrong are touched. This is not
#   an optimisation: chown/chmod rewrite an inode's ctime even when the value is
#   unchanged, so a blanket pass would restamp every file nightly and destroy
#   ctime as an age signal. The arr cleanups gate orphan deletion on ctime, and
#   mtime cannot substitute — imports preserve the release's original timestamp.
#   Making any pass unconditional silently stops orphan collection.
#
# Transcode Race Tolerance
#   "No such file or directory" errors from the file pass are ignored. Volatile
#   directories such as Emby transcodes delete files mid-scan; that is expected,
#   not a permissions failure.
#
# Dry Run Support
#   --dry-run counts the dirs, files and ownership entries that would change and
#   modifies nothing.
#
# Silent by Default
#   Only failures and diagnostics produce output; a clean run is quiet.
#
# Diagnostic — high corrected count on every run means a container has wrong PUID/PGID:
#   Correct values on unRAID: PUID=99 (nobody) PGID=100 (users)
#   Common culprits: SABnzbd, qBittorrent, slskd — check these first
#   Once fixed, this script should correct 0 files per run (pure failsafe)
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_MEDIA_PERMISSION_SHARES — shares this host applies permissions to
#   Aliased by detect_hosts() — script uses MEDIA_PERMISSION_SHARES
#
# master.conf
#
#   PERMISSIONS_DIR_MODE   — directory permissions (default 755)
#   PERMISSIONS_FILE_MODE  — file permissions (default 664)
#   PERMISSIONS_OWNER      — ownership applied to all files (default nobody:users)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
#   media_shares_permissions.sh              — normal run
#   media_shares_permissions.sh --dry-run    — preview without making changes
#   media_shares_permissions.sh --log        — verbose output
#   media_shares_permissions.sh --status     — show config and exit
#
# ==============================================================================================

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

source "$SCRIPT_DIR/../load_config.sh"

parse_args "$@"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi


acquire_lock "wait"

# detect_hosts() sets MY_ID and aliases HOST*_MEDIA_PERMISSION_SHARES
detect_hosts

# Empty array guard
if [[ ${#MEDIA_PERMISSION_SHARES[@]} -eq 0 ]]; then
    warn "MEDIA_PERMISSION_SHARES is empty for $MY_ID — nothing to do"
    warn "Check HOST*_MEDIA_PERMISSION_SHARES in host*.conf"
    exit 0
fi

[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST    Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_PERMS   Dir mode:   ${PERMISSIONS_DIR_MODE:-755}"
    echo "$ICON_PERMS   File mode:  ${PERMISSIONS_FILE_MODE:-664}"
    echo "$ICON_PERMS   Owner:      $PERMISSIONS_OWNER"
    echo "$ICON_PERMS   Shares:     ${#MEDIA_PERMISSION_SHARES[@]}"
    echo ""
    for share in "${MEDIA_PERMISSION_SHARES[@]}"; do
        local_status="missing"
        [[ -d "$share" ]] && local_status="exists"
        echo "    $share — $local_status"
    done
    echo "$ICON_GEAR    Dry Run:    $DRY_RUN"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Apply Permissions ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_PERMS Media Permissions — $MY_ID ━━━"
log "Dir mode:  ${PERMISSIONS_DIR_MODE:-755}"
log "File mode: ${PERMISSIONS_FILE_MODE:-664}"
log "Owner:     $PERMISSIONS_OWNER"
log "Shares:    ${#MEDIA_PERMISSION_SHARES[@]}"
echo ""

START=$(date +%s)
FAILED=()
UPDATED=()
SKIPPED=()
TOTAL_DIRS_FIXED=0
TOTAL_FILES_FIXED=0

# Split for find's -user/-group predicates, which take them separately
PERMISSIONS_USER="${PERMISSIONS_OWNER%%:*}"
PERMISSIONS_GROUP="${PERMISSIONS_OWNER##*:}"

for SHARE in "${MEDIA_PERMISSION_SHARES[@]}"; do
    SHARE_NAME=$(basename "$SHARE")

    # A truncated entry such as /mnt/user passes the -d check below and would chown/chmod
    # every share on the array. Because chown/chmod restamp ctime, that would erase the age
    # signal the arr cleanups gate orphan deletion on — across the whole library, in one run.
    _depth="${SHARE//[^\/]/}"
    if [[ -z "$SHARE" || "$SHARE" != /* || "${#_depth}" -lt 3 ]]; then
        error "Refusing to touch unsafe path: '${SHARE:-empty}' — expected an absolute path at least 3 levels deep"
        notify "Media permissions refused unsafe path on $(hostname): '${SHARE:-empty}'" \
            "Media Permissions" "warning"
        FAILED+=("${SHARE_NAME:-empty}")
        continue
    fi

    if [[ ! -d "$SHARE" ]]; then
        warn "$SHARE_NAME not found — skipping"
        SKIPPED+=("$SHARE_NAME")
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        # Count what would be changed without making changes
        DIR_COUNT=$(find "$SHARE" -type d ! -perm "${PERMISSIONS_DIR_MODE:-755}" \
            2>/dev/null | wc -l)
        FILE_COUNT=$(find "$SHARE" -type f ! -perm "${PERMISSIONS_FILE_MODE:-664}" \
            2>/dev/null | wc -l)
        OWNER_COUNT=$(find "$SHARE" \( ! -user "$PERMISSIONS_USER" -o ! -group "$PERMISSIONS_GROUP" \) \
            2>/dev/null | wc -l)
        warn "DRY RUN — $SHARE_NAME: $DIR_COUNT dirs, $FILE_COUNT files, $OWNER_COUNT ownership fixes needed"
        continue
    fi

    log "Updating $SHARE_NAME..."

    CHMOD_DIR_OK=true
    CHMOD_FILE_OK=true
    CHOWN_OK=true

    # Every pass below is conditional — it touches only entries that are actually wrong.
    # This is not just an optimisation. chown/chmod rewrite an inode's ctime even when the
    # value is unchanged, so a blanket pass restamps every file in the share each night and
    # erases ctime as an age signal. The arr cleanups need that signal to tell a file that
    # just landed from one that has sat untracked for days — mtime can't do it, because an
    # import preserves the release's original timestamp (measured 2026-07-27: 400 of 400
    # files imported that week had mtimes over 7 days old, one of them 9613 days).

    # Count files with wrong ownership before fixing (diagnostic)
    WRONG_OWNER=$(find "$SHARE" \( ! -user "$PERMISSIONS_USER" -o ! -group "$PERMISSIONS_GROUP" \) \
        2>/dev/null | wc -l)

    # Apply ownership first — affects all files and directories
    if [[ "$WRONG_OWNER" -gt 0 ]]; then
        find "$SHARE" \( ! -user "$PERMISSIONS_USER" -o ! -group "$PERMISSIONS_GROUP" \) \
            -exec chown "$PERMISSIONS_OWNER" {} + 2>/dev/null || CHOWN_OK=false
    fi

    # Apply directory permissions — separate pass for correctness
    # Directories need execute bit — different from files
    find "$SHARE" -type d ! -perm "${PERMISSIONS_DIR_MODE:-755}" \
        -exec chmod "${PERMISSIONS_DIR_MODE:-755}" {} + \
        2>/dev/null || CHMOD_DIR_OK=false

    # Apply file permissions — no execute bit on media files
    # Ignore "No such file" errors: race condition with volatile dirs (e.g. Emby transcodes)
    _chmod_errs=$(find "$SHARE" -type f ! -perm "${PERMISSIONS_FILE_MODE:-664}" \
        -exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>&1 | \
        grep -v "No such file or directory" | grep -c "chmod:" || true)
    [[ "$_chmod_errs" -gt 0 ]] && CHMOD_FILE_OK=false

    if [[ "$CHMOD_DIR_OK" == true && \
          "$CHMOD_FILE_OK" == true && \
          "$CHOWN_OK" == true ]]; then
        log "$ICON_UNLOCKED $SHARE_NAME — permissions applied"
        UPDATED+=("$SHARE_NAME")
        # Log diagnostic if many files had wrong ownership
        if [[ "$WRONG_OWNER" -gt 0 ]]; then
            warn "$SHARE_NAME — corrected $WRONG_OWNER file(s) with wrong ownership"
            warn "If this is high, check container PUID/PGID settings (should be PUID=99 PGID=100)"
        fi
        TOTAL_DIRS_FIXED=$(( TOTAL_DIRS_FIXED + 1 ))
        TOTAL_FILES_FIXED=$(( TOTAL_FILES_FIXED + WRONG_OWNER ))
    else
        error "$SHARE_NAME — permissions failed"
        error "  chown: $CHOWN_OK  chmod dirs: $CHMOD_DIR_OK  chmod files: $CHMOD_FILE_OK"
        FAILED+=("$SHARE_NAME")
    fi
done

END=$(date +%s)

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY MEDIA PERMISSIONS SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_PERMS   Dir mode:   ${PERMISSIONS_DIR_MODE:-755}"
echo "$ICON_PERMS   File mode:  ${PERMISSIONS_FILE_MODE:-664}"
echo "$ICON_PERMS   Owner:      $PERMISSIONS_OWNER"
echo "$ICON_TIME    Duration:   $(format_duration $(( END - START )))"
echo ""
[[ ${#UPDATED[@]} -gt 0 ]] && log "Updated: ${#UPDATED[@]} shares"
[[ ${#SKIPPED[@]} -gt 0 ]] && warn "Skipped: ${SKIPPED[*]} (not found)"
[[ ${#FAILED[@]} -gt 0 ]]  && echo "$ICON_ERROR    Failed:  ${FAILED[*]}"

# Diagnostic — high correction count indicates container PUID/PGID issue
if [[ "$TOTAL_FILES_FIXED" -gt 50 ]]; then
    warn "$TOTAL_FILES_FIXED files had wrong ownership this run"
    warn "High count suggests a container is not set to PUID=99 PGID=100"
    warn "Common culprits: SABnzbd, qBittorrent, slskd — check container env vars"
fi

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no changes made"
elif [[ ${#FAILED[@]} -gt 0 ]]; then
    echo "$ICON_ERROR    Status: SOME SHARES FAILED"
    notify "Media permissions failed on $(hostname) — ${FAILED[*]}" \
        "Media Permissions" "warning"
else
    echo "$ICON_DONE Status: done — ${#UPDATED[@]} shares updated"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

[[ ${#FAILED[@]} -gt 0 ]] && exit 1
exit 0