#!/bin/bash
# Puppet Environment Workspace Provider
# This util wraps gita, which is python and uses git, to automate a few common things we do to
# create and manage the multiple repositories involved in maintaining puppet in the
# new repo-per-module + r10k world. Tries to autostash changes during env switching
# it should be rewritten to python sometime soonish (Was a small shell that got out of hand)
# This util keeps a cache copy of control-repo in ~/.pewp, so it can track current
# contents of Puppetfile without altering any working copies. 
gitad=$HOME/.config/gita/
wd=$gitad/pewp
cache_repo=$wd/control-repo
debug=""
workspace=$HOME/git/video-puppet
Puppetfile="${cache_repo}/Puppetfile"
git_uri='git@git.xarth.tv'
git_project='video-puppet'
context=''
force=''
default_mods='control-repo hiera_data'
stashtag='pewp_stash_tag'

controlrepo="${git_uri}:${git_project}/control-repo.git"
hieradata="${git_uri}:${git_project}/hiera_data.git"

script_dir=$(dirname $(realpath $0))

function pushd() {
    command pushd "$@" > /dev/null 2>&1
}

function popd() {
    command popd "$@" > /dev/null 2>&1
}

# print error to stderr and exit with a code
function error() {
    local err=${2:-1}
    echo "$1" >&2
    exit $err
}

function debug() {
    [[ -n $debug ]] && echo "[DEBUG] $(date +%s) $1" >&2
}

# print usage guide, exit 1
function usage() {
    debug "usage() $@"
    usage=$(cat <<EOUSAGE
usage:
$0 [options] <command> [args]
  Command           Description
$(PRINT_USAGE=cat; for comm in $script_dir/commands/*; do . $comm; done)
    wd                print your workspace dir (where you ran init from).
    envs              List current local environments (same as gita group ls)
    mods              List available modules from Puppetfile
    ss                Show status on every repo in the current environment.
    status            List repos in current env and their details (same as gita ll)
    commit <message>  Commits current staged changeset in all modules of current environment with
                        message.(eq to 'git commit -am "message"' across all repos in the environment)
    pull              Issues 'git pull' across current environment
    help              Shows this usage and exits.

  Options
    -f,--force    Ignore safety checks, try to push through as-is anyway. For commit, add all files first.
    -d,--debug    Debug: display commands and output, more debug output

EOUSAGE
    )
    error "${usage}" 0
}

# Simple confirmation function
function confirm() {
    read -n1 -p "${1} confirm: (y/n): "
    [[ $REPLY =~ [Yy] ]]
}

function get_workspace_dir() {
    debug "set_workspace_dir() $@"
    workspace=$(grep ',control-repo$' $gitad/repo_path 2>/dev/null|cut -f1 -d,|rev|cut -f2- -d/|rev )
}

# if passed name is a valid puppet environment name
function valid_env_name() {
    local name="$1"
    local x
    debug "valid_env_name() $name"
    [[ -z "$name" ]] && return 1
    debug "valid_env_name() $name not empty"
    for x in main master production DEFAULT; do
        [[ "$x" == "$name" ]] && return 1
        debug "valid_env_name() $name not $x"
    done
    [[ "$name" =~ [^a-zA-Z0-9_] ]] && return 1
    return 0
}

# returns current gita context
function get_context() {
    context=$(gita context|cut -f1 -d:)
    [[ 'Context is not set' == "$context" ]] && context=''
}

# Checkout a branch for a module
function co_branch() {
    debug "co_branch() $1 $2 $3"
    local mod=$1
    local branch=$2
    local nostash=$3
    [[ -z "$branch" ]] && error "no branch passed to co_branch"
    [[ -z "$mod" ]] && error "no mod passed to co_branch"
    [[ $branch == 'DEFAULT' ]] && branch=$(default_branch_name $mod)
    pushd $workspace/$mod || error "unable to cd to $workspace/$mod"
    debug "  co_branch()->branch_exists $mod $branch defb: $(default_branch_name $mod)"
    branch_exists $mod $branch || git branch $branch $(default_branch_name $mod)
    debug "  co_branch()->stash_changes() $mod ? -z '$nostash'"
    [[ -z $nostash ]] && stash_changes $mod 
    debug "  co_branch()->git checkout $branch"
    git checkout $branch||error "Error checking out branch $branch for $mod"
    debug "  co_branch()->unstash_changes $mod $branch"
    unstash_changes $mod $branch
    popd
}

# list mods in group
function mods_in_group() {
    debug "mods_in_group() $1"
    local group=$1
    [[ -z $group ]] && error "No group passed to mods_in_group"
    gita group ll|grep "^$group: "|cut -f2- -d' '
}

# Updates our cache control-repo
function update_cache() {
    debug "update_cache()"
    pushd $cache_repo || error "Unable to cd to cache repo $cache_repo"
    git pull >/dev/null 2>&1
    popd
}

# return default branch name (usually production, maybe master or main) of given module
function default_branch_name() {
    debug "default_branch_name() $1"
    local mod=$1
    [[ -z "$mod" ]] && error "No module passed to find the default_branch_name of"
    local defbn=$(git branch -r | grep HEAD|awk '{print $3}'|cut -f2 -d'/')
    [[ -z "$defbn" ]] && branch_exists $mod 'production' && debfn='production'
    [[ -z "$defbn" ]] && branch_exists $mod 'master' && debfn='master'
    defbn=${defbn:-main}
    echo $defbn
}

# if a branch exists for a mod
function branch_exists() {
    debug "branch_exists() $1 $2"
    local mod=$1
    local branch=$2
    pushd $workspace/$mod || error "unable to cd to module dir $workspace/$mod"
    git branch -r|grep -q " origin/${branch}$" || git branch |grep -q " ${branch}$" && return 0
    popd
    return 1
}

# if env exists in control-repo
function env_exists() {
    debug "env_exists() $1"
    local env=$1
    [[ -z "$env" ]] && error "no env passed to env_exists"
    [[ 'DEFAULT' == "$env" ]] && return 0
    pushd $cache_repo || error "no cache repo?? try to init again"
    update_cache
    if git branch -r|grep -q " origin/${env}$"; then
        popd
        return 0
    fi
    popd
    branch_exists control-repo $env
}

# runs through all repos gita knows about and updates them
function update_all_repos() {
    debug "update_all_repos() $@"
    gita fetch --prune && gita pull
}

# stashes change set for a module
function stash_changes() {
    debug "stash_changes() $1"
    [[ -z $1 ]] && error "No module passed to stash_changes"
    local mod=$1
    pushd $workspace/$mod || error "No module found for $mod"
    debug "  stash_changes()->git stash save $stashtag"
    git stash save $stashtag > /dev/null
    popd
}

# unstashes changes for a module branch
function unstash_changes() {
    debug "unstash_changes() $1 $2"
    [[ -z $1 ]] && error "No module passed to unstash_changes"
    [[ -z $2 ]] && error "No module passed to unstash_changes"
    local mod=$1
    local branch=$2
    pushd $workspace/$mod || error "No module found for $mod"
    local stashid=$(git stash list | grep $stashtag | grep "n $branch:"| cut -f1 -d:)
    [[ -z "$stashid" ]] && debug "  unstash_changes()-> no stashID"
    [[ -n "$stashid" ]] && debug "  unstash_changes()-> git stash pop $stashid"
    [[ -n "$stashid" ]] && git stash pop "$stashid" > /dev/null
    popd
}

# Install Gita
function install_gita() {
    debug "install_gita() $@"
    echo "Gita is required for this utility."
    echo "Should I try to install this now (uses pip3)?"
    confirm || error "gita not present and install cancelled"
    pip3 install -U gita || error "gita install failed, please try to install yourself https://github.com/nosarthur/gita  make sure to use >= 0.12.4"
}

# takes a module name, returns its git uri
get_mod_uri() {
    local mod=$1
    debug "$FUNCNAME $1"
    [[ -z "$mod" ]] && error "no mod passed to get_mod_uri"
    mod=$(echo $mod|sed -e 's/-/_/g')
    debug "$FUNCNAME -> update_cache"
    update_cache
    debug "grep -A5 ^mod ${mod}, $Puppetfile | grep   :git  | cut -f2 -d |sed -e s/,//"
    grep -A5 "^mod '${mod}',$" $Puppetfile | grep '  :git ' | cut -f2 -d"'"|sed -e "s/',$//" 
}

# Clone a module by name
clone_mod(){
    debug "$FUNCNAME $1"
    local mod=$1
    [[ -z "$mod" ]] && error "no mod passed to clone"
    [[ -d "$workspace/$mod" ]] && return
    local mod_uri=$(get_mod_uri "$mod")
    debug "$FUNCNAME->moduri: $mod_uri"
    [[ -z $mod_uri ]] && error "URI for $mod not found"
    pushd $workspace || error "Unable to cd to workspace at $workspace"
    debug "  git clone $mod_uri"
    git clone $mod_uri
    debug "  gita add $mod"
    gita add $mod
    popd
}

# Add a module to gita group 
add_mod() {
    local mod="$1"
    local group="$2"
    debug "$FUNCNAME $mod $group"
    [[ -z "$mod" ]] && error "no mod passed to add_mod"
    [[ -z "$group" ]] && error "no group passed to add_mod"
    pushd $workspace
    debug "$FUNCNAME->clone_mod $mod"
    [[ -d $mod ]] || clone_mod $mod
    debug "  ->co_branch $mod $group"
    co_branch $mod $group
    debug "  -> gita group add -n $group $mod"
    gita group add -n $group $mod
    popd
}

## MAIN ##

comm='help'

which gita>/dev/null || install_gita

get_workspace_dir
get_context

while [[ -n "$1" ]] ; do
    x=$1
    shift
    debug "arg $x"
    case $x in
        -*)
            case $x in
                -f|--force) force=1 ;;
                -d|--debug) debug=1 ;;
                -h|--help) usage ;;
            esac
            ;;
        *)
            comm="${x}"
            args="$@"
            break
            ;;
    esac
done

# Do stuff
if [[ -z "$workspace" ]] ; then
    if [[ $comm != 'init' ]] ; then
        echo "No workspace dir. Did you init yet?"
        usage
    fi
fi

[ -f $script_dir/commands/$comm ] && {
    . $script_dir/commands/$comm
    command_$comm
    exit
}

case $comm in
    help)
        usage
        ;;
    mods)
        update_cache
        grep '^mod ' $Puppetfile |sed -e 's/mod //' -e "s/[',]//g"
        ;;
    status)
        echo "Current context: $context"
        gita group ll
        ;;
    envs)
        gita group ls
        ;;
    wd)
        echo $workspace
        ;;
    ss)
        gita super status
        ;;
    commit)
        valid_env_name $context || error "Invalid environment to commit to. '${context}' is protected or otherwise invalid"
        gita super commit -am "$args"
        ;;
    pull)
        gita super pull origin
        ;;
    *)
        usage
        ;;
esac
