#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# metascript to include
#
# to generate file header for caller script do :
# bash <relative_path_to>/metascript.sh header
# ex:
# bash lib/metascript.sh header

metascript_version=v1.0.0

# if called directly
if [[ "$0" =~ ^(.*)/metascript.sh$ ]]
then
    prefix=${BASH_REMATCH[1]}
    case $1 in
	header)
    cat <<EOF
#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
# header generated by $0 $1
# metascript_version=$metascript_version

toolsdir=$prefix
pushd \$toolsdir >/dev/null
toolsdir=\$(pwd)
source metascript.sh
popd >/dev/null

metarun=metarun
# change default to sudo if needed
# metasudo=sudo

while [[ \$# > 0 ]]
do
    case "\$1" in
        *)
           parsemetaarg "\$1"
        ;;
    esac
    shift
done

usage
EOF
    ;;
	version)
	    echo $metascript_version
    ;;
	*)
	    echo "[WARNING] only header and version arguments are supported. (prefix=$prefix), this script is intended to be included not to be directly called." >&2
	    exit 1
    esac
    exit 0
fi

if [[ "$metascript_included" == "yes" ]]
then
    log_warn "metascript already included ($0)"
else

metascript_included=yes

# at this step $(pwd) should be this of metascript.sh
# while $0 is this of caller script using metascript

if [[ -z $toolsdir ]]
then
    # assume all tools are in lib/
    toolsdir=$(dirname $(readlink -f $0))/lib
    # all tools resources are relative to this directory
fi

if [[ -z $log_functions ]]
then
    log_functions=$toolsdir/log_functions.sh
    [[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions , toolsdir=$toolsdir script=$0" >&2 ; exit 1 ;}
    source $log_functions
fi

metascript_usage()
{
    cat <<EOF >&2
metascript commands :

help|usage             help or usage of this {$0} tool. stop after usage display.
dryrun|show|showdoc    display what should/will be done

defersource=           script file defining defer() non standard function
defer=                 defer function to use, default is showdoc
toolsresourcesdir=     where to pick resource
                       default to parent of script $toolsresourcesdir
                       mostly used with dryrun
apply                  default : will actual do work without defer
EOF
}

usage()
{
    echo "[WARNING} no specific usage function for {$0}, to improve by developer" >&2
    echo >&2
    metascript_usage
}

showinfo()
{
    echo "$@"
}

metarun()
{
    $defer $metasudo "$@"
}

autoquoteargs()
{
    echo -n "$1"
    shift
    while [[ $# > 0 ]]
    do
        if [[ "$1" =~ [\ \$] ]]
        then
            echo -n " '$1'"
        else
            echo -n " $1"
        fi
        shift
    done
}

echoarray()
{
    declare -a arr=("${@}")
    declare -i len=${#arr[@]}
    # Show passed array
    for ((n = 0; n < len; n++))
    do
        echo -en " \"${arr[$n]}\""
    done
    echo
}

deferpipe()
{
    cat
    echo "# <previous line> | $@"
}

include_source()
{
    if [[ -f $1 ]]
    then
        source $1
    else
        log_fatal "Missing $1 script"
        exit 1
    fi    
}

showdoc()
{
    if [[ $1 =~ ^tools/ ]]
    then
        # assumes it handles ENV_METASCRIPT_DEFER
        "$@"
    else
        echo '```'
        autoquoteargs "$@"
        echo
        echo '```'
    fi
}

redirectto()
{
    tofile=$1
    if [[ -n $defer ]]
    then
        echo "Copy to $tofile"
        echo '```'
        cat
        echo '```'
    else
        cat > $tofile
    fi
}

execredirectfrom()
{
    fromfile=$1
    shift
    enforcefile "$fromfile" exists
    if [[ -n $defer ]]
    then
        echo '```'
        autoquoteargs "$@"
        echo ' < '"$fromfile"
        echo '```'
    else
       "$@" < $fromfile
    fi
}

execredirectto()
{
    tofile=$1
    shift
    if [[ -n $defer ]]
    then
        echo '```'
        autoquoteargs "$@"
        echo ' > '"$tofile"
        echo '```'
    else
        "$@" > $tofile
    fi
}

execredirecttoroot()
{
    tofile=$1
    shift    
    if [[ -n $defer ]]
    then
        echo '```'
        autoquoteargs "$@"
        echo ' | sudo tee '"$tofile"' >/dev/null'
        echo '```'
    else
        "$@" | sudo tee $tofile >/dev/null
    fi
}

execredirectas_to()
{
    while [[ $# > 0 ]]
    do
	case $1 in
	    user=*)
		if [[ "$1" =~ ^user=([a-z]+[a-z0-9]*)$ ]]
		then
		    asuser=${BASH_REMATCH[1]}
		else
		    log_fatal "execredirecttouser missing user= in '$@'"
		    [[ -n $defer ]] || exit 1
		fi		    
	    ;;
	    chroot=*)
		aschroot=${1/chroot=}
	    ;;
	    *)
		tofile=$1
		shift
		break
	esac
	shift
    done
    local command=()
    if [[ -n $aschroot ]]
    then
	command=(sudo chroot)
	if [[ -n $asuser ]]
	then
	    # no group yet
	    command+=(--userspec=$asuser:$asuser)
	fi
	command+=($aschroot)
    elif [[ -n $asuser ]]
    then
	command=(sudo -u $asuser)
    fi  
    command+=(tee $tofile)
    if [[ -n $defer ]]
    then
        echo '```'
        autoquoteargs "$@"
        echo ' | '"${command[@]}"' >/dev/null'
        echo '```'
    else
        "$@" | "${command[@]}" >/dev/null
    fi
}


pipeto()
{
    if [[ -n $defer ]]
    then
        echo '```'
        echo -n 'cat << EOF| '
        autoquoteargs "$@"
        echo
        cat
        echo "EOF"
        echo '```'
    else
        cat | "$@"
    fi
}


query_ext()
{
    local prompt="$1"
    local var=$2

    if [[ -n $defer ]]
    then
        # uppercase it
        eval $var=${var^^}
        $defer query $prompt "$(eval echo \$$var)"
    else
        echo -ne $prompt
        read $var
    fi
}

# two arguments first prompt, second name of var
query_password()
{
    local prompt="$1"
    local var=$2

    if [[ -n $defer ]]
    then
        # uppercase it
        eval $var=${var^^}
        $defer query_password "\"$prompt\"" "$(eval echo \$$var)"
    else
        read -sp "$prompt" $var
        echo
    fi
}

# echo yes if reply match  ^[Yy]([eE][sS]|)$ or no if does not match.
query_yesno()
{
    local prompt="$1"
    local yesno=no

    read -p "$prompt (Yes/No) " yesno

    if [[ $yesno =~ ^[Yy]([eE][sS]|)$ ]]
    then
        echo yes
    else
        echo no
    fi
}


# through hardcoded 'secret' variable
create_secret()
{
    # global secret
    # declare -g secret
    # export -n secret

    local -i length=$1

    [[ -z $length ]] && length = 32

    if (( length < 8 ))
    then
        log_warn "secret length $length < 8. very small"
    fi
    # secret=$(echo $RANDOM | md5sum | head -c $length)
    secret=$(tr -dc A-Za-z0-9 </dev/urandom | head -c $length)
}

check_missing_dest_dir()
{
    local dir=$1
    if [[ -n $defer  ]]
    then
        $defer "create $dir if it does not exist"
    else

        if [[ ! -d $dir ]]
        then
            echo "[ERROR] '$dir' does not exist please create it."
            exit 1
        fi
     fi
}

sed_substitute_expr()
{
    local from="$1"
    shift
    local to="$1"
    shift
    local s='/'
    if  [[ $from =~ [\\] ]]
    then
# escape char \ should be doubled
        from=${from//\\/\\\\}
    fi
    if [[ $from =~ $s ]]
    then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
        from=${from//$s/\\$s}
    fi
    if [[ $from =~ \[ ]]
    then
        from=${from//\[/\\\[}
    fi
    if [[ $from =~ \* ]]
    then
        from=${from//\*/\\\*}
    fi
    if [[ $from =~ ^(.*)\$$ ]]
    then
        from=${BASH_REMATCH[1]}'\$'
    fi
    if [[ $from =~ ^\^(.*)$ ]]
    then
        from='\^'${BASH_REMATCH[1]}
    fi

    if  [[ $to =~ [\\] ]]
    then
# escape char \ should be doubled
        to=${to//\\/\\\\}
    fi
    if  [[ $to =~ $s ]]
    then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
# echo "This is a limitation of metascript.sh script, replaced by \$s" >&2
        to=${to//$s/\\$s}
    fi
    if  [[ $to =~ [\&] ]]
    then
# echo "[ERROR] character & is prohibited due to sed usage" >&2
        to=${to//\&/\\\&}
    fi

    # replace it globaly
    echo "s$s$from$s$to${s}g"

}

sedreplacefromto()
{
    local from="$1"
    local to="$2"
    shift 2
    local sedexpr="$1"
    execredirectto $to sed "$sedexpr" $from
    shift
    while [[ $# > 0 ]]
    do
        sedexpr="$1"
        $defer sed -i "$sedexpr" $to
        shift
    done

}

replacefromto()
{
    local from="$1"
    local to="$2"
    shift 2
    if [[ -n $defer ]]
    then
        $defer "replace $@  from '$from' into '$to'"
    else
        local sedexpr=$(sed_substitute_expr "$1" "$2")
        execredirectto $to sed "$sedexpr" $from
        shift 2
        while [[ $# > 0 ]]
        do
            sedexpr=$(sed_substitute_expr "$1" "$2")
            $defer sed -i "$sedexpr" $to
            shift 2
        done
    fi

}

sedreplacein()
{
    local file=$1
    shift
    while [[ $# > 0 ]]
    do
        $defer sed -i "$1" $file
        shift
    done
}

replacein()
{
    local infile=$1
    shift
    if [[ -n $defer ]]
    then
        $defer "replace $@ into '$infile'"
    else
        while [[ $# > 0 ]]
        do
            sedexpr=$(sed_substitute_expr "$1" "$2")
            $defer sed -i "$sedexpr" $infile
            shift 2
        done
    fi
}

parsemetaarg()
{
    case $1 in
        apply)
            defer=
            ;;
        defersource=*)
            defersource=${1/defersource=/}
            ;;
        defer=*)
            defer=${1/defer=/}
            ;;
        dryrun|show|showdoc)
            defer=showdoc
            ;;
	debug)
	    set -ex
	    ;;
	metasudo=*)
	    metasudo=${1/metasudo=}
	    ;;
        help|usage)
            usage
	    exit 0
            ;;
        toolsresourcesdir=*)
            toolsresourcesdir=${1/toolsresourcesdir=/}
            ;;
        scl_enable=*)
            scl_args=(scl enable ${1/scl_enable=/} --)
            ;;
        *)
            log_error "unrecognized argument '$1'"
            usage
            exit 1
            ;;
    esac
}

enforcearg()
{
    local var="$1"
    local default="$2"
    eval value='$'"$var"

    if [[ -z $value ]]
    then
        log_error "{$0} expect '$var' to be set ex $var=$default"
	if [[ -n $defer ]]
	then
	    [[ -z $default ]] && default="DEFAULT"
	    log_warn "in defer/dryrun force $var=$default"
	    eval "$var=$default"
	else
            usage
            exit 1
	fi
    fi
}

enforce()
{
    objecttype=$1
    shift
    case $objecttype in
	file|dir)
	    object=$1
	    shift
	    constraint=$1
	    case $constraint in
		exists|does_not_exist|create_if_needed)
		    enforce${objecttype} "$object" "$@"
		    ;;
		*)
		    log_error "enforce $objecttype '$object' $@"
		    log_fatal "constraint '$constraint' unsupported. Please fix the code."
		    exit 1
		    ;;
	    esac		    
	    ;;
	var|arg)
	    enforcearg "$@"
	    ;;
	*)
	    log_error "enforce $objecttype $@"
	    log_fatal "objectttype $objecttype unsupported"
	    exit 1
	    ;;
    esac
}

enforcefile()
{
    local file="$1"
    local constraint="$2"

    if [[ ! -f "$file" ]]
    then
	case $constraint in
	     exists)
		 log_error "Missing expected $file"
		 [[ -n $defer ]] || exit 1
		 ;;
	     create_if_needed)
		 $metarun touch $file
		 ;;	     
        esac
    else
        if [[ $constraint = does_not_exist ]]
        then
            log_error "'$file' already exists. Move it away"
            [[ -n $defer ]] || exit 1
        fi
    fi
}

enforcedir()
{
    local dir="$1"
    local constraint="$2"

    if [[ -e $dir ]]
    then
	case $constraint in
	    does_not_exist)
		if [[ -d "$dir" ]]
		then
		    log_error "'$dir' already exists"
		    [[ -n $defer ]] || exit 1
		else
		    log_error "'$dir' already exists and is not a directory as expected"
                    [[ -n $defer ]] || exit 1
		fi
		;;
	    create_if_needed|exists)
		if [[ ! -d "$dir" ]]
		then
		    log_error "'$dir' already exists and is not a directory as expected"
                    [[ -n $defer ]] || exit 1
		fi
		;;
	esac
    else
	case $constraint in
	    exists)
		log_error "Missing expected directory $dir"
		[[ -n $defer ]] || exit 1
		;;
	    create_if_needed)
		if [[ ! -d "$dir" ]]
		then
		    $metarun mkdir -p "$dir"
		fi
		;;	     
        esac
    fi
}

applymetaargs()
{
    if [[ -n $defer ]]
    then
        if [[ -n $defersource ]]
        then
            if [[ -f $defersource ]]
            then
                log_any "source $defersource"
                source $defersource
            else
                exit_fatal "defersource $defersource provided but not a file"
            fi
        fi
        # $showinfo "generated with $0 $allparms"
        export ENV_METASCRIPT_DEFER="$defer"
        export ENV_METASCRIPT_RESOURCESDIR="$toolsresourcesdir"
    fi
}

defaultmetainit()
{
    while [[ $# > 0 ]]
    do
        parsemetaarg "$1"
        shift
    done

    applymetaargs
}

read_organisation()
{
    organisation_file=$(find organisation -name '*.conf')
    if [[ -f $organisation_file ]]
    then
        while read line
        do
            case $line in
                name=*)
                    organisation_name=${line/name=/}
                    ;;
                domain=*)
                    organisation_domain=${line/domain=/}
                    ;;
                ldap_base=*)
                    organisation_ldap_base=${line/ldap_base=/}
                    ;;
                image_keyword=*)
                    organisation_image_keyword=${line/image_keyword=/}
                    ;;
                *)
                    log_warn "'$line' not recognized as an organisation parameter"
                    ;;
            esac
        done < $organisation_file
    fi
}

check_variable_match()
{
    local var="$1"
    local match="$2"
    local default="$3"
    local value=""

    eval value='$'"$var"

    if [[ -z "$value" ]]
    then
        echo "set $var to default value $default"
        eval "$var=$default"
        eval value='$'"$var"
    fi

    if [[ ! $value =~ $match ]]
    then
        log_error "$var $value should match $match"
        exit 1
    fi

}

check_variable_in()
{
    local var="$1"
    local default="$2"
    shift 2

    local value=""
    local values="$@"

    eval value='$'"$var"

    if [[ -z "$value" ]]
    then
        echo "set $var to default value $default"
        eval "$var=$default"
        eval value='$'"$var"
    fi

    while [[ $# > 0 ]]
    do
        if [[ "$value" = "$1" ]]
        then
            return
        fi
        shift
    done

    log_error "'$var' should be within $values it is '$value'"
    exit 1
}

check_root()
{
    if [[ -n $defer ]]
    then
        $defer "{$0} script to run run as root or with sudo"
    else
        [[ $EUID -eq 0 ]] || {
            log_error "{$0}  You have to be root or use sudo to run this script"
            exit 1;
        }
    fi
}

get_timestamp_second()
{
    echo "$(date +"%Y%m%d%H%M%S")"
}

todo()
{
    log_any TODO "$@"
}

get_resource_var() {
    local varname="$1"
    local default_value="$2"

    eval value='$'"$varname"
    if [[ -z $value ]]
    then
	echo "# ($0:metascript.sh:$LINENO) $(date)" >>$collect_context
	if [[ -z $default_value ]]
	then
	    log_error "resource $varname does not exists and no non empty default provided"
	    echo "# $varname=<MISSING>" >>$collect_context
	    exit 1
	fi
	log_warn "($0) Using default value '$varname'='$default_value' HARDCODED in script : should be fixed with proper default file .resources.var, see traces in $collect_context"
	read "$varname" <<<"$default_value"
	echo "$varname=$default_value" >>$collect_context
    fi
}

setup_resources_var()
{
    local resource_var="$1"
    enforcefile "$resource_var" exists
    if [[ -f $resource_var ]]
    then
	while read line
	do
	    if [[ $line =~ ^([a-zA-Z0-9_]+)=(.+)$ ]]
	    then
		varname=${BASH_REMATCH[1]}
		value=${BASH_REMATCH[2]}
		read "$varname" <<<"$value"
	    elif [[ $line =~ ^# ]]
	    then
		if [[ -n $defer ]]
		then
		    $defer : "$line"
		fi
	    else
		log_warn "Invalid syntax in $resource_var '$line' does not match any expected expression"
	    fi	
	done <"$resource_var"
    fi
}

# allow to check mounted points
mountpoint_get_device()
{
    local mount_point="$1"
    mount_point=$(readlink -f "$mount_point")
    $defer awk "{  if (\$2 == \""$mount_point"\") print \$1 ;}" /proc/mounts
}

device_get_mountpoints()
{
    local device="$1"
    $defer awk "{  if (\$1 == \""$device"\") print \$2 ;}" /proc/mounts
}

mount_if_needed()
{
    local mountdevice="$1"
    local mountpoint="$2"
    local param="$3"
    if [[ -n $defer ]]
    then
	$defer 'device=$(mountpoint_get_device '"$mountpoint"')'
    else
	device=$(mountpoint_get_device "$mountpoint")
    fi

    enforcedir "$mountpoint" exists
    # device is a device ...
    # enforcefile "$mountdevice" exists
    
    if [[ -z $device ]]
    then
	$metarun mount $param "$mountdevice" "$mountpoint"
    elif [[ "$device" == "$mountdevice" ]]
    then
	log_info "$device already mounted on $mountdevice"
    else
	log_warn "Another device $device is mounted on  $mountpoint, not $mountdevice"
    fi
}

umount_if_needed()
{
    local mountdevice="$1"
    local mountpoint="$2"
    local param="$3"
    device=$(mountpoint_get_device "$mountpoint")
    if [[ -n "$device" ]]
    then
	if [[ "$device" == "$mountdevice" ]]
	then
	    $metarun umount "$mountpoint"
	else
	    log_warn "Another device $device is mounted on  $mountpoint, not $mountdevice"
	    if [[ $param == '--bind' ]]
	    then
		log_warn "Unmouning ANYWAY ( mount --bind show root device, not mounted directory )"
		$metarun umount "$mountpoint"
	    fi
	fi
    else
	log_warn "no device found mounted for umount_if_needed $@"
    fi
}

exec_bg()
{
    if [[ -z $defer ]]
    then
	"$@" &
    else
	$defer "$@" '&'
    fi
}


# collect all hardcoded values.
mkdir -p ~/.artlog
collect_context=~/.artlog/collect_context.var

if [[ -z $toolsresourcesdir ]]
then
    # project directory
       
    # from current $(pwd) will follow parent dir hierarchy to find .resources.var
    dir="$(pwd)"
    while [[ -n $dir ]] && [[ -d $dir ]] && [[ ! -f $dir/.resources.var ]]
    do
	new_dir=$(dirname "$dir")
	if [[ $new_dir == $dir ]]
	then
	    # protect against infinite loop
	    break
	fi
	dir="$new_dir"
    done
    if [[ -f $dir/.resources.var ]]
    then
	toolsresourcesdir=$dir
    else	
	if [[ -z $ENV_METASCRIPT_RESOURCESDIR ]]
	then
	    toolsresourcesdir=$toolsparentdir
	else
	    toolsresourcesdir=$ENV_METASCRIPT_RESOURCESDIR
	fi
    fi
fi

resources_var=$toolsresourcesdir/.resources.var
if [[ -f $resources_var ]]
then
    setup_resources_var "$resources_var"
else
    log_warn "No $resources_var found"
fi


# quick way to give scl patches to fill scl_arg array
if [[ -f $toolsresourcesdir/.scl_env ]]
then
    source $toolsresourcesdir/.scl_env
fi

# empty defer means doit
defer=$ENV_METASCRIPT_DEFER

showinfo=showinfo

allparms="$@"

applymetaargs=applymetaargs

metarun=$defer

# metascript included
fi 
