#!/bin/bash
# ==============================================================================================
# ============================= Lidarr Release Fixer ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Fix Lidarr albums where the wrong MusicBrainz release edition was selected,
# causing on-disk files to appear as unimported despite being present.
#
# Root cause: Lidarr tracks one specific release edition per album using
# foreignReleaseId. When this doesn't match the MUSICBRAINZ_ALBUMID embedded
# in the actual files, track ID lookup fails and RescanFolders imports 0 tracks
# even with perfect, fully tagged files.
#
# Fix: Read the MusicBrainz Album ID from the first FLAC or MP3 found in each
# album directory, look that release up in Lidarr's known releases for that
# album, switch monitored=true to the correct one, then queue a RefreshArtist
# to re-sync track IDs and trigger Lidarr's own import scan.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each monitored album with 0 tracked files:
#   1. Locate the album directory under the artist's root path (title glob match)
#   2. Find the first FLAC or MP3 file in that directory
#   3. Read MUSICBRAINZ_ALBUMID from the file's tags
#   4. Fetch the album's available releases from Lidarr
#   5. If the file's release exists and differs from Lidarr's current selection:
#      — PUT the album with the correct release set to monitored=true
#      — Mark the artist for a RefreshArtist command
#
# RefreshArtist is batched — one per artist, even if multiple albums were fixed.
# Lidarr handles the post-refresh rescan and import automatically.
#
# Cache-first artist list (2026-07-17) — comes from the shared tracked-data cache via
# arr_get_tracked_data(), fresh (kept warm every 30min by arr_cache_prefill.sh), live fetch
# as fallback. Everything below (per-album release lookups) still fetches live — that data
# isn't part of what's cached.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Fixer Before Cleanup
#   Runs before lidarr_cleanup.sh in the daily job list. The strike system in
#   cleanup provides a protection window, but correcting releases first means
#   files that were wrongly treated as orphans get imported rather than aged out.
#
# No Blind Fixes
#   Only switches to a release that is already in Lidarr's known release list for
#   that album. If the file's MBID isn't a recognized release, the album is skipped
#   rather than guessed at. False corrections are worse than leaving it alone.
#
# Files Are Never Touched
#   This script only modifies the Lidarr database record (release selection). Audio
#   files, tags, and directory structure are read-only. All actual importing is
#   handled by Lidarr after RefreshArtist runs.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
#   check_api                — API reachability verified before any processing
#   check_arr_version        — aborts if Lidarr major version doesn't match LIDARR_VERSION_MAJOR
#   artist count > 0         — aborts if artist fetch returns empty (API anomaly guard)
#   release list validation  — only fixes to releases explicitly known to Lidarr for that album
#   acquire_lock "skip"      — skips if another instance is already running
#   detect_hosts()           — exits cleanly on hosts without Lidarr configured
#   curl + jq + perl check   — fail fast if any required tool is missing
#
# ==============================================================================================
# TAG READING
# ==============================================================================================
#
# FLAC — Vorbis comment block (block type 4), key MUSICBRAINZ_ALBUMID
# MP3  — ID3v2 TXXX frame, description "MusicBrainz Album Id"
#         Supports Latin-1, UTF-8 (enc 0/3) and UTF-16 (enc 1/2) encodings
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY / HOST1_LIDARR_MUSIC_ROOT
#   HOST1_LIDARR_PATH_MAP — container path → host path translation
#   All aliased by detect_hosts() — script uses unprefixed names
#
# master.conf
#
#   LIDARR_RELEASE_FIXER_ENABLED — set false to disable without removing from job list
#   LIDARR_VERSION_MAJOR         — expected Lidarr major version for API safety check
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
#   lidarr_release_fixer.sh              — normal run
#   lidarr_release_fixer.sh --dry-run    — preview, no API writes
#   lidarr_release_fixer.sh --log        — verbose output
#   lidarr_release_fixer.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

for _cmd in curl jq perl; do
    if ! command -v "$_cmd" >/dev/null 2>&1; then
        error "$_cmd not found — required"
        exit 1
    fi
done
unset _cmd

acquire_lock "skip"
trap "_release_all_locks" EXIT

detect_hosts

if [[ -z "${LIDARR_URL:-}" ]] || [[ -z "${LIDARR_API_KEY:-}" ]]; then
    info "Lidarr not configured on $MY_ID — skipping"
    exit 0
fi

if [[ "${LIDARR_RELEASE_FIXER_ENABLED:-true}" == "false" ]]; then
    info "LIDARR_RELEASE_FIXER_ENABLED=false — skipping"
    exit 0
fi

declare -A ARR_PATH_MAP
local_path_map_var="${MY_ID}_LIDARR_PATH_MAP"
eval "for key in \"\${!${local_path_map_var}[@]}\"; do
    ARR_PATH_MAP[\"\$key\"]=\"\${${local_path_map_var}[\$key]}\"
done"

require_var LIDARR_URL
require_var LIDARR_API_KEY
require_var LIDARR_MUSIC_ROOT

# ── Status ────────────────────────────────────────────────────────────────────────────────────
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST    Identity:    $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_GEAR    Lidarr URL:  $LIDARR_URL"
    echo "$ICON_GEAR    Music root:  $LIDARR_MUSIC_ROOT"
    echo "$ICON_GEAR    Enabled:     ${LIDARR_RELEASE_FIXER_ENABLED:-true}"
    echo "$ICON_GEAR    Dry run:     $DRY_RUN"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ── API helpers ───────────────────────────────────────────────────────────────────────────────
lidarr_api() {
    local endpoint="$1"
    local response http_code body
    response=$(curl -sf --max-time 30 \
        -H "X-Api-Key: $LIDARR_API_KEY" \
        -w "\n%{http_code}" \
        "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null)
    http_code=$(echo "$response" | tail -1)
    body=$(echo "$response" | head -n -1)
    if [[ "$http_code" != "200" ]]; then
        error "Lidarr API HTTP $http_code for: $endpoint"
        return 1
    fi
    echo "$body"
}

lidarr_api_put() {
    local endpoint="$1"
    local payload="$2"
    local http_code
    http_code=$(curl -sf --max-time 30 -X PUT \
        -H "X-Api-Key: $LIDARR_API_KEY" \
        -H "Content-Type: application/json" \
        -d "$payload" \
        -w "%{http_code}" -o /dev/null \
        "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null)
    if [[ "$http_code" != "202" ]] && [[ "$http_code" != "200" ]]; then
        error "Lidarr PUT HTTP $http_code for: $endpoint"
        return 1
    fi
}

lidarr_api_post() {
    local endpoint="$1"
    local payload="$2"
    curl -sf --max-time 30 -X POST \
        -H "X-Api-Key: $LIDARR_API_KEY" \
        -H "Content-Type: application/json" \
        -d "$payload" \
        -o /dev/null \
        "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null
}

# ── Tag readers ───────────────────────────────────────────────────────────────────────────────
# Read MUSICBRAINZ_ALBUMID from a FLAC file's Vorbis comment block (block type 4)
read_flac_mbid() {
    perl -e '
        open(my $fh, "<:raw", $ARGV[0]) or exit;
        read($fh, my $magic, 4); substr($magic, 0, 4) eq "fLaC" or exit;
        while (1) {
            read($fh, my $hdr, 4) == 4 or last;
            my $w    = unpack("N", $hdr);
            my $last = ($w >> 31) & 1;
            my $type = ($w >> 24) & 0x7f;
            my $len  = $w & 0xffffff;
            read($fh, my $data, $len);
            if ($type == 4) {
                my $pos = 0;
                my $vl = unpack("V", substr($data, $pos, 4)); $pos += 4 + $vl;
                my $n  = unpack("V", substr($data, $pos, 4)); $pos += 4;
                for (1..$n) {
                    my $cl = unpack("V", substr($data, $pos, 4)); $pos += 4;
                    my $c  = substr($data, $pos, $cl); $pos += $cl;
                    if ($c =~ /^MUSICBRAINZ_ALBUMID=(.+)$/i) { print "$1\n"; exit; }
                }
            }
            last if $last;
        }
    ' "$1" 2>/dev/null
}

# Read MusicBrainz Album Id from an MP3's ID3v2 TXXX frame.
# Handles Latin-1/UTF-8 (enc 0/3) with single-null separator and
# UTF-16 (enc 1/2) by stripping null bytes and pattern-matching.
read_mp3_mbid() {
    perl -e '
        open(my $fh, "<:raw", $ARGV[0]) or exit;
        read($fh, my $hdr, 10) == 10 or exit;
        substr($hdr, 0, 3) eq "ID3" or exit;
        my $ver = ord(substr($hdr, 3, 1));
        my $sz = 0; $sz = ($sz << 7) | ord($_) for split //, substr($hdr, 6, 4);
        read($fh, my $data, $sz) == $sz or exit;
        my $pos = 0;
        while ($pos + 10 <= $sz) {
            my $id = substr($data, $pos, 4); $pos += 4;
            last unless $id =~ /^[A-Z][A-Z0-9]{3}$/;
            my $fs;
            if ($ver >= 4) {
                my $n = 0; $n = ($n << 7) | ord($_) for split //, substr($data, $pos, 4);
                $fs = $n;
            } else {
                $fs = unpack("N", substr($data, $pos, 4));
            }
            $pos += 6;
            last if $fs < 1 || $pos + $fs > $sz;
            if ($id eq "TXXX") {
                my $enc  = ord(substr($data, $pos, 1));
                my $body = substr($data, $pos + 1, $fs - 1);
                if ($enc == 1 || $enc == 2) {
                    # UTF-16: strip BOM, collapse to ASCII, pattern match
                    $body =~ s/^\xff\xfe|^\xfe\xff//;
                    (my $flat = $body) =~ s/\x00//g;
                    if ($flat =~ /^MusicBrainz Album Id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i) {
                        print "$1\n"; exit;
                    }
                } else {
                    my ($desc, $val) = split /\x00/, $body, 2;
                    if (defined $desc && lc($desc) eq "musicbrainz album id" &&
                        defined $val &&
                        $val =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) {
                        print "$val\n"; exit;
                    }
                }
            }
            $pos += $fs;
        }
    ' "$1" 2>/dev/null
}

read_file_mbid() {
    local file="$1"
    case "${file##*.}" in
        [Ff][Ll][Aa][Cc]) read_flac_mbid "$file" ;;
        [Mm][Pp]3)        read_mp3_mbid  "$file" ;;
    esac
}

# ── Safety checks ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_SHIELD Safety Checks ━━━"

if ! check_api "$LIDARR_URL" "Lidarr" 10; then
    notify "Lidarr release fixer aborted on $(hostname) — API unreachable" \
        "Lidarr Release Fixer" "warning"
    exit 1
fi
check_arr_version "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$LIDARR_VERSION_MAJOR" "Lidarr" || exit 1
info "API reachable and version OK"

# ── Fetch all artists and build path cache ────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_SYNC Fetching artist paths ━━━"

declare -A ARTIST_PATH_CACHE
declare -A ARTIST_NAME_CACHE

# Cache-first — arr_get_tracked_data() serves the shared cache when it's fresh (now kept
# current every 30min by arr_cache_prefill.sh in CRITICAL_MAINTENANCE_SCRIPTS), falls back to
# a live fetch when it's stale, and waits out an active rescan before either.
ALL_ARTISTS=$(arr_get_tracked_data "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1") || {
    error "Failed to fetch artists"
    exit 1
}

while IFS= read -r artist; do
    aid=$(echo "$artist" | jq -r '.id')
    apath=$(echo "$artist" | jq -r '.path // empty')
    aname=$(echo "$artist" | jq -r '.artistName // empty')
    [[ -n "$apath" ]] && ARTIST_PATH_CACHE[$aid]=$(translate_path "$apath")
    [[ -n "$aname" ]] && ARTIST_NAME_CACHE[$aid]="$aname"
done < <(echo "$ALL_ARTISTS" | jq -c '.[]' 2>/dev/null)
unset ALL_ARTISTS

ARTIST_COUNT="${#ARTIST_PATH_CACHE[@]}"
info "Loaded $ARTIST_COUNT artists"

if [[ "$ARTIST_COUNT" -eq 0 ]]; then
    error "No artists returned — aborting"
    exit 1
fi

# ── Fetch zero-file albums ────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_SYNC Fetching zero-file monitored albums ━━━"

ALL_ALBUMS=$(lidarr_api "album") || {
    error "Failed to fetch albums"
    exit 1
}

ZERO_FILE_ALBUMS=$(echo "$ALL_ALBUMS" | jq -c \
    '[.[] | select(.monitored == true and .statistics.trackFileCount == 0)]' 2>/dev/null)
unset ALL_ALBUMS

TOTAL_ZERO=$(echo "$ZERO_FILE_ALBUMS" | jq 'length' 2>/dev/null)
info "Monitored albums with 0 tracked files: $TOTAL_ZERO"

# ── Process albums ────────────────────────────────────────────────────────────────────────────
START=$(date +%s)
FIXED=0
SKIPPED_NO_DIR=0
SKIPPED_NO_FILE=0
SKIPPED_NO_MBID=0
SKIPPED_NO_MATCH=0
SKIPPED_CORRECT=0
ERRORS=0
declare -A ARTISTS_TO_REFRESH

echo ""
echo "━━━ $ICON_GEAR Processing albums ━━━"

while IFS= read -r album; do
    album_id=$(echo "$album" | jq -r '.id')
    album_title=$(echo "$album" | jq -r '.title')
    artist_id=$(echo "$album" | jq -r '.artistId')
    artist_name="${ARTIST_NAME_CACHE[$artist_id]:-artist $artist_id}"

    log "Checking: $artist_name — $album_title (album $album_id)"

    artist_host_path="${ARTIST_PATH_CACHE[$artist_id]:-}"
    if [[ -z "$artist_host_path" ]] || [[ ! -d "$artist_host_path" ]]; then
        log "Artist dir not found: $artist_host_path"
        (( SKIPPED_NO_DIR++ ))
        continue
    fi

    # Find album directory — case-insensitive prefix match on title
    album_dir=$(find "$artist_host_path" -maxdepth 1 -type d -iname "${album_title}*" \
        2>/dev/null | head -1)
    if [[ -z "$album_dir" ]]; then
        log "Album dir not found: $album_title"
        (( SKIPPED_NO_DIR++ ))
        continue
    fi

    # Find first FLAC or MP3
    music_file=$(find "$album_dir" -maxdepth 2 -type f \
        \( -iname "*.flac" -o -iname "*.mp3" \) 2>/dev/null | head -1)
    if [[ -z "$music_file" ]]; then
        log "No FLAC or MP3 in: $album_dir"
        (( SKIPPED_NO_FILE++ ))
        continue
    fi

    # Read MBID from file tags
    file_mbid=$(read_file_mbid "$music_file")
    if [[ -z "$file_mbid" ]]; then
        log "No MBID tag in: $music_file"
        (( SKIPPED_NO_MBID++ ))
        continue
    fi
    log "File MBID: $file_mbid"

    # Fetch full album JSON (includes releases array)
    album_full=$(lidarr_api "album/${album_id}") || { (( ERRORS++ )); sleep 0.2; continue; }
    sleep 0.1

    # Currently selected release
    current_release_id=$(echo "$album_full" | jq -r \
        '.releases[] | select(.monitored == true) | .foreignReleaseId' 2>/dev/null | head -1)

    if [[ "$current_release_id" == "$file_mbid" ]]; then
        log "Release already correct: $file_mbid"
        (( SKIPPED_CORRECT++ ))
        continue
    fi

    # Verify file's MBID is a known release for this album
    release_known=$(echo "$album_full" | jq -r \
        --arg rid "$file_mbid" \
        '.releases[] | select(.foreignReleaseId == $rid) | .foreignReleaseId' 2>/dev/null)
    if [[ -z "$release_known" ]]; then
        log "File MBID $file_mbid not in Lidarr release list for: $album_title"
        (( SKIPPED_NO_MATCH++ ))
        continue
    fi

    # Build updated album object with correct release selected
    updated_album=$(echo "$album_full" | jq \
        --arg target "$file_mbid" \
        '.releases = [.releases[] | .monitored = (.foreignReleaseId == $target)]' 2>/dev/null)

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would fix: $artist_name — $album_title"
        warn "  $current_release_id → $file_mbid"
        (( FIXED++ ))
        continue
    fi

    if lidarr_api_put "album/${album_id}" "$updated_album"; then
        warn "$ICON_GEAR Fixed: $artist_name — $album_title"
        warn "  $current_release_id → $file_mbid"
        ARTISTS_TO_REFRESH[$artist_id]="$artist_id"
        (( FIXED++ ))
    else
        (( ERRORS++ ))
    fi
    sleep 0.2

done < <(echo "$ZERO_FILE_ALBUMS" | jq -c '.[]')

# ── Queue RefreshArtist for all fixed artists ─────────────────────────────────────────────────
if [[ "${#ARTISTS_TO_REFRESH[@]}" -gt 0 ]] && [[ "$DRY_RUN" == false ]]; then
    echo ""
    echo "━━━ $ICON_SYNC Queuing RefreshArtist ━━━"
    for artist_id in "${!ARTISTS_TO_REFRESH[@]}"; do
        if lidarr_api_post "command" \
            "{\"name\": \"RefreshArtist\", \"artistId\": ${artist_id}}"; then
            log "Queued RefreshArtist for: ${ARTIST_NAME_CACHE[$artist_id]:-$artist_id}"
        else
            error "Failed to queue RefreshArtist for artist $artist_id"
        fi
        sleep 0.1
    done
    info "Refresh queued for ${#ARTISTS_TO_REFRESH[@]} artist(s)"
fi

# ── Summary ───────────────────────────────────────────────────────────────────────────────────
END=$(date +%s)
echo ""
echo "━━━━━ $ICON_SUMMARY LIDARR RELEASE FIXER SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:          $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR    Candidates:        $TOTAL_ZERO zero-file albums"
echo "$ICON_DONE    Fixed:             $FIXED"
echo "$ICON_SKIP    Already correct:   $SKIPPED_CORRECT"
echo "$ICON_SKIP    No dir on disk:    $SKIPPED_NO_DIR"
echo "$ICON_SKIP    No music file:     $SKIPPED_NO_FILE"
echo "$ICON_SKIP    No MBID tag:       $SKIPPED_NO_MBID"
echo "$ICON_SKIP    MBID not in list:  $SKIPPED_NO_MATCH"
[[ "$ERRORS" -gt 0 ]] && echo "    Errors:            $ERRORS"
echo "$ICON_TIME    Duration:          $(format_duration $(( END - START )))"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

if [[ "$FIXED" -gt 0 ]] && [[ "$DRY_RUN" == false ]]; then
    notify "Lidarr release fixer on $(hostname) — corrected $FIXED album release(s)" \
        "Lidarr Release Fixer" "normal"
fi

exit 0
