#!/bin/bash
#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE
#------------------------------------------------------------------------------

#==============================================================================
# Declarations and Environment

# Allow override of P4U_HOME, which is set only when testing P4U scripts.
export P4U_HOME="${P4U_HOME:-/p4/common/bin}"
export P4U_LIB="${P4U_LIB:-/p4/common/lib}"
export P4U_ENV="$P4U_LIB/p4u_env.sh"
export VERBOSITY=${VERBOSITY:-3}

# Environment isolation.  For stability and security reasons, prepend
# PATH to include dirs where known-good scripts exist.
# known/tested PATH and, by implication, executables on the PATH.
export PATH="$P4U_HOME:$PATH:~/bin:."
export P4CONFIG="${P4CONFIG:-.p4config}"

[[ -r "$P4U_ENV" ]] || {
   echo -e "\\nError: Cannot load environment from: $P4U_ENV\\n\\n"
   exit 1
}

declare BASH_LIBS=$P4U_ENV
BASH_LIBS+=" $P4U_LIB/libcore.sh"
BASH_LIBS+=" $P4U_LIB/libp4u.sh"

for bash_lib in $BASH_LIBS; do
   # shellcheck disable=SC1090
   source "$bash_lib" ||\
      { echo -e "\\nFATAL: Failed to load bash lib [$bash_lib]. Aborting.\\n"; exit 1; }
done

declare Version=1.1.1
export VERBOSITY=3

#==============================================================================
# Local Functions

#------------------------------------------------------------------------------
# Function: terminate
function terminate
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   vvmsg "$THISSCRIPT: EXITCODE: $OverallReturnStatus"

   # Stop logging.
   [[ "${P4U_LOG}" == off ]] || stoplog

   # Don't litter.
   cleanTrash

   # With the trap removed, exit.
   exit "$OverallReturnStatus"
}

#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional).  Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1).  If displaying an
# errror, usually $1 should be -h so that the longer usage message doesn't
# obsure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare errorMessage=${2:-Unset}

   if [[ "$errorMessage" != Unset ]]; then
      echo -e "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
   fi

   echo "USAGE for $THISSCRIPT v$Version:

$THISSCRIPT [-i <instance>] [-L <log>] [-v<n>] [-p|-n] [-D]

or

$THISSCRIPT [-h|-man|-V]
"
   if [[ $style == -man ]]; then
      echo -e "
DESCRIPTION:
	This script obsoletes the SetDefaultDepotSpecMapField.py trigger.

	It does so by following a series of steps.  First, it ensures that
	the configurable server.depot.root is set correctly, setting it
	is not set.

	Next, the Triggers table is checked to ensure the call to the
	SetDefaultDepotSpecMapField.py is not called; it is deleted from
	the Triggers table if found.

	Last, it resets the 'Map:' field of depot specs for depot
	types where that is appropriate, setting it to the default value of
	'<DepotName>/...', so that it honors the server.depot.root
	configruable.  This is done for depots of these types:

	* stream
	* local
	* spec
	* unload

	but not these:
	* unload
	* remote
	* graph

	If an unknown depot type is encountered, the Map field is reset
	as well if it is set.

	This script does a preflight check first, reporting any cases
	where the starting conditions are not as expected, such as. These
	are treated as Warnings:

	* Configurable server.depot.root is already set
	* SetDefaultDepotSpecMapField.py not found in triggers
	* Depot already has Map field set to the default <DepotName>/...

	These conditions are treated as Errors, and will abort processing:
	* Depot Map field set to something other than the default.
	* Configurable server.depot.root is set, but to something other
	than what it should be.

OPTIONS:
 -v<n>	Set verbosity 1-5 (-v1 = quiet, -v5 = highest).

 -L <log>
	Specify the path to a log file, or the special value 'off' to disable
	logging.  By default, all output (stdout and stderr) goes to
	EDITME_DEFAULT_LOG

	NOTE: This script is self-logging.  That is, output displayed on the screen
	is simultaneously captured in the log file.  Do not run this script with
	redirection operators like '> log' or '2>&1', and do not use 'tee.'

 -p	Run preflight checks only, and then stop. By default, actual changes
	occur if preflight checks find no issues.

 -n	No-Op.  No actions are taken that would affect data significantly;
	instead commands are displayed rather than executed.

 -D     Set extreme debugging verbosity.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message
 -V	Dispay version info for this script and its libraries.

EXAMPLES:
	A typical flow for this script is to do a preflight first, and then
	a live run, for any given instance:
	$THISSCRIPT -i 1 -p
	$THISSCRIPT -i 1

	Note that if using '-n', the '-v5' flag should also be used.
"
   fi

   exit 1
}

#==============================================================================
# Command Line Processing

declare SDPInstance=Unset
declare SDPEnvFile="/p4/common/bin/p4_vars"
declare SDPInstanceCfgFile=
declare -i PreflightOnly=0
declare -i RemoveOldTrigger=1
declare -i SetSDRConfigurable=1
declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-i) SDPInstance="$2"; shiftArgs=1;;
      (-h) usage -h;;
      (-man) usage -man;;
      (-V) show_versions; exit 1;;
      (-v1) export VERBOSITY=1;;
      (-v2) export VERBOSITY=2;;
      (-v3) export VERBOSITY=3;;
      (-v4) export VERBOSITY=4;;
      (-v5) export VERBOSITY=5;;
      (-L) export P4U_LOG=$2; shiftArgs=1;;
      (-p) PreflightOnly=1;;
      (-n) export NO_OP=1;;
      (-D) set -x;; # Debug; use 'set -x' mode.
      (*) usage -h "Unknown arg ($1).";;
   esac

   # Shift (modify $#) the appropriate number of times.
   shift; while [[ $shiftArgs -gt 0 ]]; do
      [[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
      shiftArgs=$shiftArgs-1
      shift
   done
done
set -u

#==============================================================================
# Command Line Verification

# If not set on the command line with -i, default SDPInstance to the
# $SDP_INSTANCE environment variable, if defined.
[[ "$SDPInstance" == "Unset" ]] && SDPInstance="${SDP_INSTANCE:-Unset}"

[[ "$SDPInstance" == "Unset" ]] && \
   bail "The '-i <SDP_Instance>' argument is required unless \$SDP_INSTANCE is defined in the environment."

SDPInstanceCfgFile="/p4/common/config/p4_${SDPInstance}.vars"

[[ -r "$SDPInstanceCfgFile" ]] || \
   bail "Missing SDP instance config file: $SDPInstanceCfgFile"

# Load the standard SDP shell environment for the given instance.
# shellcheck disable=SC1090
source "$SDPEnvFile" "$SDPInstance"

[[ "${P4U_LOG:-Unset}" == Unset ]] && \
   P4U_LOG="${LOGS:-/tmp}/${THISSCRIPT%.sh}.$(date +'%Y%m%d-%H%M%S').log"

#==============================================================================
# Main Program

trap terminate EXIT SIGINT SIGTERM

declare -i OverallReturnStatus=0
declare DepotType=
declare Depot=
declare ActualMapFieldValue=
declare ExpectedMapFieldValue=
declare SDRExpectedValue="/p4/$SDPInstance/depots"
declare SDRActualValue=
declare TmpFile1="$P4TMP/tmp1.$$.$RANDOM"
declare TmpFile2="$P4TMP/tmp2.$$.$RANDOM"
declare TmpPrefix="$P4TMP/${THISSCRIPT/.sh/.tmp}"
declare -i ErrorCount=0
declare -i WarningCount=0

if [[ "${P4U_LOG}" != off ]]; then
   touch "${P4U_LOG}" || bail "Couldn't touch log file [${P4U_LOG}]."

   # Redirect stdout and stderr to a log file.
   exec > >(tee "${P4U_LOG}")
   exec 2>&1

   initlog
fi

msg "Started $THISSCRIPT v$Version at $(date)."

msg "${H2}\\nPreflight Check for super access."

[[ "$("$P4BIN" protects -m)" == "super" ]] || \
   bail "Super access for $P4USER could not be verified. Aborting."

msg "Super user access for user $P4USER verified."

msg "${H2}\\nPreflight Check for server.depot.root configurable."
SDRActualValue=$("$P4BIN" -ztag -F "%Value%" configure show server.depot.root)

if [[ -z "$SDRActualValue" ]]; then
   msg "The configurable server.depot.root is not (yet) set, as expected."
elif [[ "$SDRActualValue" == "$SDRExpectedValue" ]]; then
   warnmsg "The configurable server.depot.root is already set to the intended value."
   WarningCount+=1
   SetSDRConfigurable=0
else
   errmsg "The configurable server.depot.root is set to an unexpected value [$SDRActualValue], expected value is [$SDRExpectedValue]."
   WarningCount+=1
fi

msg "${H2}\\nPreflight Checks for Depot Spec Map Fields:"
# Note: graph depots aren't reported by 'p4 depots' with flags
# given here, so we don't need any special processing to ignore them
# (as server.depot.root does not apply to them).
for data in $("$P4BIN" -ztag -F "%name%:%type%" depots); do
   vvmsg "d=[$data]"
   [[ "$data" == *":"* ]] || bail "Unexpected output. Bailling."
   Depot="${data%%:*}"
   DepotType="${data##*:}"
   vvmsg "D=[$Depot] T=[$DepotType]"

   case "$DepotType" in
      (archive|remote)
         vmsg "Skipping depot $Depot of type $DepotType."
         continue
      ;;
      (stream|local|spec|unload)
         vmsg "Processing depot $Depot of type $DepotType."
      ;;
      (*)
         warnmsg "Processing depot $Depot of UKNOWN type $DepotType."
         WarningCount+=1
      ;;
   esac

   ExpectedMapFieldValue="/p4/$SDPInstance/depots/$Depot/..."
   DefaultMapFieldValue="$Depot/..."
   ActualMapFieldValue=$("$P4BIN" -ztag -F "%Map%" depot -o "$Depot")

   if [[ "$ActualMapFieldValue" == "$DefaultMapFieldValue" ]]; then
      warnmsg "Depot $Depot already has default map field value of $DefaultMapFieldValue."
      WarningCount+=1
   else
      echo "$ActualMapFieldValue" > "$TmpFile1"
      echo "$ExpectedMapFieldValue" > "$TmpFile2"
      if ! diff -q "$TmpFile1" "$TmpFile2"; then
         echo "DBG AV=[$ActualMapFieldValue]"
         errmsg "Depot $Depot has unexpected map field value of: $ActualMapFieldValue"
         ErrorCount+=1
      else
         msg "Depot $Depot has expected Map field value of: $ActualMapFieldValue"
      fi
   fi
done

msg "${H2}\\nPreflight Check for trigger script SetDefaultDepotSpecMapField.py."
"$P4BIN" triggers -o > "$TmpFile1"
if grep -i -q SetDefaultDepotSpecMapField.py "$TmpFile1"; then
   msg "Trigger SetDefaultDepotSpecMapField.py detected, as expected."
else
   warnmsg "Missing trigger SetDefaultDepotSpecMapField.py (which was about to be deleted anyway)."
   WarningCount+=1
   RemoveOldTrigger=0
fi

msg "Preflight checks discovered $ErrorCount errors and $WarningCount warnings."

[[ "$ErrorCount" -gt 0 ]] && \
   bail "Preflivhg checkes FAILED.\\nAborting due to errors found in preflight checks."

msg "Preflight checks PASSED."

if [[ "$PreflightOnly" -eq 1 ]]; then
   msg "Exiting after preflight checks due to '-p'."
   exit "$OverallReturnStatus"
fi

msg "${H2}\\nUpdating configurable server.depot.root."
# Set the configurable server.depot.root. If this fails, we bail. This
# is the first change to actual data in our processing, an so we can
# bail here without foregoing idempotency.
if [[ "$SetSDRConfigurable" -eq 1 ]]; then
   run "$P4BIN configure set server.depot.root=$SDRExpectedValue" \
      "Setting configurable server.depot.root to: $SDRExpectedValue" ||\
      bail "Failed to set server.depot.root configurable."
else
   msg "Configurable server.depot.root is already set to the expected value."
fi

msg "${H2}\\nUpdating triggers table."
if [[ "$RemoveOldTrigger" -eq 1 ]]; then
   "$P4BIN" triggers -o | grep -v '^#' | grep -i -v SetDefaultDepotSpecMapField.py > "$TmpFile1" ||\
      bail "Failed to get triggers table."
   run "$P4BIN -s triggers -i < $TmpFile1" \
      "Loading updated table with SetDefaultDepotSpecMapField.py removed."

   if [[ "$CMDEXITCODE" -ne 0 ]]; then
      errmsg "Failed to udpate triggers table."
      ErrorCount+=1
      OverallReturnStatus=1
   fi
else
   msg "Triggers table update not required, SetDefaultDepotSpecMapField.py not found."
fi

msg "${H2}\\nUpdating depot spec Map fields."
# Failures in this next block updating depot spec Map fields, will be
# reported but will not cause an abort. Manual post-processing would be
# required if there are failures here.
for data in $("$P4BIN" -ztag -F "%name%:%type%" depots); do
   vvmsg "d=[$data]"
   [[ "$data" == *":"* ]] || bail "Unexpected output. Bailling."
   Depot="${data%%:*}"
   DepotType="${data##*:}"

   case "$DepotType" in
      (archive|remote) continue ;;
      (stream|local|spec|unload) true;;
      (*)
         warnmsg "Processing depot $Depot of UKNOWN type $DepotType."
         WarningCount+=1
      ;;
   esac

   ExpectedMapFieldValue="/p4/$SDPInstance/depots/$Depot/..."
   DefaultMapFieldValue="$Depot/..."
   ActualMapFieldValue=$("$P4BIN" -ztag -F "%Map%" depot -o "$Depot")

   if [[ "$ActualMapFieldValue" == "$DefaultMapFieldValue" ]]; then
      msg "Skipping update of depot $Depot; it is already correct."
   else
      # Get the current depot spec, sans the 'Map:' field value.
      "$P4BIN" depot -o "$Depot" | grep -v '^#' | grep -v '^Map:' \
         > "$TmpPrefix.$Depot.p4s"

      # Append the correct Map field value to the end of the spec file.
      echo "Map: $DefaultMapFieldValue" >> "$TmpPrefix.$Depot.p4s"

      # Load the updated spec.
      run "$P4BIN -s depot -i < $TmpPrefix.$Depot.p4s" \
         "Updating depot spec for depot $Depot."

      if [[ "$CMDEXITCODE" -ne 0 ]]; then
         errmsg "Failed to load this updated depot spec:$(cat "$TmpPrefix.$Depot.p4s")"
         ErrorCount+=1
         OverallReturnStatus=1
      fi
   fi
done

# Be tidy
for f in "$TmpFile1" "$TmpFile2" "$TmpPrefix"*; do GARBAGE+="$f"; done

if [[ "$OverallReturnStatus" -eq 0 ]]; then
   msg "${H}\\nAll processing completed successfully.\\n"
else
   msg "${H}\\nProcessing completed, but with errors.  Scan above output carefully.\\n" 
fi

# Illustrate using $SECONDS to display runtime of a script.
msg "That took $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\\n"

# See the terminate() function, which is really where this script exits.
exit "$OverallReturnStatus"
