#!/bin/bash

# Useful built-in functions for both linuxconf and user scripts.
function lcf_echo2 () {
    echo "$@" 1>&2
}
function lcf_err () {
    lcf_echo2 Error: "$@"
    return 1
}
function lcf_die () {
    lcf_err "$@"
    exit 1
}
function lcf_bgrun () {
    # Usage: lcf_bgrun /var/log/your.log sslocal -s xxx -p 'hello world my password' -l xxx --xxx
    # Usage: lcf_bgrun /var/log/your.log auto_restart frpc -c my_server.ini
    # v202504-1
    local logF="$1"
    shift
    local cmd="$1"
    shift
    if [ "$cmd" = auto_restart ] && [ ! -f /tmp/.auto_restart ]; then
        echo IyEvYmluL2Jhc2gKCndoaWxlIHRydWU7IGRvCiAgICAiJEAiCiAgICBbWyAkPyA9IDEzMCBdXSAmJiBicmVhawogICAgc2xlZXAgMQpkb25lCg== | base64 -d > /tmp/.auto_restart
        chmod ugo+rx /tmp/.auto_restart
        cmd=/tmp/.auto_restart
    fi
    echo "[$(date --utc)] EXEC $cmd $@" >> "$logF"
    nohup "$cmd" "$@" >> "$logF" 2>&1 & disown
    return $?
}


function lc_assert_user_is () {
    [[ "$(whoami)" = "$1" ]] || exit 0
}
function lc_assert_user_is_not () {
    [[ "$(whoami)" != "$1" ]] || exit 0
}

function lc_include () {
    # for every included shell script, spawn sub shell,
    #   in the subshell, source the script, call (eval) the function name specified by global variable $LCI_SUBSHELL_OP
    # print the return code of the eval-ed function to stderr, but this function should always return success.

    for script in "$@"; do
        lcf_echo2 "$script -- $LCI_SUBSHELL_OP started as $(whoami), at $(pwd)"
        (
            source "$script"
            declare -F "$LCI_SUBSHELL_OP" >/dev/null 2>&1 || exit 0
            eval "$LCI_SUBSHELL_OP"
        )
        lcf_echo2 "$script -- $LCI_SUBSHELL_OP completed with status $?"
        # TODO: should I abort on failure? It will stop running other include files. Don't do it now.
    done

    return 0
}

function lci_fsmap () {
    # If existing symlink is correct, do nothing & return 0 (success).
    #   Otherwise, delete the existing symlink. 
    # If the existing item is file/folder, move to '$filename.linuxsync_backup'.
    # If target folder doesn't exist, create it.
    # Then create a symlink.

    local content_path="$(realpath "$1")" || return $?
    local symlink_path="$2"

    if [ -L "$symlink_path" ]; then
        if [ "$(readlink "$symlink_path")" = "$content_path" ]; then
            return 0
        else
            rm "$symlink_path" || return $?
        fi
    elif [ -e "$symlink_path" ]; then
        mv "$symlink_path" "${symlink_path}.linuxsync_backup" || rm -f "$symlink_path" || return $?
    fi

    [ -d "$(dirname "$symlink_path")" ] || mkdir -p "$(dirname "$symlink_path")" || return $?

    ln -s "$content_path" "$symlink_path"
}

function lc_fsmap () {
    lci_fsmap "$@"
    local result=$?
    [ $result = 0 ] || lcf_err "lc_fsmap returned error $result"
    return $result
}

function lci_state_file_append () {
    local fname="$1"
    local prefix="$2"
    local uname="$3"

    echo "${prefix}_u_$uname=1" | tee -a "$fname" > /dev/null
    return $?
}
function lci_state_file_contains () {
    local fname="$1"
    local prefix="$2"
    local uname="$3"

    grep "^${prefix}_u_$uname=1" "$fname" > /dev/null
    return $?
}
function lci_state_file_list () {
    # stdout: uname list
    local fname="$1"
    local prefix="$2"
    local ar_uname=()

    grep "^${prefix}_u_.*=1$" "$fname" | sed "s/^${prefix}_u_//" | sed "s/=1$//"
    return $?
}

function lci_conf_get_masterconf_path () {
    local fname="$1"    # lc config path
    grep ^masterconf= "$fname" | sed s/^masterconf=//
}
function lci_overwrite_conf () {
    local fname="$1"    # (output) lc config path
    local confpath="$2" # user masterconf script path

    local newpath="$(realpath "$confpath")"
    [ "$newpath" != "" ] || lcf_err "lci_overwrite_conf: cannot unfold path $confpath. Permission error?" || return $?

    # Don't re-create if old config already looks good. In this case, init_done should be preserved. TODO: rename lc register to lc init, then should I remove this behavior??
    if [[ -f "$fname" ]]; then
        local oldpath="$(lci_conf_get_masterconf_path $fname)"
        [[ "$oldpath" = "$newpath" ]] && return 0
    fi

    echo -e "#autogenerated config, could be overwritten without warning.\nmasterconf=$newpath" | tee "$fname" > /dev/null || lcf_err "lci_overwrite_conf: unable to create $fname" || return $?
    chmod ugo+rw "$fname"
}


function lci_register () {
    local confpath="$1"
    [[ -f "$confpath" ]] || lcf_die "lci_register: $confpath not exist"

    # For new register (or register a different dir), clear all existing conf. This will trigger init again.
    lci_overwrite_conf /etc/linuxconf.conf "$confpath" || lcf_die "lci_register cannot write new conf"
}

function lci_call () {
    # calls an lc function in masterconf (and included subconf)
    [ "$1" = "" ] && lcf_die "logic error: lci_call without arg"
    export LCI_SUBSHELL_OP="$1"
    local masterconf="$(lci_conf_get_masterconf_path /etc/linuxconf.conf)" || lcf_die "unable to call lc_init. Cannot read masterconf path from /etc/linuxconf.conf"
    local workdir="$(dirname "$masterconf")"
    cd "$workdir" || lcf_die "unable to enter config directory: $workdir"
    lc_include "$masterconf"
    export LCI_SUBSHELL_OP=__lc_operation_undefined
}

function lci_init_if_needed () {
    local uname="$(whoami)"
    lci_state_file_contains /etc/linuxconf.conf init_done "$uname" && return 0

    lci_call lc_init
    
    lci_state_file_append /etc/linuxconf.conf init_done "$uname" || lcf_die "lc_init functions succeeded, but unable to update /etc/linuxconf.conf"
}

function lci_startup_if_needed () {
    local uname="$(whoami)"

    local state_file="/tmp/.linuxconf-state-$uname"
    if [[ ! -f $state_file ]]; then
        touch $state_file && chmod ugo+rw $state_file || lcf_die "failed to create tmp file $state_file"
    fi

    lci_state_file_contains $state_file startup_done "$uname" && return 0

    lci_call lc_startup
   
    lci_state_file_append $state_file startup_done "$uname" || lcf_die "lc_startup functions succeeded, but unable to update $state_file"
}

function lci_usage () {
    lcf_echo2 "linuxconf v$lci_version"
    lcf_echo2 "This tool helps you manage all linux customization in one centralized directory, making backup/sync much easier."
    lcf_echo2 "Usage: Run '$0 register <path/to/masterconf.sh>', then it will work out-of-box."
    exit 1
}

# All install hook function should:
#   be safe for duplicate call,
#   works for root / non-root,
#   clears non-root alternative when called with root.
function lci_install_startup_hook () {
    # if is root:
    #   if root config exists, overwrite it
    #   if non-root config exists, clear it
    # else:
    #   if root config exists, return
    #   if non-root config exists, overwrite it
    local service_file=W1VuaXRdCkRlc2NyaXB0aW9uPWxpbnV4Y29uZiBzdGFydHVwIGhvb2sgYXMgcm9vdCB1c2VyCkFmdGVyPW5ldHdvcmsudGFyZ2V0CltTZXJ2aWNlXQpUeXBlPW9uZXNob3QKRXhlY1N0YXJ0PS91c3IvYmluL2xpbnV4Y29uZiBfc3RhcnR1cF9hbGwKUmVtYWluQWZ0ZXJFeGl0PXllcwpbSW5zdGFsbF0KV2FudGVkQnk9bXVsdGktdXNlci50YXJnZXQK

    if [[ "$(whoami)" = root ]]; then
        if command -v systemctl > /dev/null ; then
            echo "$service_file" | base64 -d > /etc/systemd/system/lc-hook.service &&
            systemctl daemon-reload &&
            systemctl enable lc-hook.service
        elif [ -f /etc/rc.local ]; then
            echo '/usr/bin/linuxconf _startup_all' >> /usr/bin/linuxconf _startup_all
        else
            lcf_err "neither systemd nor /etc/rc.local available."
        fi || return $?
        #TODO: check if any user installed non-root startup hook
    else
        [ -f /etc/systemd/system/lc-hook.service ] && return 0
        grep '/usr/bin/linuxconf _startup_all' /etc/rc.local >/dev/null 2>&1 && return 0
        # TODO: install non-root startup hook
        lcf_die "non-root startup hook not supported yet"
    fi
}
function lci_install_login_hook () {
    # https://wiki.archlinux.org/title/XDG_Autostart
    [ "$XDG_CONFIG_DIRS" = "" ] && local XDG_CONFIG_DIRS=/etc/xdg
    [ "$XDG_CONFIG_HOME" = "" ] && local XDG_CONFIG_HOME="$HOME/.config"
    local desktop_file=W0Rlc2t0b3AgRW50cnldClR5cGU9QXBwbGljYXRpb24KTmFtZT1sY194ZGdfbG9naW4KRXhlYz0vdXNyL2Jpbi9saW51eGNvbmYgX3hkZ19sb2dpbgpYLUdOT01FLUF1dG9zdGFydC1lbmFibGVkPXRydWUK

    # if is root:
    #   if root config exists, overwrite it
    #   if non-root config exists, clear it
    # else:
    #   if root config exists, return
    #   if non-root config exists, overwrite it

    if [[ "$(whoami)" = root ]]; then
        mkdir -p "$XDG_CONFIG_DIRS/autostart" &&
        echo "$desktop_file" | base64 -d > "$XDG_CONFIG_DIRS/autostart/lc-hook.desktop" || return $?

        for uconfig in /home/*/.config/autostart/lc-hook.desktop "$XDG_CONFIG_HOME/autostart/lc-hook.desktop"; do
            [ -f "$uconfig" ] && rm -f "$uconfig" || true
        done
    else
        [ -f "$XDG_CONFIG_DIRS/autostart/lc-hook.desktop" ] && return 0

        mkdir -p "$XDG_CONFIG_HOME/autostart" &&
        echo "$desktop_file" | base64 -d > "$XDG_CONFIG_HOME/autostart/lc-hook.desktop" || return $?
    fi
}

lci_version=0.2.0
subcommand="$1"
if [[ "$subcommand" != register ]] && [[ "$subcommand" != "" ]]; then
    [[ ! -f /etc/linuxconf.conf ]] && lcf_die "Please run '$0 register <path/to/masterconf.sh>' at least once"
fi
if [[ "$subcommand" = register ]]; then
    lci_register "$2"
    lci_install_startup_hook || lcf_die "failed to install on_startup hook"
    lci_install_login_hook || lcf_die "failed to install on_login hook"
    lci_init_if_needed
    lci_startup_if_needed
elif [[ "$subcommand" = _cron ]]; then
    # TODO: implement cron. with crontab or same systemd service?
    :
elif [[ "$subcommand" = _startup ]]; then
    lci_startup_if_needed
elif [[ "$subcommand" = _startup_all ]]; then
    # systemd should call this service as root, and it will spawn subprocess for all users with sudo
    [[ "$(whoami)" != root ]] && lcf_die "$0 _startup_all started as non-root. Exit because sudo might fail."
    ar_uname=($(lci_state_file_list /etc/linuxconf.conf init_done)) || lcf_die "List all initialized users: lci_state_file_list failed"
    for uname in "${ar_uname[@]}"; do
        lcf_echo2 "Spawn subprocess '$0 _startup' as user $uname..."
        sudo -u "$uname" "$0" _startup
    done
elif [[ "$subcommand" = _xdg_login ]]; then
    lci_call lc_login
else
    lci_usage
    exit
fi

