#!/bin/bash -
#===============================================================================
#
#         FILE: recover_transcripts
#
#        USAGE: ./recover_transcripts item
#
#  DESCRIPTION: Intended to be run on `borg`; collects assets from the
#               locally-mounted backup disk and places them in a local
#               directory (organised to be compatible with the IA), then
#               uploads anything that is missing on the IA.
#
#               Version 0.1.* looks for assets in the 'eps/' directory and
#               copies them to the cache. Also moves the IA copies so all is
#               aligned. Many shows earlier than mid 2019 are likely to need
#               this addition.
#
#      OPTIONS: ---
# REQUIREMENTS: ---
#         BUGS: ---
#        NOTES: ---
#       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
#      VERSION: 0.1.4
#      CREATED: 2024-07-14 13:22:58
#     REVISION: 2024-08-20 17:38:19
#
#===============================================================================

# set -o nounset                              # Treat unset variables as an error

VERSION="0.1.4"

SCRIPT=${0##*/}
# DIR=${0%/*}

STDOUT="/dev/fd/2"

#
# Select the appropriate working directory for the host
#
case $(hostname) in
    i7-desktop)
        echo "To be run only on 'borg'"
        exit 1
        ;;
    borg)
        BASEDIR="$HOME/IA"
        REPAIRS="$BASEDIR/repairs"
        BACKUP="/mnt/backup_disk/HPR/HPR-MIRROR"
        ;;
    *)
        echo "Wrong host!"
        exit 1
        ;;
esac

cd "$BASEDIR" || { echo "Failed to cd to $BASEDIR"; exit 1; }

#
# Load library functions
#
LIB="$HOME/bin/function_lib.sh"
[ -e "$LIB" ] || { echo "Unable to source functions"; exit; }
# shellcheck disable=SC1090
source "$LIB"

#
# Enable coloured messages
#
define_colours

#
# Sanity checks
#
JQ=$(command -v jq)
[ -n "$JQ" ] || { echo "Program 'jq' was not found"; exit 1; }
IA=$(command -v ia)
[ -n "$IA" ] || { echo "Program 'ia' was not found"; exit 1; }
REPIT="$BASEDIR/repair_item"
[ -e "$REPIT" ] || { echo "Program '$REPIT' was not found"; exit 1; }
IADB="$BASEDIR/ia.db"
[ -e "$IADB" ] || { echo "Database '$IADB' was not found"; exit 1; }

# {{{ -- Functions -- _IA_move, queued_tasks, _verbose, _usage


#===  FUNCTION  ================================================================
#         NAME: _IA_move
#  DESCRIPTION: Performs a file move on the IA, with retries if it fails.
#               Assumes the existence of functions 'coloured', '_log', 
#               '_verbose' and '_DEBUG'
#   PARAMETERS: $1      IA command to run (as a string)
#               $2      The path to move from
#               $3      The path to move to
#      RETURNS: False if the number of retries is exceeded, otherwise true.
#===============================================================================
_IA_move () {
    local from="${1:?Usage _IA_move command from to}"
    local to="${2:?Usage _IA_move command from to}"

    local retry_threshold=5
    local retries=0
    local sleeptime=20
    local command="ia move \"$from\" \"$to\" --no-derive --no-backup > /dev/null 2>&1"
    _DEBUG "$command"

    # coloured 'blue' "Moving $from to $to"

    #
    # Run 'command'. If it succeeds then exit. If it fails enter the 'until'
    # loop and report the problem, then sleep and try again. Count the number
    # of times this is done, so it doesn't loop forever. If we have reached
    # the limit count this as a failure and exit with an error.  If we haven't
    # retried enough yet, sleep for a while and try again. The intention is to
    # catch the case when an upload times out. The 'ia' command is performing
    # its own retries per upload when the system is overloaded, but these are
    # non-fatal.
    #
    until eval "$command"; do
        coloured 'red' "Failure when moving $from to $to"
        ((retries++))

        _log "$(printf 'Failed to move %s to %s [%d]' "$from" "$to" $retries)"

        [ "$retries" -eq "$retry_threshold" ] && {
            _verbose \
                "$(coloured 'red' "Retry limit reached; abandoning this move")"
            return 1
        }

        _verbose "$(coloured 'blue' "Pausing for $sleeptime seconds and retrying")"
        sleep $sleeptime
    done # until eval ...

    coloured 'green' "Moved $from to $to on the IA"
    _log "Moved $from to $to on the IA"

    return
}

#===  FUNCTION  ================================================================
#         NAME: queued_tasks
#  DESCRIPTION: Queries the IA for any queued or running tasks for an item.
#               Writes the number to STDOUT so it can be captured.
#   PARAMETERS: $1      IA item (like hpr1192)
#      RETURNS: Nothing
#===============================================================================
queued_tasks () {
    local item="${1:?Usage: queued_tasks item}"
    local -i count=0

    count="$(ia tasks "$item" |\
        jq -s '[.[] | if .category == "catalog" then .status else empty end] | length')"

    echo "$count"

    return
}

#===  FUNCTION  ================================================================
#         NAME: make_dir
#  DESCRIPTION: Make a directory if it doesn't exist, failing gracefully on
#               errors.
#   PARAMETERS: $1      directory path
#      RETURNS: True if success, otherwise exits the caller script
#===============================================================================
make_dir () {
    local dir="${1}"

    if [[ ! -d $dir ]]; then
        mkdir -p "$dir" || {
            coloured 'red' "Failed to create $dir"
            exit 1
        }
    fi
}

#===  FUNCTION  ================================================================
#         NAME: _ifbool
#  DESCRIPTION: Simplifies conditional expressions when they nned to return
#               one of two strings. Use as:
#               echo "Hello $(_ifbool 1 'World' 'Everyone')" → "Hello World"
#   PARAMETERS: $1      Integer being tested. If 1 then it's true, otherwise
#                       it's false. Non-numeric is treated as 0/false.
#               $2      String returned for True
#               $3      String returned for False
#      RETURNS: Nothing
#===============================================================================
_ifbool () {
    local -i _bool="${1:-0}"
    local _t="${2:-true}"
    local _f="${3:-false}"

    if [ "$_bool" -eq 1 ]; then
        echo "$_t"
    else
        echo "$_f"
    fi

    return
}

#===  FUNCTION  ================================================================
#         NAME: _log
#  DESCRIPTION: Appends a record to the file "$LOGFILE"
#   PARAMETERS: $1      Message to write
#      RETURNS: Nothing
#===============================================================================
_log () {
    local message="${1}"

    echo "$(date +%F\ %T) $message" >> "$LOGFILE"
}

#===  FUNCTION  ================================================================
#         NAME: _verbose
#  DESCRIPTION: Writes a message in verbose mode
#   PARAMETERS: *       message strings to write
#      RETURNS: Nothing
#===============================================================================
_verbose () {
    [ "$VERBOSE" -eq 0 ] && return
    for msg; do
        printf '%s\n' "$msg"
    done
}

#===  FUNCTION  ================================================================
#         NAME: _usage
#  DESCRIPTION: Reports usage; always exits the script after doing so
#   PARAMETERS: 1 - the integer to pass to the 'exit' command
#      RETURNS: Nothing
#===============================================================================
_usage () {
    local -i result=${1:-0}

    cat >$STDOUT <<-endusage
${SCRIPT} - version: ${VERSION}

Usage: ./${SCRIPT} [-h] [-D] [-F] [-v] item

Attempts to repair an IA item where the upload has failed for some reason.

Options:
  -h                    Print this help.
  -d 0|1                Dry run: -d 1 (the default) runs the script in dry-run
                        mode where nothing is changed but the actions that
                        will be taken are reported; -d 0 turns off dry-run
                        mode and the actions will be carried out.
  -D                    Run in debug mode where a lot more information is
                        reported.
  -F                    Ignore (some) interlocks that will cause failure, such
                        as the existence of the local cache directory for the
                        item being processed.
  -v                    Run in verbose mode where more information is
                        reported. Default is off.

Arguments:
    item                The item in the form 'hpr1234'

endusage
    exit "$result"
}

# }}}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#-------------------------------------------------------------------------------
# Directories and files
#-------------------------------------------------------------------------------
LOGS="$BASEDIR/logs"
make_dir "${LOGS}"
LOGFILE="$LOGS/$SCRIPT.log"

while getopts :d:DFhv opt
do
    case "${opt}" in
        D) DEBUG=1;;
        d) DRYRUN=$OPTARG;;
        F) FORCE=1;;
        h) _usage 0;;
        v) VERBOSE=1;;
        *) echo "** Unknown option"
           _usage 1;;
    esac
done
shift $((OPTIND - 1))

#
# Set option defaults and check their values
#
DRYRUN=${DRYRUN:-1}
if [[ $DRYRUN -ne 0 && $DRYRUN -ne 1 ]]; then
    echo "** Use '-d 0' or '-d 1'"
    _usage 1
fi
[[ $DRYRUN -eq 1 ]] && echo "Dry run mode"

DEBUG=${DEBUG:-0}
[[ $DEBUG -eq 1 ]] && coloured 'yellow' "Debug mode"

FORCE=${FORCE:-0}

VERBOSE=${VERBOSE:-0}

#
# Should have one argument
#
if [[ $# -ne 1 ]]; then
    coloured 'red' "Missing argument"
    _usage 1
fi
item="${1}"

#
# Ensure item spec is correctly formatted
#
if [[ $item =~ hpr([0-9]+) ]]; then
    printf -v item 'hpr%04d' "$((10#${BASH_REMATCH[1]}))"
else
    coloured 'red' "Incorrect show specification: $item"
    coloured 'yellow' "Use 'hpr9999' format"
    exit 1
fi
_DEBUG "Parsed item: $item"

_log "$SCRIPT $VERSION ($(_ifbool "$DRYRUN" 'dry-run' 'live'))"

#
# Having an entry for the show in 'ia.db' is important, so check there is one
#
SQL="select 1 from episodes where id = ${item:3}"
if [[ $(sqlite3 -list "$IADB" "$SQL" 2>/dev/null) -ne 1 ]]; then
    coloured 'red' "Unable to find show $item in the local IA database"
    coloured 'yellow' "Can't continue"
    exit 1
fi
_verbose "$(coloured 'yellow' "Show $item is in the local IA database")"
_log "Show $item is in the local IA database"

#
# It's possible that the show upload failed before anything was uploaded, even
# the metadata. It's never been seen, but it seems wise to cater for it.
#
# TODO: uncomment below; disabled for speed
if ! ia metadata "$item" --exists > /dev/null 2>&1; then
    coloured 'red' "This item is not apparently on the IA; can't continue"
    exit 1
fi
_verbose "$(coloured 'yellow' "Show $item is on the IA")"
_log "Show $item is on the IA"

#
# Directory paths
#
FROMPARENTDIR="$BACKUP/public_html/eps"
FROMDIR="$FROMPARENTDIR/$item"
TOPARENTDIR="$REPAIRS/$item"
TOASSETDIR="$TOPARENTDIR/$item"

#
# RE to ignore certain files using 'grep -v -E ...'
#
IGNORE="($item\.(flac|mp3|ogg|opus|spx|wav)$)"

#-------------------------------------------------------------------------------
# Check there are asset files on the backup disk before proceeding. At least
# we need the transcripts. If no files at all we can't continue.
#-------------------------------------------------------------------------------
declare -a BACKUPFILES
mapfile -t BACKUPFILES < \
    <(find "$FROMPARENTDIR" -type f -name "$item*" | grep -v -E "${IGNORE}")
_DEBUG "$(coloured 'purple' "Backup files")" "${BACKUPFILES[@]}"

if [[ ! -d $FROMDIR || ${#BACKUPFILES[@]} -eq 0 ]]; then
    coloured 'red' "No files found in $FROMDIR"
    coloured 'red' "Can't continue!"
    exit 1
fi

_log "Files found on backup disk ${#BACKUPFILES[*]}"

#-------------------------------------------------------------------------------
# Make the needed local cache directory for later
#-------------------------------------------------------------------------------
if [[ $FORCE -ne 1 && -e $TOPARENTDIR ]]; then
    coloured 'red' "Directory $TOPARENTDIR already exists; can't continue".
    coloured 'yellow' 'This implies that all files have been copied already.'
    coloured 'yellow' "If you're sure, consider running: '$REPIT -X -d0 $item'"
    coloured 'yellow' 'Otherwise, consider running again with option -F.'
    exit 1
else
    if [[ $DRYRUN -eq 1 ]]; then
        coloured 'yellow' "Would have created directory $TOPARENTDIR"
    else
        mkdir -p "$TOASSETDIR"
        _verbose "$(coloured 'yellow' "Created directory $TOASSETDIR")"
        _log "Created directory $TOASSETDIR"
    fi
fi

#-------------------------------------------------------------------------------
# Collect asset data from the database
#-------------------------------------------------------------------------------
SQL="select filename from assets where episode_id = ${item:3}"
declare -a IADBASSETS
mapfile -t IADBASSETS < <(sqlite3 -list "$IADB" "$SQL" 2>/dev/null)
_DEBUG "$(coloured 'purple' "SQLite IA DB files")" "${IADBASSETS[@]}"
_log "Files found in ia.db ${#IADBASSETS[*]}"

#-------------------------------------------------------------------------------
# Collect IA data, only original files generated by HPR. We exclude audio
# files from this set.
#-------------------------------------------------------------------------------
JQPROG='.files[] | select(.source == "original" and .format != "Metadata" and '
JQPROG+='.format != "Item Tile") | (.name) | @text'
declare -a IAFILES
mapfile -t IAFILES < \
    <(ia metadata "$item" | $JQ -r "$JQPROG" | grep -v -E "${IGNORE}")
_DEBUG "$(coloured 'purple' "IA files (originals)")" "${IAFILES[@]}"
_log "Files found on IA (originals) ${#IAFILES[*]}"

#-------------------------------------------------------------------------------
# Work out whether to copy assets from the backup disk, or whether to move
# files on the IA. Whatever we decide we also need to copy transcripts from
# the backup disk and upload to the IA
#-------------------------------------------------------------------------------
#
# Check each asset from the $IADB database to see if it's on the IA. We'll get back
# a path if it's where we want it, otherwise just a filename.
#
declare -a MOVES

coloured 'purple' "Checking IA files for moves"

#
# If we find an asset by looking for its basename in the list of files we got
# from the IA and if they are the same we need to move such files to the
# sub-directory.
#
for asset in "${IAFILES[@]}"; do
    #
    # Skip IA files with directories
    #
    if [[ $asset =~ / ]]; then
        continue
    fi

    IA_match=$( grep "${asset}" <(printf '%s\n' "${IADBASSETS[@]}") )
    if [[ $IA_match = "$asset" ]]; then
        MOVES+=("$IA_match")
    fi
done

#
# If we found any moves then we can move them in the IA item now and copy the
# files from the backup disk to the cache in case we need them. They will
# eventually get deleted by 'cron'.
#
if [[ ${#MOVES[@]} -gt 0 ]]; then
    _DEBUG "$(coloured 'purple' "Files to be moved")" "${MOVES[@]}" "----"

    mcount=0
    for asset in "${MOVES[@]}"; do
        # source & destination for IA moves
        iafrom="$item/$asset"
        iato="$item/$item/$asset"
        _DEBUG "\$iafrom: $iafrom" "\$iato:   $iato" ""

        #
        # If IA source and destination are the same no moves are needed. For
        # the local cache the later 'rsync' will be enough.
        #
        if [[ $iafrom != "$iato" ]]; then
            if [[ $DRYRUN -eq 1 ]]; then
                coloured 'yellow' "ia move $iafrom $iato --no-derive --no-backup"
                coloured 'yellow' "cp $FROMPARENTDIR/$asset $TOASSETDIR/"
            else
                #
                # Perform the move. If the retries are exceeded things get
                # complicated, so just abort so we can try again later.
                #
                _verbose "$(coloured 'blue' "Moving $iafrom → $iato on IA")"
                if _IA_move "$iafrom" "$iato"; then
                    #
                    # Update the cache (but only if the move occurred)
                    #
                    _verbose "$(coloured 'blue' "Copying from backup disk to cache")"
                    cp "$FROMPARENTDIR/$asset" "$TOASSETDIR/"

                    ((mcount++))
                else
                    coloured 'red' "Retries exhausted. Aborting recovery"
                    exit 1
                fi
            fi
        fi

    done
    #
    # Report what was done
    #
    coloured 'green' "Moved $mcount $(ngettext file files "$mcount")"
    _log "Moved $mcount $(ngettext file files "$mcount")"

else
    coloured 'yellow' "No moves needed"
    _log "No moves needed"
fi

#
# Wait for the IA moves to finish
#
if [[ $DRYRUN -eq 0 ]]; then
    if [[ $mcount -gt 0 ]]; then
        until [[ $(queued_tasks "$item") -eq 0 ]]; do
            coloured 'yellow' "Waiting for IA tasks to complete"
            sleep 1m
        done
    fi
else
    if [[ $mcount -gt 0 ]]; then
        coloured 'yellow' "Would have waited for any IA tasks to complete"
    fi
fi

#-------------------------------------------------------------------------------
# Copy files from the backup disk to the cache
#-------------------------------------------------------------------------------
if [[ $DRYRUN -eq 1 ]]; then
    coloured 'yellow' "Would have copied files from backup disk → cache"
    rsync -n -vaP --exclude=index.html "$FROMDIR" "$TOPARENTDIR"
else
    rsync -vaP --exclude=index.html "$FROMDIR" "$TOPARENTDIR"
    _verbose "$(coloured 'yellow' "Copied files from $FROMDIR")"
    _log "Copied files from $FROMDIR"
fi

# TODO: Is this needed?
#
# Put any source audio in the right place.
#
# if [[ $DRYRUN -eq 1 ]]; then
#     coloured 'yellow' "Would have moved source files if found"
# else
#     #
#     # Turn on 'nullglob' to get an empty result if the glob expression doesn't
#     # match.
#     #
#     NG=$(shopt -p nullglob)
#     shopt -s nullglob
#
#     #
#     # Any source files should be in repairs/hpr1234/ and should go to the IA
#     # in the comparable place. We will not put it on the HPR server though.
#     #
#     # TODO: Is this right?
#     movecount=0
#     for file in "$TOPARENTDIR"/*_source.*; do
#         if mv "$file" "$TOPARENTDIR"; then
#             ((movecount++))
#         fi
#     done
#
#     eval "$NG"
#
#     #
#     # Show the directories after any move
#     #
#     if [[ $movecount -gt 0 ]]; then
#         _verbose "$(coloured 'yellow' "Moved source file(s)")"
#         ls -lR "$REPAIRS/$item/"
#     fi
#
# fi

#-------------------------------------------------------------------------------
# Using the cache as the reference upload whatever is missing to the IA
#-------------------------------------------------------------------------------
if [[ $DRYRUN -eq 1 ]]; then
    coloured 'yellow' "Would have found and repaired missing files"
else
    _verbose "$(coloured 'yellow' "Finding and repairing missing files")"
    _log "Finding and repairing missing files (with $REPIT)"
    "$REPIT" -X -d0 "$item"
fi

# vim: syntax=sh:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21:fdm=marker

