#!/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

# Version ID Block. Relies on +k filetype modifier.
#------------------------------------------------------------------------------
# shellcheck disable=SC2016
declare VersionID='$Id: //p4-sdp/dev_c2s/tools/sdp_jobs_report.sh#2 $ $Change: 31472 $'
declare VersionStream=${VersionID#*//}; VersionStream=${VersionStream#*/}; VersionStream=${VersionStream%%/*};
declare VersionCL=${VersionID##*: }; VersionCL=${VersionCL%% *}
declare Version=${VersionStream}.${VersionCL}
[[ "$VersionStream" == r* ]] || Version="${Version^^}"

export TOOLS_DIR="${TOOLS_DIR:-$PWD}"

# 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=$TOOLS_DIR:$PATH:.
export P4CONFIG=${P4CONFIG:-.p4config}

declare ThisScript="${0##*/}"
declare ReportStyle="OpenJobs"
declare ReportStyleTitle=
declare JobSearchQuery=
declare WorkshopUser="Unset"
declare JobStatusValues=
declare MaxTitleLength=70
declare -i ShowUserJobsOnly=0
declare -i SilentMode=0
declare -i JobCount=0
declare -i ErrorCount=0
declare -i LogCounter=1
declare JobList=
declare -a Jobs
declare Log=
declare LogLink="${ThisScript%.sh}.log"
declare -i UserLogSet=0
declare -i GenCSVFile=0
declare CSVFile=
declare Verbosity=3
declare H="=============================================================================="

#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
function vmsg () { [[ "$Verbosity" -ge 4 ]] || return; msg "$*"; }
function vvmsg () { [[ "$Verbosity" -ge 5 ]] || return; msg "$*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "${2:-1}"; }

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

   vvmsg "$ThisScript: EXITCODE: $ErrorCount"

   # Stop logging.
   [[ "${Log}" == off ]] || msg "\\nLog is: $Log\\n${H}"

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

#------------------------------------------------------------------------------
# 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
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure 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
      msg "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
   fi

   msg "USAGE for $ThisScript version $Version:

$ThisScript [-a|-o] [-me | -u <workshop_user>] [-st <status1>[,<status2>,...]] [-csv] [-L <log>] [-si] [-v<n>] [-n] [-D]

or

$ThisScript [-h|-man]
"
   if [[ $style == -man ]]; then
      echo -e "
DESCRIPTION:
	This script generates a report of SDP jobs.

OPTIONS:
 -a	Show all SDP jobs regardless of the state.

 -o	Show only \"Open\" SDP jobs, which includes jobs in these states:
        * open
        * inprogress
        * blocked

	This skips jobs in these states:
	* closed
        * duplicate
        * fixed (optional status to use before closed).
	* punted
        * obsolete
	* suspended

	The '-o' behaviour is the default.

 -me	Show only jobs for which the OwnedBy setting is the current user.
	Guessing logic is applied to map the current OS user to a Workshop account,
	e.g. ttyler -> tom_tyler, etc.  This works for some SDP project members.

	See the guess_workshop_user() function in env.sh for details.

	Works with '-a' and '-o'.

 -u <workshop_user>
	Show only jobs for which the OwnedBy setting is the specified Workshop user
	account.

	Specify the special value 'none' to list unassigned jobs, i.e. those for
	which the OwnedBy field is not set.

	Works with '-a' and '-o'.

 -st <status1>[,<status2>,...]
	Specify a comma-delimited list of status values to include.

	The valid values can be seen by doing: p4 jobspec -o

 -csv	Generate a CSV file form of the report in addition to the standard report.
	This is intended to support JIRA import.

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

	The default is -v3.

	Specify -v4 to see the query used with the 'p4 jobs -e' command.

 -L <log>
	Specify the path to report log file, or the special value 'off' to disable
	logging.  By default, all output (stdout and stderr) goes to a file, then
	name of which is displayed when the script starts.

	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.'

	In addition to the log file, this script updates a log symlink with a
	fixed name, $LogLink, that points to the unique log file name.

-si	Operate silently.  All output (stdout and stderr) is redirected to the log
	only; no output appears on the terminal.  This cannot be used with
	'-L off'.
      
 -n	No-Op.  Prints commands instead of running them.

 -D     Set extreme debugging verbosity.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message

EXAMPLES:
	Typical usage is with no arguments, to display open jobs:

	$ThisScript

	Show opened jobs assigned to me (based on the \$WORKSHOP_USER shell
	environment setting).

	$ThisScript -me

	Show only items listed as inprogress:

	$ThisScript -st inprogress
"
   fi

   exit 2
}

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

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-a) ReportStyle="AllJobs";;
      (-o) ReportStyle="OpenJobs";;
      (-me) ShowUserJobsOnly=1; WorkshopUser="me";;
      (-u) ShowUserJobsOnly=1; WorkshopUser="$2"; shiftArgs=1;;
      (-st) JobStatusValues="$2"; shiftArgs=1;;
      (-csv) GenCSVFile=1;;
      (-h) usage -h;;
      (-man) usage -man;;
      (-v1) Verbosity=1;;
      (-v2) Verbosity=2;;
      (-v3) Verbosity=3;;
      (-v4) Verbosity=4;;
      (-v5) Verbosity=5;;
      (-L) Log="$2"; UserLogSet=1; shiftArgs=1;;
      (-si) SilentMode=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

# shellcheck disable=SC1090 disable=SC1091
source "$TOOLS_DIR/env.sh"

[[ $SilentMode -eq 1 && "$Log" == off ]] && \
   usage -h "Cannot use '-si' with '-L off'."

[[ -n "$Log" ]] || Log="$PWD/jobs_report.${WORKSHOP_PROJECT_TAG}.$(date +'%Y%m%d-%H%M').txt"

[[ "$WorkshopUser" == "me" ]] && WorkshopUser="$(guess_workshop_user)"

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

trap terminate EXIT SIGINT SIGTERM

if [[ "$Log" != off ]]; then
   # Initialize a unique log file.  If the user specified a non-default log name with
   # '-L', move any existing log by that name aside so the new log will have the
   # intended name.  If the log name is the default timestamp-based log file name,
   # and that file already exists (e.g. due to concurrent operations), increment a
   # counter baked into the name until we find an available log name to use.
   if [[ "$UserLogSet" -eq 1 ]]; then
      if [[ -e "$Log" ]]; then
         MovedLog="${Log%.log}.moved.$LogCounter.$$.log"
	 while [[ -e "$MovedLog" ]]; do
            LogCounter+=1
            MovedLog="${Log%.log}.moved.$LogCounter.$$.log"
         done
      fi
   else
      while [[ -e "$Log" ]]; do
         Log="$PWD/jobs_report.${WORKSHOP_PROJECT_TAG}.$(date +'%Y%m%d-%H%M').${LogCounter}.$$.txt"
         LogCounter+=1
      done
   fi

   touch "$Log" || bail "Couldn't touch log file [$Log]."

   # Target the fixed-name log symlink to the unique log file.
   if [[ -L "$LogLink" ]]; then
      rm -f "$LogLink" || bail "Could not do: rm -f \"$LogLink\""
   elif [[ -e "$LogLink" ]]; then
      mv -f "$LogLink" "$LogLink.moved.$(date +'%Y%m%d-%H%M').log" ||\
         bail "Could not do this to move this file to make way for a symlink: mv -v \"$LogLink\""
   fi

   ln -s "${Log##*/}" "$LogLink" || bail "Couldn't do: ln -s \"${Log##*/}\" \"$LogLink\""

   # Redirect stdout and stderr to a log file.
   if [[ $SilentMode -eq 0 ]]; then
      exec > >(tee "$Log")
      exec 2>&1
   else
      exec >"$Log"
      exec 2>&1
   fi

   msg "${H}\\nLog is: $Log"

   CSVFile="${Log%.log}.csv"
else
   CSVFile="${LogLink}.csv"
fi

msg "Started $ThisScript version $Version as $(id -u -n)@$(hostname -s) on $(date)."

if [[ $ReportStyle == "OpenJobs" ]]; then
   ReportStyleTitle="Open Jobs"

   if [[ -n "$JobStatusValues" ]]; then
      # Query to show only SDP jobs in specific states.
      if [[ "$JobStatusValues" =~ /,/ ]];then
         # Query for a single job status
         JobSearchQuery="Project=$WORKSHOP_PROJECT Status=$JobStatusValues"
      else
         # Query for multiple job status values
         JobSearchQuery="Project=$WORKSHOP_PROJECT ("
         declare -i first=1
         for status in ${JobStatusValues//,/ }; do
            if [[ $first -eq 1 ]]; then
               first=0
               JobSearchQuery+="Status=$status"
            else
               JobSearchQuery+="|Status=$status"
            fi
         done
         JobSearchQuery+=")"
      fi
   else
      # Query to show only "Open" SDP jobs. See usage info for this script and
      # 'p4 jobspec -o' for details on all states.
      JobSearchQuery="Project=$WORKSHOP_PROJECT ^Status=closed ^Status=duplicate ^Status=fixed ^Status=obsolete ^Status=punted ^Status=suspended"
   fi
else
   ReportStyleTitle="Open Jobs"

   # Query to show all SDP jobs.
   JobSearchQuery="Project=$WORKSHOP_PROJECT"
fi

[[ "$ShowUserJobsOnly" -eq 1 ]] && ReportStyleTitle+=" owned by $WorkshopUser"

vmsg "Job Search Query: $JobSearchQuery"

printf "SDP Jobs Report showing $ReportStyleTitle\\n\\n %-8s %-12s %-2s %-16s %-16s %-12s %-s\\n" "Job" "Status" "Pr" "Reporter" "Owner" "Component" "Title"
printf " %-8s %-12s %-2s %-16s %-16s %-12s %-s\\n" "--------" "------------" "--" "----------------" "----------------" "------------" "----------------------------------------------------------------------"

if [[ "$GenCSVFile" -eq 1 ]]; then
   echo "IssueKey,Status,Priority,ReportedBy,Owner,Component,Title,Description,DevNotes" > "$CSVFile" ||\
      bail "Could not generate CSV file: $CSVFile"
fi

# Find jobs matching our query.
for job in $(p4 -ztag -F %Job% jobs -e "$JobSearchQuery"); do
   JobList+="${job##*-} "
done

# Sort the jobs list nicely.
for jobNum in $(echo "$JobList" | tr ' ' '\n' | sort -n); do
   job="$(echo "$WORKSHOP_PROJECT_TAG" | awk '{ print toupper($0) }')-$jobNum"
   Jobs[$JobCount]="$job"
   JobCount+=1
done

for job in ${Jobs[*]}; do
   jobData=$(p4 -ztag -F "%ReportedBy%:%OwnedBy%:%Component%:%Status%:%Severity%" job -o "$job")
   reportedBy=$(echo "$jobData" | cut -d ':' -f 1)
   ownedBy=$(echo "$jobData" | cut -d ':' -f 2)
   component=$(echo "$jobData" | cut -d ':' -f 3)
   status=$(echo "$jobData" | cut -d ':' -f 4)
   priority=$(echo "$jobData" | cut -d ':' -f 5)

   # Skip jobs by users other than the current user of '-me' was specified.
   if [[ $ShowUserJobsOnly -eq 1 ]]; then
      if [[ "$WorkshopUser" == "none" ]]; then
         [[ -n "$ownedBy" ]] && continue
      else
         [[ "$ownedBy" != "$WorkshopUser" ]] && continue
      fi
   fi

   jobTitle=$(p4 -ztag -F %Description% job -o "$job" | head -1)
   jobTitle=$(echo "$jobTitle" | cut -c -$MaxTitleLength)
   printf " %-8s %-12s %-2s %-16s %-16s %-12s %-s\\n" "$job" "$status" "$priority" "$reportedBy" "$ownedBy" "$component" "$jobTitle"

   if [[ "$GenCSVFile" -eq 1 ]]; then
      fullDescription=$(p4 -ztag -F %Description% job -o "$job" | tr '\n' ' ' | tr ',' ';')
      devNotes=$(p4 -ztag -F %DevNotes% job -o "$job" | tr '\n' ' ' | tr ',' ';')
      # echo "IssueKey,Status,Priority,ReportedBy,Owner,Component,Title,Description,DevNotes" > "$CSVFile" ||\
      echo "$job,$status,$priority,$reportedBy,$ownedBy,$component,$jobTitle,$fullDescription,$devNotes" >> "$CSVFile" ||\
         errmsg "Problem writing data to: $CSVFile"
   fi
   JobCount+=1
done

msg "${H}\\n$JobCount jobs reported in $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\n"

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