#!/bin/bash
# Warning: all variable assignments in function should be changed to local variable. Don't learn from me! (I'm lazy)

_self_bin_name="$0"
function where_is_him () {
    SOURCE="$1"
    while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
        DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
        SOURCE="$(readlink "$SOURCE")"
        [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
    done
    DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
    echo -n "$DIR"
}

function where_am_i () {
    _my_path=`type -p ${_self_bin_name}`
    [[ "$_my_path" = "" ]] && where_is_him "$_self_bin_name" || where_is_him "$_my_path"
}

source "$(where_am_i)/antidote.config.sh" || ! echo "Failed to source config file" || exit 2

function echo2 () {
    echo "$@" 1>&2
}

function curl_wrapped () {
    # Add some options for every curl request, basing on config file. 
    curl_options=( "--http1.1" --max-time 30 ) # Old version of curl failed to fallback to http1.1 on http2 error. 
    [[ "$cis_bearer" != "" ]] && curl_options+=( -H "Authorization: $cis_bearer" )
    [[ "$cis_cookie" != "" ]] && curl_options+=( --cookie "$cis_cookie" )
    [[ "$cis_bearer$cis_cookie" = "" ]] && echo2 "Warning: You must set either cis_cookie or cis_bearer to authenticate your CIS API call, but you have set neither. curl requests is very likely to fail!"

    curl "${curl_options[@]}" "$@"
    return $?
}

##############################################################################

function upload_to_smb_share () {
    # returns smb_url from stdout
    release_ver="$1"
    local_wf_dir="$2"

    # if local_wf_dir is already a samba url, we do nothing and return. 
    [[ "${local_wf_dir:0:2}" = \\\\ ]] || [[ "${local_wf_dir:0:2}" = // ]] && echo "$local_wf_dir" && return 0

    # The workflow builder should copy all xaml file to there. 
    find "$local_wf_dir" -name '*.pdb' -exec rm -f '{}' ';'
    cmd="deltree /antidote-cis/$smb_namespace/$release_ver;mkdir /antidote-cis;mkdir /antidote-cis/$smb_namespace;mkdir /antidote-cis/$smb_namespace/$release_ver;mask \"\";recurse ON;prompt OFF;cd /antidote-cis/$smb_namespace/$release_ver;lcd $local_wf_dir;mput *"
    
    hash smbclient || ! echo "smbclient is not installed in this environment. Pushing from local directory is unavailable. Please push from existing smbshare with 'antidote-cis push //MY_SHARE/my/shared/folder'. " || exit 3
    smbclient "$smb_provider" --user "$smb_username%$smb_password" -c "$cmd" 1>&2 || return 2
    smb_url="$smb_provider/antidote-cis/$smb_namespace/$release_ver"
    echo "$smb_url"
}

# CIS upload pkg
function cis_upload_pkg () {
    workflow_name="$1"
    release_ver="$2"
    pkg_path="$3"

    if [[ "${pkg_path:0:6}" = "cis://" ]]; then
        echo2 "Submitting request to upload package $cis_namespace.$workflow_name:$release_ver, using CIS internal package (trigger argless upload)..."
        curl_wrapped -s --data '[{"Cloud":"Public","PackageType":"'"$cis_namespace.$workflow_name"'","Version":"'"$release_ver"'","PackageId":"00000000-0000-0000-0000-000000000000","ActionType":501,"Notes":"antidote-auto-upload","ActionArgs":{"PackageSourcePath":null,"ScriptParameters":""}}]' "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages/Request" -H 'content-type: application/json'
    else
        smb_url=`upload_to_smb_share "$release_ver" "$pkg_path"` || return $?
        echo2 "Submitting request to upload package $cis_namespace.$workflow_name:$release_ver, using smb:$smb_url..."
        curl_wrapped -s --data '[{"Cloud":"Public","PackageType":"'"$cis_namespace.$workflow_name"'","Version":"'"$release_ver"'","PackageId":"00000000-0000-0000-0000-000000000000","ActionType":501,"Notes":"antidote-auto-upload","ActionArgs":{"PackageSourcePath":"'"$smb_url"'","ScriptParameters":""}}]' "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages/Request" -H 'content-type: application/json'
    fi
    return $?
}

function try_grep_pkgid () {
    # returns pkgid from stdout
    workflow_name="$1"
    release_ver="$2"
    resp=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages?null"` || return $?
    [[ "$resp" = "" ]] && echo2 "Invalid api response. Is cookie expired or not correctly set?" && return 2
    ! echo "$resp" | json2table Id,Version -p | grep -F "|$release_ver|" > /dev/null && echo "Error: release_ver $release_ver doesn't exist at all" 1>&2 && return 2

    # output pkgid
    pkgid=`echo "$resp" | json2table Id,Version -p | grep -v 00000000-0000-0000-0000-000000000000 | grep -F "|$release_ver|" | sed 's/|.*$//g'`
    [[ "$pkgid" = "" ]] && return 2
    echo "$pkgid"
}


# The only difference between release_pkg and set_default is: ActionType. One is 505, another is 503. 
function cis_release_pkg () {
    workflow_name="$1"
    release_ver="$2"
    echo2 "Release package $cis_namespace.$workflow_name:$release_ver..."
    pkg_id=`try_grep_pkgid "$workflow_name" "$release_ver"` || ! echo2 "Failed to get pkgid. Does release version exist?" || return $?
    curl_wrapped -s --data '[{"Cloud":"Public","PackageType":"'"$cis_namespace.$workflow_name"'","Version":"'"$release_ver"'","PackageId":"'"$pkg_id"'","ActionType":505,"Notes":"antidote-auto-release","ActionArgs":{"PackageSourcePath":null,"ScriptParameters":""}}]' "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages/Request" -H 'content-type: application/json'
    return $?
}
function cis_set_default_pkgver () {
    workflow_name="$1"
    release_ver="$2"
    echo2 "Setting default version for $cis_namespace.$workflow_name:$release_ver..."
    pkg_id=`try_grep_pkgid "$workflow_name" "$release_ver"` || ! echo2 "Failed to get pkgid. Does release version exist?" || return $?
    curl_wrapped -s --data '[{"Cloud":"Public","PackageType":"'"$cis_namespace.$workflow_name"'","Version":"'"$release_ver"'","PackageId":"'"$pkg_id"'","ActionType":503,"Notes":"antidote-auto-release","ActionArgs":{"PackageSourcePath":null,"ScriptParameters":""}}]' "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages/Request" -H 'content-type: application/json'
    return $?
}

# CIS show pkg list
# GET https://beta-cps.trafficmanager.net/cis.client.svc/Public/M365FleetAGCTest.E2EPOCBuildPortalWorkflow/Packages?null

# CIS wait upload pkg success
function cis_wait_for_upload () {
    workflow_name="$1"
    release_ver="$2"
    echo2 "Waiting for CIS to create package..."
    while true; do
        try_grep_pkgid "$workflow_name" "$release_ver" && break # success
        echo2 -n .
        sleep 5
    done
}

function _select_param_and_create_json_impl () {
    # pick parameter from cis_default_workflow_parameter, and format them into result format. 
    # input: stdin, list of requested parameter name. Output: stdout. 
    while read -r requiredParam; do
        [[ "$requiredParam" = "" ]] && continue
        prefix="$requiredParam="
        for kv in "${cis_default_workflow_parameter[@]}"; do
            [[ "$kv" == "$prefix"* ]] && requiredParam_val="${kv:${#prefix}}" && requiredParam_val="${requiredParam_val/\\/\\\\}" # The backslash in json must be escaped. 
        done
        [[ "$requiredParam_val" = "" ]] && echo2 "Note: workflow parameter $requiredParam has an empty value."
        echo -n "${result_param_string}\"$requiredParam\":\"$requiredParam_val\","
    done
}
function assemble_workflow_parameters () {
    # Use variable $cis_default_workflow_parameter, query workflow parameter list, and fill them. 
    # The tail of array variable $cis_default_workflow_parameter has higher preference (because params from command line are appended after tail)
    workflow_name="$1"
    params_query_response=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/JobTypeDefinition/$cis_namespace"  | json2table WorkflowDefinitions/InputParameters,Name -p | grep "|$workflow_name|" | cut -d '|' -f 1` || ! echo2 "Failed to get Workflow definition for workflow $workflow_name" || return $?
    [[ "$params_query_response" = "[]" ]] && params="" || params=`echo "$params_query_response" | json2table Name -p | sed 's/VAL: //g' | grep -oE '[A-Za-z0-9_-]+'` || ! echo2 "Failed to get Workflow definition for workflow $workflow_name" || return $?

    echo "$params" | _select_param_and_create_json_impl
}
function assemble_runtime_settings () {
    # Use variable $cis_default_workflow_parameter, query runtime settings list, and fill them. 
    # TODO: Many runtime settings have default value, I'll append them into the front of cis_default_workflow_parameter in the FUTURE! 
    # The tail of array variable $cis_default_workflow_parameter has higher preference (because params from command line are appended after tail)

    # runtime_settings are related to namespace, so we don't need workflow_name. (We're adding an '@' to all param name here)
    params=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/JobTypeDefinition/$cis_namespace" | json2table GlobalSettings/Name -p | sed 's/VAL: /@/g' | tr -d '|'` || ! echo2 "Failed to get runtime settings for namespace $cis_namespace" || return $?

    echo "$params" | _select_param_and_create_json_impl | tr -d @
}

function cis_run_job () {
    workflow_name="$1"
    release_ver="$2"
    echo2 "Submitting request to create $workflow_name:$release_ver..."
    pkg_id=`try_grep_pkgid "$workflow_name" "$release_ver"` || ! echo2 "Failed to get pkgid" || return $?
    workflow_params=`assemble_workflow_parameters "$workflow_name"` || ! echo2 "Failed to assemble wf_parameter" || return $?
    runtime_settings=`assemble_runtime_settings` || ! echo2 "Failed to assemble runtime_settings" || return $?
    echo2 "Using workflow parameters: $workflow_params, runtime settings: $runtime_settings"
    job_id=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace/GenericJob?workflowDefinitionName=$workflow_name" -H 'content-type: application/json' --data '{"JobType":"'"$cis_namespace"'","Workflow":"'"$workflow_name"'","ServiceHostCapability":"AzureJB","ServiceHostLocation":"Any","RuntimePackageId":"4dd32e8b-6bc7-478e-a1ab-48fcdc1bf3d3","PackageId":"'"$pkg_id"'","DisplayName":"Antidote-autosubmitted-WF","WorkflowParameters":{'"$workflow_params"'},"RuntimeSettings":{'"$runtime_settings"'},"WorkflowSettings":{"RuntimeSettings":{'"$runtime_settings"'}}}'` || ! echo2 "failed to get job_id" || return $?
    [[ "$job_id" =~ ^[0-9][0-9]*_[0-9a-f-]*$ ]] || ! echo2 "Failed to create job. Invalid cookie or invalid workflow_name?" || return 3
    
    echo2 "Waiting for CIS to create job... (job_id=$job_id)"
    niddle_should_break='"DisplayStatus":"NotStarted"'
    niddle_should_crash='"CustomState":"Internal Error"'
    while true; do
        resp=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace/GenericJob/$job_id/GetJobHierarchy?null"`
        echo "$resp" | grep "$niddle_should_break" > /dev/null && break
        echo "$resp" | grep "$niddle_should_crash" > /dev/null && echo2 "CIS reported 'Internal Error' while creating job. https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$job_id" && return 3
        echo2 -n .
        sleep 5
    done
    
    echo2 "Starting job..."
    # There's some bug report saying that, this job-starting request failed. Let's validate and retry. 
    while true; do
        # http 204 means success. 
        curl_wrapped "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace/GenericJob/$job_id/Start/" -X 'PUT' -H 'content-type: application/json' -H 'accept: application/json, text/javascript, */*; q=0.01' --data-raw '{"Notes":"antidote-autostart"}' -v 3>&2 2>&1 1>&3 | grep -F '204 No Content' > /dev/null && break
        sleep 2
    done
    echo2 "Job started. log_url = https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$job_id"

    echo "$job_id" # This is the output
    return $?
}

function cis_get_job_status () {
    # Argument should be job_id of *root* workflow. (not child workflow!)
    jobid="$1"
    getjobhierarchy_apires=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace/GenericJob/$jobid/GetJobHierarchy"` && 
    subworkflows_text=`echo "$getjobhierarchy_apires" | json2table DisplayName,Id,DisplayStatus -p` || ! echo2 "API GetJobHierarchy failed" || return $?
    if inprogress_callstack=`echo "$subworkflows_text" | grep -F '|InProgress|'`; then
        # There's some workflow in-progress. It maybe inprogress or blocked. 
        # Note that, there may be multiple workflows in "InProgress" state. In this scenario, 
        #   the last "InProgress" workflow is the bottom sub-workflow which we're interested in. 
        #   We just check the last sub-workflow, to see if there's any "Blocked" activity. 

        bottom_subworkflow_id=`echo "$inprogress_callstack" | tail -n 1 | cut -d '|' -f 3` && 
        bottom_wf_pagedtasks_apires=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace/GenericJob/$bottom_subworkflow_id/WithPagedTasksAndIncidents"` && 
        bottom_wf_activities=`echo "$bottom_wf_pagedtasks_apires" | json2table Data/Tasks/DisplayName,StateName` || ! echo2 "API WithPagedTasksAndIncidents failed" || return $?
        if echo "$bottom_wf_activities" | tr -d ' ' | grep -F '|Blocked|' > /dev/null; then
            # I found some activity blocked! Show incident and exit. 
            callstack_formatted=`echo "$getjobhierarchy_apires" | json2table DisplayName,Id,DisplayStatus | grep -F ' InProgress '` || ! echo2 "API GetJobHierarchy failed" || return $?
            icm_link=`echo "$bottom_wf_pagedtasks_apires" | json2table Data/Tasks/Incidents/ExternalLink -p | sed 's/VAL: //g' | tr -d '|'` || icm_link=""
            _mdcode='```'
            [[ ! -v ANTIDOTE_JOBSTAT_MARKDOWN ]] &&
                echo2 -e ">>> Incident detected:\nCallstack: \n$callstack_formatted\nActivity: \n$bottom_wf_activities\nCIS Log: https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$jobid\nICM Link: $icm_link" ||
                echo2 -e "## Incident detected\n\n[View CIS Log](https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$jobid)\n\n### Callstack\n\n$_mdcode\n$callstack_formatted\n$_mdcode\n\n### Activity\n\n$_mdcode\n$bottom_wf_activities\n$_mdcode\n[View ICM Incident]($icm_link)"
            echo "Blocked" && return 0
        fi

        # if there's no activity "InProgress", it's not an error. that's a race condition. 
        echo "InProgress" && return 0
    fi

    # Since the first line is the root workflow. If it's not InProgress, then we can trust the DisplayStatus. Just print it out. 
    echo "$subworkflows_text" | grep -vF 'DisplayName|DisplayStatus|Id' | head -n 1 | cut -d '|' -f 2

    [[ ! -v ANTIDOTE_JOBSTAT_MARKDOWN ]] &&
        echo2 -e "CIS Log: https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$jobid" ||
        echo2 -e "[View CIS Log](https://beta-cps.trafficmanager.net/Public/$cis_namespace/JobDetails/$jobid)"
    return $?
}


function subcmd_push () {
    local_wf_dir="$1"
    workflow_name="$2"
    [[ "$workflow_name" = "" ]] && workflow_name="$cis_default_workflow_name"
    release_ver="$3"
    [[ "$release_ver" = "" ]] && release_ver="ricin.$RANDOM$RANDOM"
    echo "$release_ver" > "/tmp/.antidote-cis.$workflow_name-ver"

    cis_upload_pkg "$workflow_name" "$release_ver" "$local_wf_dir" &&
    cis_wait_for_upload "$workflow_name" "$release_ver" &&
    cis_release_pkg "$workflow_name" "$release_ver"
    return $?
}
function subcmd_setdef () {
    workflow_name="$1"
    [[ "$workflow_name" = "" ]] && workflow_name="$cis_default_workflow_name"
    release_ver="$2"
    [[ "$release_ver" != "" ]] || release_ver=`cat "/tmp/.antidote-cis.$workflow_name-ver"` || ! echo2 "Error: Unable to determine workflow version" || exit 1
    echo "$release_ver" > "/tmp/.antidote-cis.$workflow_name-ver"

    cis_set_default_pkgver "$workflow_name" "$release_ver"
    return $?
}
function subcmd_release () {
    workflow_name="$1"
    [[ "$workflow_name" = "" ]] && workflow_name="$cis_default_workflow_name"
    release_ver="$2"
    [[ "$release_ver" != "" ]] || release_ver=`cat "/tmp/.antidote-cis.$workflow_name-ver"` || ! echo2 "Error: Unable to determine workflow version" || exit 1
    echo "$release_ver" > "/tmp/.antidote-cis.$workflow_name-ver"

    cis_release_pkg "$workflow_name" "$release_ver"
    return $?
}
function subcmd_cloudrun () {
    workflow_name="$1"
    [[ "$workflow_name" = "" ]] && workflow_name="$cis_default_workflow_name"
    release_ver="$2"
    [[ "$release_ver" != "" ]] || release_ver=`cat "/tmp/.antidote-cis.$workflow_name-ver"` || ! echo2 "Error: Unable to determine workflow version" || exit 1
    echo "$release_ver" > "/tmp/.antidote-cis.$workflow_name-ver"

    cis_run_job "$workflow_name" "$release_ver"
    return $?
}
function subcmd_jobstatus () {
    jobid="$1"
    [[ "$jobid" = "" ]] && echo2 "Error: subcommand 'jobstatus' requires argument 'jobid'" && exit 1

    cis_get_job_status "$jobid"
    return $?
}
function subcmd_listver () {
    workflow_name="$1"
    [[ "$workflow_name" = "" ]] && workflow_name="$cis_default_workflow_name"

    resp=`curl_wrapped -s "https://beta-cps.trafficmanager.net/cis.client.svc/Public/$cis_namespace.$workflow_name/Packages"` || return $?
    [[ "$resp" = "" ]] && echo2 "Invalid api response. Is cookie expired or not correctly set?" && return 2
    echo "$resp" | json2table Version,State -p
    return $?
}

# Initialization
antidote_version="1.1.04"
subcmd="$1"
shift

# Retrieve all workflow parameters (name=value) from arg array. Runtime settings starts with @
filteredArgs=()
for arg in "$@"; do
    [[ "$arg" =~ '=' ]] && cis_default_workflow_parameter+=("$arg") || filteredArgs+=("$arg")
done

# Call the actual subcommand
case "$subcmd" in
    push)
        subcmd_push "${filteredArgs[@]}" ; exit $?
        ;;
    setdef)
        subcmd_setdef "${filteredArgs[@]}" ; exit $?
        ;;
    cloudrun)
        subcmd_cloudrun "${filteredArgs[@]}" ; exit $?
        ;;
    jobstatus)
        subcmd_jobstatus "${filteredArgs[@]}" ; exit $?
        ;;
    release)
        subcmd_release "${filteredArgs[@]}" ; exit $?
        ;;
    listver )
        subcmd_listver "${filteredArgs[@]}" ; exit $?
        ;;
    all)
        subcmd_push "${filteredArgs[@]}" && 
        subcmd_setdef "${filteredArgs[@]:1}" && 
        subcmd_cloudrun "${filteredArgs[@]:1}"
        exit $?
        ;;
    *)
        echo2 "
Antidote-CIS version $antidote_version, maintained by Recolic Keghart <root@recolic.net> or Bensong Liu <bensl@microsoft.com> (the same person!)

[[ Please read and modify antidote.config.sh before using this tool! ]]

Usage:
       $0 push path/to/your/workflow/dir [BootstrapCAWorkflow] [v4.1.1]
       $0 push //existing-samba-share/your/workflow/dir [BootstrapCAWorkflow] [v4.1.1]
       $0 release [BootstrapCAWorkflow] [v4.1.1]
       $0 setdef [BootstrapCAWorkflow] [v4.1.1]
       $0 cloudrun [BootstrapCAWorkflow] [v4.1.1] [ParameterName=ParameterValue ...]
       $0 listver [BootstrapCAWorkflow]
       $0 jobstatus <JOB_ID>

The 'push' subcommand uploads your workflow to CIS, and release it. 
The 'release' subcommand releases your package. 
The 'setdef' subcommand set a released package as the default version. 
The 'cloudrun' subcommand would run the workflow on CIS. It doesn't wait for job completion. 
The 'listver' subcommand shows all versions of your workflow. 
The 'jobstatus' subcommand would show the status of specified job. It's usually 'InProgress', 'Blocked' or 'Finished'. 

You can push from local directory, CORPNET samba share, or CIS itself. Refer to README.md for more info. 
If you omit the version number, 'push' would generate a random verion number, while other subcommands would use the version number in your last push.
    WARNING: OMITTING VERSION NUMBER IS NOT USING THE DEFAULT VERSION OF WORKFLOW!
If you omit the workflow name, antidote would use the cis_default_workflow_name in antidote.config.sh.
The workflow_parameters in command line would overwrite workflow_parameters in antidote.config.sh. 
    You may set a default value for workflow_parameters in antidote.config.sh, and they could be overwritten by command line parameters. 
    If your workflow parameter has 'bool' type, use string 'true' or 'false' as its value. 
    ParameterName with a leading '@' is 'RuntimeSettings', refer to README.md for more info. 
    ParameterName and ParameterValue must not contain punctuation mark (').

Examples: 
antidote-cis push ../bin/net472 BootstrapCAWorkflow
antidote-cis push //reddog/builds/branches/git_azure_deployment_builder_master/1.11/MyWorkflow MyWorkflow 1.11
antidote-cis push //RECOLICPC/mybuild
antidote-cis setdef BootstrapCAWorkflow
antidote-cis cloudrun BootstrapCAWorkflow
antidote-cis cloudrun BuildDomainControllerWorkflow jihyan.9.24.2 IsFirstDC=true UserNameKey=unkey PasswordKey=pwkey @SubscriptionId=420f71ab-35f2-4550-9de6-7c8fab764b4d @Region=westus2
antidote-cis jobstatus 2517645620722579999_df3a452e-d580-47c2-b96e-61f1671358c9

Available env: 
    ANTIDOTE_JOBSTAT_MARKDOWN: If this var is set, 'jobstatus' would output as markdown instead of plain text. 
"
        exit 1
esac


