#!/bin/bash

function Usage 
{
	echo -e "\nUsage:"
	echo -e "\t$(basename $0) [-a <admin>][-d <domain>][-h][-H <hostname>][-I][-J][-L][-S][-u <user>][-v]"
	echo -e "\t\t-a <admin>\tDomain admin [${ADMIN}]."
	echo -e "\t\t-d <domain>\tDomain admin [${DOMAIN}]."
	echo -e "\t\t-h\t\tDisplay this help message."
	echo -e "\t\t-H <hostname>\tDefine new host name."
	echo -e "\t\t-I\t\tInitialize default setup."
	echo -e "\t\t-J\t\tJoin default domain."
	echo -e "\t\t-L\t\tLeave default domain."
	echo -e "\t\t-S\t\tSetup SSH access."
	echo -e "\t\t-u <user>\tDefault user [${DEF_USER}]."
	echo -e "\t\t-v\t\tBe verbose.\n"
	exit $1
}

function Abend 
{
	local msg=$1
	local ret=$2
	echo -e "\n$(basename $0) - ABEND: ${msg}\n" 1>&2
	[ ${ret} -gt 0 ] && exit ${ret}
	exit -1
}

function AssurePkg 
{
	[ -z "${VERBOSE}" ] || echo "AssurePkg: $@"
	for pkg in "$@"; do
		PKGINFO=$(dpkg-query -W -f='${Status} ${Version}' $pkg 2> /dev/null)
		if [ $? -gt 0 ]; then 
			[ -z "${VERBOSE}" ] || echo "AssurePkg: Installing $pkg ... [#1]"
			apt-get install -y $pkg
			[ $? -gt 0 ] && Abend "AssurePkg cannot install <$pkg>" $?
		elif [[ $PKGINFO =~ not-installed ]]; then
			[ -z "${VERBOSE}" ] || echo "AssurePkg: Installing $pkg ... [#2]"
			apt-get install -y $pkg
			[ $? -gt 0 ] && Abend "AssurePkg cannot install <$pkg>" $?
		fi
	done
}

function PatchConfig
{
	local key=$1
	local val=$2
	local cfg=$3
	grep -q -E "$key" "$cfg"
	if [ $? -ne 0 ]; then
		echo "$key = $val" >> "$cfg"
	else
		sed -E -e "s:$key\s*=\s*\S+:$key = $val:gi" -i "$cfg"
	fi
}

#
# Input validation helpers (Debian 6+ / bash)
#

_is_valid_dns_label() 
{
  # RFC-ish label: 1..63 chars, alnum or hyphen, no leading/trailing hyphen
  local s="$1"
  [[ -n "$s" ]] || return 1
  (( ${#s} <= 63 )) || return 1
  [[ "$s" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]] || return 1
  return 0
}

_is_valid_domain_name() 
{
  # Domain: one or more labels separated by dots; total length <= 253 (common DNS limit)
  local d="$1"
  [[ -n "$d" ]] || return 1
  (( ${#d} <= 253 )) || return 1
  # No leading/trailing dot, no empty labels, no consecutive dots
  [[ "$d" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$ ]] || return 1

  # Per-label strictness (optional but makes failure modes clearer)
  local IFS='.'
  local part
  for part in $d; do
    _is_valid_dns_label "$part" || return 1
  done
  return 0
}

_is_valid_host_name() 
{
  # Short hostname: single label only
  local h="$1"
  _is_valid_dns_label "$h"
}

_is_valid_fqdn_or_host() 
{
  # Accept either short hostname OR fqdn (host + domain)
  local x="$1"
  [[ -n "$x" ]] || return 1
  # Disallow whitespace
  [[ "$x" =~ ^[^[:space:]]+$ ]] || return 1

  if [[ "$x" == *.* ]]; then
    local host="${x%%.*}"
    local dom="${x#*.}"
    _is_valid_host_name "$host" || return 1
    _is_valid_domain_name "$dom" || return 1
    return 0
  else
    _is_valid_host_name "$x"
  fi
}

_normalize_dns_name() 
{
  # Remove trailing dot and lowercase (DNS is case-insensitive; this helps consistency)
  local s="$1"
  s="${s%.}"
  echo "${s,,}"
}

#
# Environment testing tools
#

has_timeout() 
{
  command -v timeout >/dev/null 2>&1
}

run_with_timeout() 
{
  local t="$1"
  shift

  if has_timeout; then
    timeout "$t" "$@"
  else
    "$@"
  fi
}

has_hostnamectl() 
{
  command -v hostnamectl >/dev/null 2>&1 || return 1
  [[ -d /run/systemd/system ]] || return 1
  [[ "$(ps -p 1 -o comm= 2>/dev/null)" == "systemd" ]] || return 1

  run_with_timeout 2 hostnamectl status >/dev/null 2>&1 || return 1
  return 0
}

#
#	Setup default settings
#

AssurePkg hostname sed grep

DOMAIN=$(hostname -d 2>/dev/null || echo "") 
HOST=$(hostname -s)
DEF_USER="hostmaster"
CMD=
CMDOPT=
ADMIN="Administrator"
VERBOSE=
WGET_OPTS="--no-check-certificate"

#
#	Setup  and actions
#

function SetupHostName
{
  local fqdn="$1"

  local newHostName=${fqdn%%.*}
  local oldHostName=${HOST}

  local newDomainName=${fqdn#*.}
  [[ "$newHostName" == "$fqdn" ]] && newDomainName="${DOMAIN}"

  local oldDomainName
  oldDomainName=$(hostname -d 2>/dev/null || echo "")

  # Build old/new FQDN values (may be hostname-only if domain is empty)
  local oldFqdn="$oldHostName"
  [[ -n "$oldDomainName" ]] && oldFqdn="${oldHostName}.${oldDomainName}"

  local newFqdn="$newHostName"
  [[ -n "$newDomainName" ]] && newFqdn="${newHostName}.${newDomainName}"

  # Regex-safe dot escaping for exact FQDN token matching
  local oldFqdnRe="${oldFqdn//./\\.}"
  
  # ---------------------------------------------------------------------------
  # /etc/hosts update logic (in-place only, touch only if change is needed)
  # ---------------------------------------------------------------------------

  local has127=0
  grep -qE '^[[:space:]]*127\.0\.1\.1[[:space:]]+' /etc/hosts && has127=1

  if [[ $has127 -eq 1 ]]; then
    # Default Debian-style hosts file detected: 127.0.1.1 is authoritative.
    # Normalize ONLY if needed; preserve trailing comment.

    local line before current_norm desired_norm
    line=$(grep -E '^[[:space:]]*127\.0\.1\.1[[:space:]]+' /etc/hosts | head -n1)

    before=${line%%#*}

    current_norm=$(echo "$before" | tr -s '[:space:]' ' ' | sed -E 's/^ //; s/ $//')
    desired_norm=$(echo "127.0.1.1 ${newFqdn} ${newHostName}" | tr -s '[:space:]' ' ' | sed -E 's/^ //; s/ $//')

    if [[ "$current_norm" != "$desired_norm" ]]; then
      [ -z "${VERBOSE}" ] || echo "Update /etc/hosts (127.0.1.1): ${current_norm} -> ${desired_norm}"

      sed -E -i \
        "s|^([[:space:]]*127\.0\.1\.1[[:space:]]+)[^#]*([[:space:]]*#.*)?$|\\1${newFqdn} ${newHostName}\\2|" \
        /etc/hosts
    else
      [ -z "${VERBOSE}" ] || echo "/etc/hosts (127.0.1.1) already up to date - skipping."
    fi

  else
    # Custom hosts file: do not assume any IP/layout; update only name tokens.
    # Touch file only if at least one replacement would apply (non-comment parts only).

    local need_hosts_update=0
    local oldFqdnRe="${oldFqdn//./\\.}"

    # 1) Exact oldFqdn token present (only relevant if domain changed and old domain is known)
    if [[ -n "$oldDomainName" && "$oldDomainName" != "$newDomainName" ]]; then
      grep -qE "^[[:space:]]*[^#]*([^[:alnum:]-]|^)${oldFqdnRe}([^[:alnum:]-]|$)" /etc/hosts && need_hosts_update=1
    fi

    # 2) Any FQDN token starting with old host label: oldHost.<...>
    if [[ $need_hosts_update -eq 0 ]]; then
      grep -qE "^[[:space:]]*[^#]*([[:space:]]|^)${oldHostName}\.[[:alnum:]-]" /etc/hosts && need_hosts_update=1
    fi

    # 3) Short hostname token: oldHost
    if [[ $need_hosts_update -eq 0 ]]; then
      grep -qE "^[[:space:]]*[^#]*([[:space:]]|^)${oldHostName}([[:space:]]|$)" /etc/hosts && need_hosts_update=1
    fi

    if [[ $need_hosts_update -eq 1 ]]; then
      [ -z "${VERBOSE}" ] || echo "Update /etc/hosts (custom): updating host/FQDN tokens"

      sed -E -i "
        /^[[:space:]]*#/b

        # Work on non-comment part only, keep comment untouched
        h
        s/[[:space:]]*#.*$//

        # If the old FQDN exists as an exact token (domain change case), replace it
        $( [[ -n "$oldDomainName" && "$oldDomainName" != "$newDomainName" ]] && \
           echo "s/(^|[[:space:]])${oldFqdnRe}([[:space:]]|$)/\\1${newFqdn}\\2/g" )

        # Replace any token with old host label: oldHost.<suffix> -> newHost.<suffix>
        # NOTE: charclass fixed to avoid 'Invalid range end' (hyphen at end)
        s/(^|[[:space:]])${oldHostName}\.([[:alnum:]-][[:alnum:].-]*)([[:space:]]|$)/\\1${newHostName}.\\2\\3/g

        # Replace short hostname token: oldHost -> newHost
        s/(^|[[:space:]])${oldHostName}([[:space:]]|$)/\\1${newHostName}\\2/g

        # Re-attach original comment (if any)
        x
        s/^[^#]*//
        x
        G
        s/\n//
      " /etc/hosts
    else
      [ -z "${VERBOSE}" ] || echo "No matching hostname tokens in /etc/hosts - skipping."
    fi
  fi
  
  # ---------------------------------------------------------------------------
  # Hostname change (system hostname + /etc/hostname)
  # ---------------------------------------------------------------------------

  if [[ "$oldHostName" != "$newHostName" ]]; then
    [ -z "${VERBOSE}" ] || echo "Setup host name: ${oldHostName} -> ${newHostName}"

    if has_hostnamectl; then
      hostnamectl hostname "${newFqdn}"
    else
      echo "${newHostName}" > /etc/hostname
      hostname "${newHostName}" 2>/dev/null || true
    fi

    [ -e /etc/mailname ] && echo "$(hostname -f)" > /etc/mailname
    HOST=$(hostname -s)
  fi
}

function InitDefaultSetup
{
	[ -z "${VERBOSE}" ] || echo "Initialize default setup"
	AssurePkg vim sudo virt-what
	local vm=$(virt-what)
	case ${vm} in
		vmware )
			AssurePkg open-vm-tools
			;;
	esac
	if [ -e /root/.bashrc ]; then
		[ -z "${VERBOSE}" ] || echo "Patch /root/.bashrc ..."
		sed -E -e "s/^#\s*export\s+LS_OPTIONS=(.*)$/export LS_OPTIONS=\1/g" -i /root/.bashrc
		sed -E -e "s/^#\s*eval\s+(\"\`dircolors\`\")/eval \1/g" -i /root/.bashrc
		sed -E -e "s/^#\s*alias\s+ls=(.*)$/alias ls=\1/g" -i /root/.bashrc
		sed -E -e "s/^#\s*alias\s+ll=(.*)$/alias ll=\1\nalias la='ls \$LS_OPTIONS -la'/g" -i /root/.bashrc
	fi
	if [ -e /etc/skel/.bashrc ]; then
		[ -z "${VERBOSE}" ] || echo "Patch /etc/skel/.bashrc ..."
		sed -E -e "s/^#\s*alias\s+ll=(.*)$/alias ll=\'ls -l\'/g" -i /etc/skel/.bashrc
		sed -E -e "s/^#\s*alias\s+la=(.*)$/alias la=\'ls -la\'/g" -i /etc/skel/.bashrc
	fi
	if [ -e /home/${DEF_USER}/.bashrc ]; then
		[ -z "${VERBOSE}" ] || echo "Patch /home/${DEF_USER}/.bashrc ..."
		sed -E -e "s/^#\s*alias\s+ll=(.*)$/alias ll=\'ls -l\'/g" -i /home/${DEF_USER}/.bashrc
		sed -E -e "s/^#\s*alias\s+la=(.*)$/alias la=\'ls -la\'/g" -i /home/${DEF_USER}/.bashrc
	fi 
	sed -E -e "s/^sudo:(.*):(.*):\s*$/sudo:\1:\2:${DEF_USER}/g" -i /etc/group
}

function DoJoinDomain
{
	local domain=$1
	#
	#	Remarks:
	#		use_fully_qualified_names	=> login-names and groups in sudoers
	#		ad_gpo_access_control		=> BUG workaround, since GPO isn't working
	#
	[ -z "${VERBOSE}" ] || echo "Joining domain: ${domain}"
	AssurePkg realmd packagekit policykit-1
	mkdir -p /var/lib/samba/private
	[ -z "${VERBOSE}" ] || echo "realm join --user=${ADMIN} ${domain}"
	realm join --user=${ADMIN} ${domain}
	[ $? -ne 0 ] && Abend "AD join failed.\nPlease run 'journalctl -xn' to determine why.", 1
	local sssd_version="$(apt-cache show sssd | grep -E -m 1 '^Version:' | sed -E 's/^.*\s//')"
	[ -z "${VERBOSE}" ] || echo "SSSD version: ${sssd_version}"
	PatchConfig enumerate "True" "/etc/sssd/sssd.conf"
	PatchConfig use_fully_qualified_names "False" "/etc/sssd/sssd.conf"
	PatchConfig fallback_homedir "/home/%d/%u" "/etc/sssd/sssd.conf"
	PatchConfig ldap_id_mapping "False" "/etc/sssd/sssd.conf"
	PatchConfig ad_gpo_access_control "permissive" "/etc/sssd/sssd.conf"
#	sed -E -e "s/^#?\s*(ad_gpo_access_control)/#\1/g" -i /etc/sssd/sssd.conf
  if dpkg --compare-versions "${sssd_version}" ge '2.4.1'; then
  	PatchConfig ad_gpo_ignore_unreadable "True" "/etc/sssd/sssd.conf"
#	  sed -E -e "s/^#?\s*(ad_gpo_ignore_unreadable)/#\1/g" -i /etc/sssd/sssd.conf
  fi
	systemctl enable sssd
	systemctl restart sssd
	echo "session required pam_mkhomedir.so skel=/etc/skel/ umask=0022" | tee -a /etc/pam.d/common-session
	AssurePkg libsss-sudo 
	echo "%domain\ admins ALL=(ALL) ALL" | tee -a /etc/sudoers.d/domain_admins 
#	echo "%linux\ admins ALL=(ALL) ALL" | tee -a /etc/sudoers.d/domain_admins 
	echo "%domain\ staff ALL=(ALL) ALL" | tee -a /etc/sudoers.d/domain_staff 
	echo "%staff ALL=(ALL) ALL" | tee -a /etc/sudoers.d/domain_staff 
	echo "The computer is joined to the domain.  Please reboot, ensure that you are connected to the network, and you should be able to login with domain credentials." 
}

function DoLeaveDomain
{
	local domain=$1
	[ -z "${VERBOSE}" ] || echo "Leaving domain: ${domain}"
	[ -z "${VERBOSE}" ] || echo "realm leave --user=${ADMIN}"
	realm leave --user=${ADMIN}
	[ $? -ne 0 ] && Abend "AD leave failed.\nPlease run 'journalctl -xn' to determi -ine why.", 1
	sed '/session required pam_mkhomedir.so skel=\/etc\/skel\/ umask=0022/d' -i /etc/pam.d/common-session
	sed "/%domain\\\ admins/d" -i /etc/sudoers.d/domain_admins
	sed "/%linux\\\ admins/d" -i /etc/sudoers.d/domain_admins
	sed "/%domain\\\ staff/d" -i /etc/sudoers.d/domain_staff
	sed "/%staff/d" -i /etc/sudoers.d/domain_staff
}

function SetupSSHAccess
{
	local ssh_dir="$1/.ssh"
	local user=$(basename $1)
	AssurePkg openssh-server
	if [ ! -e ${ssh_dir} ]; then
		sudo --user=${user} mkdir -p ${ssh_dir}
		chmod 700 ${ssh_dir}
	fi
	[ -e ${ssh_dir}/authorized_keys ] || sudo --user=${user} wget -O ${ssh_dir}/authorized_keys ${WGET_OPTS} http://www.ac.pestinger.net/devop-tools/authorized_keys
}

#
#	Process cmd-line-options
#
while getopts ":a:d:hH:IJLSu:v" opt; do
	case ${opt} in
		a )
			ADMIN=${OPTARG}
			;;
    d )
      # Validate domain name
      _d="$(_normalize_dns_name "${OPTARG}")"
      _is_valid_domain_name "${_d}" || Abend "Invalid domain name format for -d: '${OPTARG}'" 2
      DOMAIN="${_d}"
      ;;
		h )
			Usage 0
			;;
    H )
      # Validate hostname or FQDN
      _h="$(_normalize_dns_name "${OPTARG}")"
      _is_valid_fqdn_or_host "${_h}" || Abend "Invalid host name / FQDN format for -H: '${OPTARG}'" 2
      SetupHostName "${_h}"
      CMDOPT="${CMDOPT}${opt}"
      ;;			
		I )
			InitDefaultSetup
			CMDOPT="${CMDOPT}${opt}"
			;;
		J )
			[ -z "${CMD}" ] || Abend "Invalid Option combination: -${OPTARG}" 2
			CMD="join"
			;;
		L )
			[ -z "${CMD}" ] || Abend "Invalid Option combination: -${OPTARG}" 2
			CMD="leave"
			;;
		S )
			[ -z "${CMD}" ] || Abend "Invalid Option combination: -${OPTARG}" 2
			CMD="ssh"
			;;
	  u )
	    DEF_USER=${OPTARG}
	    ;;
		v )
			VERBOSE=1
			;;
		\? )
			Abend "Invalid Option: -${OPTARG}" 2
			;;
	esac
done
shift $((OPTIND - 1))
#
#	Execute command(s)
#
[[ -n ${CMD} ]] || [[ -n ${CMDOPT} ]] || Usage -2
case ${CMD} in
	join )
		[ -n "${DOMAIN}" ] || Abend "DOMAIN is undefined!" 2
		DoJoinDomain ${DOMAIN}
		;;
	leave )
		[ -n "${DOMAIN}" ] || Abend "DOMAIN is undefined!" 2
		DoLeaveDomain ${DOMAIN}
		;;
  ssh )
		SetupSSHAccess /root
		def_user_path="$(getent passwd ${DEF_USER} | cut -d: -f6)"
		[[ -n ${def_user_path} ]] && [[ -d $(realpath ${def_user_path}) ]] && SetupSSHAccess ${def_user_path}
		;;
esac
exit 0


