#!/usr/bin/bash

# source bash base library
# shellcheck disable=SC1091
source /usr/libexec/bash-base.bash || {
   echo "$0: fatal error: failed to source /usr/libexec/bash-base.bash" >&2
   exit 1
}

bb_require_libs bash-ini ldapusermgmt/common

# shellcheck disable=SC2120,SC2034
function local_usage() {
    usage "${1-1}" "${2-}" "<section> <key> .." "<section> -" <<EOF
    [ -p <password-hash> ]                 .. enforce this hash for all users
  Create one or more users.
  Note: arguments specified on command line override those in config file
EOF
}

# get_free_uid <retvar> <searchbase> <searchlimit>
function get_free_uid() {
    (( $# == 2 )) || (( $# == 3 )) || \
        bb_fatal "get_free_uid needs 2 or 3 args but called with $#"

    local uid=$2
    local -i limit="${3-0}"

    while ldap_cmd read ldapsearch \
        "(&(objectClass=posixAccount)(uidNumber=$uid))" \
        uidNumber | grep -E -q '^uidNumber:'
    do
        if (( limit > 0 && uid >= limit ))
        then
            return 1
        fi
        uid=$((uid + 1))
    done
    printf -v "$1" "%i" "$uid"
}

function parse_local_arg() {
    (( $# == 1 )) || bb_fatal "parse_local_arg called with $# instead of 1 arg"

    local _op_ch="$1"

    case "$_op_ch" in
        p) declare -g PASSWORD_HASH="$OPTARG";;
#       r) declare -g RESOLVER="$OPTARG";;
#       R) declare -g RESOLVER_ARGS="$OPTARG";;
        *) return 1
    esac
}

# config_read_useradd <section>
function config_read_useradd() {
    (( $# == 1 )) || \
        bb_fatal "config_read_useradd: called with $# instead of 1 arg"

    local _section="$1"

    config_section_exists "$_section" || \
        bb_quit 1 "no such config section: $_section"

    # env settings
    config_read_optional HOMEDIR_HOST "homedir host" "$_section"

    # resolvers
    # if resolver was not specified on cmdline we read it from config file
    # config_read_mandatory RESOLVER_CONFDIR "resolver conf dir" "$_section"
    # [[ ${RESOLVER+x} == x ]] || \
    #   config_read_mandatory RESOLVER "resolver" "$_section"
    # [ -d "$RESOLVER_CONFDIR" ] || \
    #   bb_fatal "resolver conf dir does not exist: $RESOLVER_CONFDIR"

    # if resolver args was not specified on cmdline we read it from config file
    # [[ ${RESOLVER_ARGS+x} == x ]] || \
    #   config_read_mandatory RESOLVER_ARGS "resolver args" "$_section"

    # resolving
    config_read_mandatory RESOLVER_PLUGIN "resolver plugin" "$_section" || \
        bb_quit 1 "failed to read mandatory config entry: resolver plugin"
    config_read_optional RESOLVER_PLUGIN_ARGS \
        "resolver plugin args" "$_section"

    config_read_mandatory USERNAME_FORMAT "username format" "$_section" || \
        bb_quit 1 "failed to read mandatory config entry: username format"
    config_read_mandatory GECOS_FORMAT "gecos format" "$_section" || \
        bb_quit 1 "failed to read mandatory config entry: gecos format"
    config_read_optional  LDAP_ADDONS_FORMAT "ldap addons format" "$_section"

    # values for selected section
    config_read_mandatory UID_BASE "uid base" "$_section" || \
        bb_quit 1 "failed to read mandatory config entry: uid base"
    # shellcheck disable=SC2034
    config_read UID_LIMIT "uid limit" "$_section" || UID_LIMIT=0
 #   config_read_optional USERNAME_PREFIX "username prefix" "$_section"

    config_read_optional LDAP_EXTRA_SUBHIER "ldap extra subhier" "$_section"
    config_read_mandatory HOMEDIR_PREFIX "homedir prefix" "$_section" || \
        bb_quit 1 "failed to read mandatory config entry: homedir prefix"
    config_read_optional LDAP_EXTRA_SUBHIERACHY "ldap subhierachy" "$_section"
    config_read_optional USER_SKEL "skel" "$_section"
    config_read_optional USER_QUOTA "homedir quota" "$_section"

    config_read_optional POST_CREATE_USER_HOOK "post create user hook" \
        "$_section"
    config_read_optional POST_CREATE_USERS_HOOK "post create users hook" \
        "$_section"
    config_read_optional POST_CREATE_HOMEDIR_HOOK "post create homedir hook" \
        "$_section"
}

# add_user <username> <gecos> [<ldap-extra-data>]
# returns 91 (skipped), 92, (ldifuseradd failed), 93 (ldapadd failed)
#         94 (files helper failed), 95 (partly exists)
function add_user() {
    (( $# == 2 || $# == 3 )) || \
        bb_fatal "add_user called with $# instead of 2 or 3 args"

    local USERNAME="$1"
    local GECOS="$2"
    local LDAP_DATA_USER="${3-}"

    (( ${#USERNAME} < 33 )) || {
        USERNAME="${USERNAME:0:32}"
        bb_msg info "add_user: cropping username to 32 chars: $USERNAME"
    }

    if lum_launch lumfinger -q "$USERNAME"
    then
        bb_msg info "skipping existing user: $USERNAME"
        return 91
    fi

    local -a _ldif_cmd=("ldifuseradd")

    # set password
    _ldif_cmd+=("-p" "${PASSWORD_HASH-'{SSHA}x'}")

    [ -z "${LDAP_EXTRA_SUBHIER-}" ] || _ldif_cmd+=("-X" "$LDAP_EXTRA_SUBHIER")

    [ "$WINDOWS_USERS" != "true" ] || _ldif_cmd+=("-w" "-y" "x" "-z" "x")

    if [ "$AUTOFS_USERS" == "true" ]
    then
        _ldif_cmd+=("-a" "-P" "$HOMEDIR_HOST:$HOMEDIR_PREFIX" "-Q")
    else
        _ldif_cmd+=("-d" "$HOMEDIR_PREFIX/$USERNAME")
    fi

    [ -z "$LDAP_DATA_USER" ] || _ldif_cmd+=("-L" "$LDAP_DATA_USER")

    local this_uid
    get_free_uid this_uid "$NEXT_UID_CANDIDATE" || \
        bb_quit 1 "no free UID found"
    NEXT_UID_CANDIDATE="$((this_uid + 1))"
    local this_upgid="$this_uid"

    _ldif_cmd+=("-c" "$GECOS" "-u" "$this_uid" "$USERNAME")

    # create ldif content
    bb_msg debug "calling ${_ldif_cmd[*]}"
    "${_ldif_cmd[@]}" > "$TMPDIR/$USERNAME.ldif" || {
        bb_msg err "failed to create users ldif file ($?): ${_ldif_cmd[*]})"
        return 92
    }

    # make sure no dn exists yet, to enable safe cleanup on failure below
    local -a add_user__ldif_dns=() add_user__ldap_dns=()
    local _dn _dn_head _dn_tail

    ldap_extract_attributes dn add_user__ldif_dns < "$TMPDIR/$USERNAME.ldif"
    for _dn in "${add_user__ldif_dns[@]}"
    do
        _dn_head="${_dn%%,*}"
        _dn_tail="${_dn#*,}"

        ldap_extract_attributes dn add_user__ldap_dns \
            < <(ldap_cmd read ldapsearch -QLLL -b "$_dn_tail" "$_dn_head" dn)

        (( ${#add_user__ldap_dns[@]} == 0 )) || {
            bb_msg err "failed to create user, it partly exists" \
                "in ldap: ${add_user__ldap_dns[*]}"
            return 95
        }
    done

    # add ldif content to ldap server and cleanup on error
    ldap_cmd write ldapadd -f "$TMPDIR/$USERNAME.ldif" || {
        bb_msg err "ldapadd of ldif data failed with retval: $?"
        bb_msg debug "cleaning up non complete useradd dns:" \
            "${add_user__ldif_dns[*]}"
        ldap_cmd write ldapdelete "${add_user__ldif_dns[@]}" 2>/dev/null
        return 93
    }


    # shellcheck disable=SC2034
    [ -n "${LDAP_ONLY-}" ] || {
        local CU_UID="$this_uid"
        local CU_GID="$this_upgid"
        local CU_HOMEPATH="$HOMEDIR_PREFIX/$USERNAME"
        local CU_SKEL="${USER_SKEL-}"
        local CU_HOMEQUOTA="${USER_QUOTA-}"
        local CU_POST_CREATE_HOMEDIRHOOK="${POST_CREATE_HOMEDIR_HOOK-}"
        homedir_host_helper "$main__homedir_host" create_userdata \
            CU_UID CU_GID CU_HOMEPATH CU_SKEL CU_HOMEQUOTA \
            CU_POST_CREATE_HOMEDIRHOOK || {
            bb_msg err "error creating userdata for $USERNAME: $?"
            bb_msg err "deleteing user $USERNAME"
            lumuserdel "$USERNAME"
            return 1
        }
    }

    bb_msg debug "just added $USERNAME uid=$this_uid gid=$this_upgid on" \
        "$HOMEDIR_HOST:$HOMEDIR_PREFIX/$USERNAME"
}

function process_key_old() {
    (( $# == 1 )) || bb_fatal "process_key called with $# instead of 1 arg"

    local key="$1"

    [ -n "$key" ] || return 0 # skip empty lines

    bb_msg debug "resolving key: $RESOLVER_BIN ${RESOLVER_ARGS-} '$key'"
    if RESOLVER_OUTPUT=$(eval "$RESOLVER_BIN ${RESOLVER_ARGS-} \"$key\"")
    then
        local _retval

        bb_msg debug "resolver output: '$RESOLVER_OUTPUT'"
        add_user "$RESOLVER_OUTPUT" || {
            _retval="$?"
            (( _retval == 91 )) || return "$_retval"
        }
    else
        bb_msg err "resolver failed to lookup ($?): $key"
        return 1
    fi
}

function process_key() {
    (( $# == 1 )) || bb_fatal "process_key called with $# instead of 1 arg"

    local _key="$1"

    [ -n "$_key" ] || return 0 # skip empty lines
    bb_msg debug "process_key: key=$_key"

    declare -f "plugin_${RESOLVER_PLUGIN}_resolve" >/dev/null || \
        bb_quit 1 "fatal: no resolver plugin called $RESOLVER_PLUGIN found"

    local -a _res_args=()
    # FIXME: RESOLVER_PLUGIN_ARGS should better be an array
    # shellcheck disable=SC2206
    [ -z "${RESOLVER_PLUGIN_ARGS-}" ] || _res_args=($RESOLVER_PLUGIN_ARGS)

    # shellcheck disable=SC2034
    local -A process_key__attrs
    bb_msg debug "process_key: calling resolver plugin:" \
        "\"plugin_${RESOLVER_PLUGIN}_resolve" \
        "process_key__attrs \"$_key\"" "${_res_args[@]}"

    local _retval=0
    "plugin_${RESOLVER_PLUGIN}_resolve" \
        process_key__attrs "$_key" "${_res_args[@]}" || _retval="$?"

    case "$_retval" in
        8)
            bb_msg err "skipping unknown user for key: $_key"
            return 0
            ;;
        92)
            bb_msg err "resolver failed for key: $_key"
            return 0
            ;;
        91) bb_msg warning "resolver did not get all arguments for: $_key";;
        0) :;;
        *) bb_msg alert "resolver returned unknown error code: $_retval";;
    esac

    bb_msg debug "process_key: resolver plugin returned:" \
        "$(declare -p process_key__attrs)"

    local process_key__username process_key__gecos
    format_apply process_key__username "$USERNAME_FORMAT" \
        process_key__attrs "username format" || return 1
    format_apply process_key__gecos "$GECOS_FORMAT" \
        process_key__attrs "gecos format" || return 1

    # shellcheck disable=SC2034
    local process_key__addons=
    [ -z "${LDAP_ADDONS_FORMAT-}" ] || {
        format_apply process_key__addons "$LDAP_ADDONS_FORMAT" \
            process_key__attrs "ldap addons format" || return 1
    }

    bb_msg debug "username=$process_key__username"
    bb_msg debug "gecos=$process_key__gecos"

    if add_user "$process_key__username" "$process_key__gecos" \
        "$process_key__addons"
    then
        [ -z "${POST_CREATE_USER_HOOK-}" ] || {
            local process_key__posthook

            format_apply process_key__posthook "$POST_CREATE_USER_HOOK" \
                process_key__attrs "post create user hook"
            if [ -z "${DRYRUN-}" ]
            then
                bb_msg debug "process_key: calling post create user hook:" \
                    "$process_key__posthook"
                $process_key__posthook || \
                    bb_msg err "post create user hook failed for key '$_key': $?"
            else
                bb_msg info "[dry run] calling post create user hook:" \
                    "$process_key__posthook"
            fi
        }
    else
        return "$?"
    fi
}

function cleanup() {
    [ -z "${TMPDIR-}" ] || rm -rf "$TMPDIR"
}

function main() {
    # shellcheck disable=SC2119
    (( $# > 1 )) || local_usage 1 "Missing mandatory arguments"

    local _section="$1"
    shift

    # useradd for history reason is the only tool that takes SECTION as
    # first local arg
    # if the section is also specified with -s make sure its the same or fail
    # in all cases make sure the SECTION variable is used
    if [ -z "${SECTION-}" ]
    then
        # -s was not used, set it from local arg
        declare -g SECTION="$_section"
        # and lets pass it to other commands
        COMMON_LUM_ARGS+=("-s" "$SECTION")
    else
        # -s was used, make sure its same as local arg
        [ "$_section" == "$SECTION" ] || \
            local_usage 1 "two different sections specified on command line"
    fi
    # lets use SECTION from now on
    unset _section

    config_init
    config_read_useradd "$SECTION"

    load_plugins

    NEXT_UID_CANDIDATE="$UID_BASE"

    # useradd will always work on one remote files host
    # so we lookup and try nop before starting to work
    [ -n "${LDAP_ONLY-}" ] || {
        local main__homedir_host

        config_read_with_default main__homedir_host \
            "homedir host" "localhost" "$SECTION"

        homedir_host_helper "$main__homedir_host" nop || {
            bb_quit 1 "failed to reach homedir server (if remove then" \
                "check SSH authorized_keys and local known_hosts"
        }
    }

    # read target CNs from pipe if requested
    local -a _target_keys
    if [[ "$#" == "1" && "$1" == "-" ]]
    then
        if [ -t 0 ]
        then
            bb_quit 1 "arg '-' while stdin is a terminal but must not be one"
        fi

        readarray -t _target_keys
    else
        # target CNs are specified as args
        _target_keys=("$@")
    fi

    declare -g TMPDIR
    TMPDIR=$(mktemp -d /tmp/lumuseradd.XXXXXXXXXXX) || \
        bb_fatal "failed to: mktemp -d /tmp/lumuseradd.XXXXXXXXXXX"
    bb_register_exit_hook cleanup
    chmod 700 "$TMPDIR"
    bb_msg debug "using temp dir: $TMPDIR"

    local _current_key
    local -i _okays=0
    for _current_key in "${_target_keys[@]}"
    do
        if process_key "$_current_key"
        then
            _okays=$((_okays + 1))
        else
            (( $? == 91 )) || \
                bb_msg err "failed to create user with key: $_current_key"
        fi
    done

    [ -z "${POST_CREATE_USERS_HOOK-}" ] || {
        if (( _okays == 0 ))
        then
            bb_msg info "did not create any users," \
                "skipping 'post create users hook'"
        else
            if [ -z "${DRYRUN-}" ]
            then
                bb_msg debug "main: calling post create users hook:" \
                    "$POST_CREATE_USERS_HOOK"
                $POST_CREATE_USERS_HOOK || \
                    bb_msg err "ERROR: post create users hook failed: $?"
            else
                bb_msg info "[dry run] calling post create users hook:" \
                    "$POST_CREATE_USERS_HOOK"
            fi
        fi
    }
}

#parse_common_args main "p:r:R:" parse_local_arg "$@"
parse_common_args main "p:" parse_local_arg "$@"
bb_quit
