#!/usr/bin/env bash
# Samba active directory provision
# Tool for provision samba active directory
#
# Copyright (C) 2024 Evgenii Sozonov <arzdez@altlinux.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# shellcheck disable=SC1091

set -euo pipefail

. shell-getopt
. shell-ini-config
. service-samba-ad-bind
. service-samba-ad-status
. service-samba-ad-functions
. service-samba-ad-configure

show_usage() {
    cat <<EOF
Usage: $PROG_NAME [OPTIONS]

Tool for managing Samba Active Directory Domain Controller.

Options:
  -h, --help          Show this help message and exit.
  -v, --version       Show program's version number and exit.
  -j, --join          Join an existing domain as a Domain Controller.
                      (Requires JSON input with join parameters).
  -d, --demote        Demote this Domain Controller from the domain.
                      (Requires JSON input with demote parameters).
  -p, --provision     Provision a new Active Directory domain.
                      (Requires JSON input with provision parameters).
  -b, --backup        Backup Samba AD data.
  -r, --restore       Restore Samba AD data from backup.
                      (Requires JSON input with restore parameters).
  -s, --status        Show the current status of the Samba AD DC deployment.
  --start             Start the Samba AD DC service.
  --stop              Stop the Samba AD DC service.
  --configure         Configure the Samba AD DC service.
                      (Requires JSON input with configuration parameters).

Input for provision, join, demote, restore, and configure modes is expected
as a JSON object via standard input.

EOF
    exit 0
}

show_version() {
    echo "$PROG_NAME version $VERSION"
    exit 0
}

PROG_NAME="${0##*/}"
VERSION="0.3"
MODE="provision"
GLOBAL_EXIT=0
input_json=

OPTIONS_LIST="help,
              version,
              demote,
              provision,
              restore,
              backup,
              status,
              start,
              stop,
              configure"

OPTIONS_SHORT_LIST="h,v,d,p,b,r,s,c"

TEMP=$(getopt -n "$PROG_NAME" -o "$OPTIONS_SHORT_LIST" -l "$OPTIONS_LIST" -- "$@")
eval set -- "$TEMP"

while :; do
    case "$1" in
        -h | --help)
            show_usage
            ;;
        -v | --version)
            show_version
            ;;
        -d | --demote)
            MODE="demote"
            ;;
        -p | --provision)
            MODE="provision"
            ;;
        -r | --restore)
            MODE="restore"
            ;;
        -b | --backup)
            MODE="backup"
            ;;
        -s | --status)
            MODE="status"
            ;;
        --start)
            MODE="start"
            ;;
        --stop)
            MODE="stop"
            ;;
        -c | --configure)
            MODE="configure"
            ;;
        --)
            shift
            break
            ;;
        *)
            fatal "Unrecognized option: $1"
            ;;
    esac
    shift
done

deploy_service() {
    local retval=0
    local input_json=
    local mode_value=
    local keys=
    input_json="$(read_stdin)"

    keys=$(echo "$input_json" | jq -r 'keys[]')

    for key in $keys; do
        case "$key" in
            mode)
                mode_value=$(echo "$input_json" | jq -r --arg k "$key" '.[$k] | keys_unsorted[0]')
                ;;
            *)
                false
                ;;
        esac
    done

    if [ "$mode_value" = "create" ]; then
        domain_provision "$input_json" || retval=1
    elif [ "$mode_value" = "join" ]; then
        dc_join "$input_json" || retval=1
    fi

    return $retval
}

domain_provision() {
    local input_json="$1"
    local unsensetive_json=
    local args=()
    local global_keys=
    local dns_keys=
    local join_keys=
    local value=
    local dns_backend=
    local domain_realm=
    local forwarders=
    local backend_store_size=
    local hostname=
    local domain_netbios_name=
    local force_deploy=
    local deployed=false
    local retval=0

    global_keys=$(echo "$input_json" | jq -r 'keys[]')

    for key in $global_keys; do
        case "$key" in
            backendStore)
                value=$(get_object_key "$input_json" ".backendStore")
                if [ "$value" = "mdb" ]; then
                    args+=(--backend-store mdb)
                    backend_store_size=$(echo "$input_json" | jq -r '.backendStore.mdb.backendStoreSize')
                    if [ -n "$backend_store_size" ]; then
                        args+=(--backend-store-size "${backend_store_size}Gb")
                    fi
                fi
                ;;
            functionalLevel)
                value=$(get_object_key "$input_json" ".functionalLevel")
                [ -n "$value" ] && args+=(--function-level "$value")
                if [ "$value" = "2016" ]; then
                    args+=(--option="ad dc functional level=2016")
                fi
                ;;
            netBiosName)
                value=$(get_json_value "$input_json" "$key" ".")
                domain_netbios_name=$(to_lower "$value")
                ;;
            realm)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && args+=(--realm "$value")
                domain_realm=$(to_lower "$value")
                ;;
            siteName)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && args+=(--site "$value")
                ;;
            useRfc2307)
                value=$(get_json_value "$input_json" "$key" ".")
                [ "$value" = "true" ] && args+=(--use-rfc2307)
                ;;
            hostNetBiosName)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && hostname="$value"
                ;;
            force_deploy)
                force_deploy=$(get_json_value "$input_json" "$key" ".")
                ;;
            *)
                false
                ;;
        esac
    done

    dns_keys=$(echo "$input_json" | jq -r '.dnsSettings | keys[]')
    for dns_key in $dns_keys; do
        case "$dns_key" in
            dnsBackend)
                value="$(get_object_key "$input_json" ".dnsSettings.dnsBackend")"
                dns_backend=$(echo "$value" | tr -d '[]\n ')
                [ -n "$value" ] && args+=(--dns-backend "$value")
                ;;
            forwarders)
                value="$(get_json_value "$input_json" "$dns_key" ".dnsSettings")"
                forwarders=$(echo "$value" | tr -d '[]"\n ')
                if [ -n "$value" ] && [ "$dns_backend" = "SAMBA_INTERNAL" ]; then
                    args+=("--option=dns forwarder=$value")
                fi
                ;;
            *)
                false
                ;;
        esac
    done

    deploy_keys=$(echo "$input_json" | jq -r '.mode.create | keys[]')
    for deploy_key in $deploy_keys; do
        case "$deploy_key" in
            adminPassword)
                value="$(get_json_value "$input_json" "$deploy_key" ".mode.create")"
                [ -n "$value" ] && args+=(--adminpass "$value")
                ;;
            *)
        esac
    done

    if [ "$dns_backend" = "BIND9_DLZ" ]; then
        if ! bind_is_installed; then
            echo "ERROR: BIND9_DLZ backend selected, but BIND package is not installed."
            return 1
        fi
    fi

    deployed="$(get_deployed)"
    if [ "$deployed" = "true" ] && [ "$force_deploy" != "true" ]; then
        echo "ERROR: Samba AD is already deployed. Use 'force-deploy' option to re-provision."
        return 1
    elif [ "$deployed" = "true" ] && [ "$force_deploy" = "true" ]; then
        echo "Warning: Forcing re-provision of Samba AD. Existing configuration will be overwritten."
        reset_to_default
    fi

    backup_config /etc/samba/smb.conf "original"
    backup_config /etc/krb5.conf "original"
    backup_config /etc/resolv.conf "original"

    if [ -n "$domain_netbios_name" ]; then
        args+=(--domain "$domain_netbios_name")
    else
        domain_netbios_name="${domain_realm%%.*}"
        args+=(--domain "$domain_netbios_name")
    fi

    set_hostname "$domain_realm" "$hostname" || retval=1
    samba-tool domain provision "${args[@]}" || retval=1

    if [ $retval -eq 0 ]; then
        edit_krb5_conf "$domain_realm" || {
            echo "ERROR: Failed to configure /etc/krb5.conf after provision."
            retval=1
        }

        if [ $retval -eq 0 ]; then
            if is_networkmanager_managed; then
                setup_networkmanager_dns "$domain_realm"
            fi
            if is_systemd_networkd_managed; then
                setup_systemd_networkd_dns "$domain_realm"
            fi
            edit_resolv_conf "$domain_realm" || {
                echo "ERROR: Failed to configure /etc/resolv.conf after provision."
                retval=1
            }
            edit_resolvconf_conf || {
                echo "ERROR: Failed to configure /etc/resolvconf.conf after provision."
                retval=1
            }
        fi

        if [ "$dns_backend" = "BIND9_DLZ" ] && [ $retval -eq 0 ]; then
            prepare_bind "$input_json" "$forwarders" || {
                echo "Failed to configure BIND9_DLZ backend."
                retval=1
            }
        fi

        if [ $retval -eq 0 ]; then
            echo "Samba AD successfully provisioned and system has been configured."
            unsensetive_json="$(remove_sensitive_data "$input_json")"
            echo "$unsensetive_json" >/var/lib/alterator/service/samba-ad/status.json
        else
            echo "WARNING: Post-provision configuration steps failed. The domain provisioned, but the system may require manual configuration checks."
        fi
    else
        echo "ERROR: Failed to provision domain."
        reset_to_default
    fi

    return $retval
}

dc_join() {
    local input_json="$1"
    local unsensitive_json=
    local args=()
    local global_keys=
    local dns_keys=
    local dns_backend=
    local forwarders=
    local join_keys=
    local value=
    local admin_password=
    local admin_login=
    local dc_in_domain_ip=
    local server_role=
    local domain_realm=
    local domain_netbios_name=
    local upper_domain_netbios_name=
    local hostname=
    local force_deploy=false
    local retval=0

    global_keys=$(echo "$input_json" | jq -r 'keys[]')

    for key in $global_keys; do
        case "$key" in
            backendStore)
                value=$(get_object_key "$input_json" ".backendStore")
                if [ "$value" = "mdb" ]; then
                    args+=(--backend-store mdb)
                   backend_store_size=$(echo "$input_json" | jq -r '.backendStore.mdb.backendStoreSize')
                    if [ -n "$backend_store_size" ]; then
                        args+=(--backend-store-size "${backend_store_size}Gb")
                    fi
                fi
                ;;
            dnsBackend)
                value=$(get_object_key "$input_json" ".dnsBackend")
                [ -n "$value" ] && args+=(--dns-backend "$value")
                ;;
            functionalLevel)
                value=$(get_object_key "$input_json" ".functionalLevel")
                if [ "$value" = "2016" ]; then
                    args+=(--option="ad dc functional level=2016")
                fi
                ;;
            netBiosName)
                value=$(get_json_value "$input_json" "$key" ".")
                domain_netbios_name=$(to_lower "$value")
                ;;
            realm)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && args+=(--realm "$value")
                domain_realm=$(to_lower "$value")
                ;;
            siteName)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && args+=(--site "$value")
                ;;
            force_deploy)
                force_deploy=$(get_json_value "$input_json" "$key" ".")
                ;;
            hostNetBiosName)
                value=$(get_json_value "$input_json" "$key" ".")
                [ -n "$value" ] && hostname="$value"
                ;;
            *)
                false
                ;;
        esac
    done

    dns_keys=$(echo "$input_json" | jq -r '.dnsSettings | keys[]')
    for dns_key in $dns_keys; do
        case "$dns_key" in
            dnsBackend)
                value="$(get_object_key "$input_json" ".dnsSettings.dnsBackend")"
                dns_backend=$(echo "$value" | tr -d '[]\n ')
                [ -n "$value" ] && args+=(--dns-backend "$value")
                ;;
            forwarders)
                value="$(get_json_value "$input_json" "$dns_key" ".dnsSettings")"
                forwarders=$(echo "$value" | tr -d '[]"\n ')
                if [ -n "$value" ] && [ "$dns_backend" = "SAMBA_INTERNAL" ]; then
                    args+=("--option=dns forwarder=$value")
                fi
                ;;
            *)
                false
                ;;
        esac
    done

    join_keys=$(echo "$input_json" | jq -r '.mode.join | keys[]')
    for join_key in $join_keys; do
        case "$join_key" in
            serverRole)
                value=$(get_object_key "$input_json" ".mode.join.serverRole")
                [ -n "$value" ] && server_role="$value"
                ;;
            adminLogin)
                value="$(get_json_value "$input_json" "$join_key" ".mode.join")"
                [ -n "$value" ] && admin_login="$value"
                ;;
            ipAddressDc)
                value="$(get_json_value "$input_json" "$join_key" ".mode.join")"
                [ -n "$value" ] && dc_in_domain_ip="$value"
                ;;
            adminPassword)
                value="$(get_json_value "$input_json" "$join_key" ".mode.join")"
                [ -n "$value" ] && admin_password="$value"
                ;;
            *)
        esac
    done

    if [ -z "$admin_password" ] || [ -z "$admin_login" ] || [ -z "$dc_in_domain_ip" ]; then
        echo "ERROR: 'Admin password', 'Admin Login' and "DC IPv4 address" is required for joining a domain."
        retval=1
    else
        deployed="$(get_deployed)"
        if [ "$deployed" = "true" ] && [ "$force_deploy" != "true" ]; then
            echo "ERROR: Samba AD is already deployed. Use 'force_deploy' option to re-provision."
            return 1
        elif [ "$deployed" = "true" ] && [ "$force_deploy" = "true" ]; then
            echo "Warning: Forcing re-provision of Samba AD. Existing configuration will be overwritten."
            reset_to_default
        fi
        backup_config /etc/samba/smb.conf "original"
        backup_config /etc/krb5.conf "original"
        backup_config /etc/resolv.conf "original"

        prepare_resolv_conf_to_join "$dc_in_domain_ip" "$domain_realm"

        set_hostname "$domain_realm" "$hostname" || retval=1

        if [ -n "$domain_netbios_name" ]; then
            true
        else
            domain_netbios_name="${domain_realm%%.*}"
        fi
        upper_domain_netbios_name=$(upper "$domain_netbios_name")
        echo "$admin_password" | samba-tool domain join "$domain_realm" "$server_role" "${args[@]}" -U"$admin_login"@"$upper_domain_netbios_name" || retval=1

        if [ $retval -eq 0 ]; then
            edit_krb5_conf "$domain_realm" || {
                echo "ERROR: Failed to configure /etc/krb5.conf after join."
                retval=1
            }

            if [ $retval -eq 0 ]; then
                if is_networkmanager_managed; then
                    setup_networkmanager_dns "$domain_realm"
                fi
                if is_systemd_networkd_managed; then
                    setup_systemd_networkd_dns "$domain_realm"
                fi
                edit_resolv_conf "$domain_realm" || {
                    echo "ERROR: Failed to configure /etc/resolv.conf after provision."
                    retval=1
                }
                edit_resolvconf_conf || {
                    echo "ERROR: Failed to configure /etc/resolvconf.conf after provision."
                    retval=1
                }
            fi

            if [ "$dns_backend" = "BIND9_DLZ" ] && [ "$retval" -eq 0 ]; then
                prepare_bind "$input_json" "$forwarders" || { 
                    echo "Failed to configure BIND9_DLZ backend."
                    retval=1
                }
            fi

            if [ "$retval" -eq 0 ]; then
                echo "Samba AD successfully joined domain '$domain_realm' and system has been configured."
                unsensitive_json="$(remove_sensitive_data "$input_json")"
                echo "Saving deployment configuration to /var/lib/alterator/service/samba-ad/status.json"
                echo "$unsensitive_json" >/var/lib/alterator/service/samba-ad/status.json
            else
                echo "WARNING: Post-join configuration steps failed. The DC joined the domain, but the system may require manual configuration checks."
            fi
        else
            echo "ERROR: Failed to join domain."
            reset_to_default
        fi
    fi

    return $retval
}

undeploy_service() {
    local input_json=
    local admin_password=
    local admin_login=
    local keys=
    local value=
    local out=
    local args=()
    local save_current_config=false
    local reset_to_default_flag=false
    local force_demote=false
    local retval=0
    input_json="$(read_stdin)"

    keys=$(echo "$input_json" | jq -r 'keys[]')
    for key in $keys; do
        case "$key" in
            adminPassword)
                admin_password=$(get_json_value "$input_json" "$key" ".")
                ;;
            adminLogin)
                admin_login=$(get_json_value "$input_json" "$key" ".")
                ;;
            saveCurrentConfig)
                save_current_config=$(get_json_value "$input_json" "$key" ".")
                ;;
            resetToDefaults)
                reset_to_default_flag=$(get_json_value "$input_json" "$key" ".")
                ;;
            forceUndeploy)
                force_demote=$(get_json_value "$input_json" "$key" ".")
                ;;
            *)
                false
                ;;
        esac
    done

    if [ -z "$admin_password" ] || [ -z "$admin_login" ]; then
        echo "ERROR: 'Admin password' and 'Admin Login' is required for demoting a domain controller."
    else
        out="$(echo "$admin_password" | samba-tool domain demote "${args[@]}" -U"$admin_login")" || retval=1
        if [ $retval -eq 0 ]; then
            echo "$out"
            rm -f /var/lib/alterator/service/samba-ad/status.json
            echo "Domain controller successfully demoted."
            if systemctl is-active --quiet samba.service; then
                stop_service || retval=1
            fi
            if [ "$save_current_config" = "true" ]; then
                backup_config /etc/samba/smb.conf "before_undeploy"
                backup_config /etc/krb5.conf "before_undeploy"
                backup_config /etc/resolv.conf "before_undeploy"
            fi
            if [ "$reset_to_default_flag" = "true" ]; then
                reset_to_default
                echo "System reset to default configuration."
            fi
        else
            if [ "$force_demote" = "true" ]; then
                echo "Warning: Forcing demotion despite errors."
                echo "This error will be ignored."
                echo "$out"
                if systemctl is-active --quiet samba.service; then
                    stop_service || retval=1
                fi
                if [ "$save_current_config" = "true" ]; then
                    backup_config /etc/samba/smb.conf "before_undeploy"
                    backup_config /etc/krb5.conf "before_undeploy"
                    backup_config /etc/resolv.conf "before_undeploy"
                fi
                reset_to_default
                rm -f /var/lib/alterator/service/samba-ad/status.json
                echo "Domain controller demoted and system reset to default configuration."
                retval=0
            else
                if grep -q "The attempted logon is invalid. This is either due to a bad username or authentication information" <<<"$out"; then
                    echo "ERROR: Invalid credentials provided for demotion."
                    echo "ERROR: Failed to demote domain controller."
                else
                    echo "$out"
                    echo "ERROR: Failed to demote domain controller."
                fi
            fi
        fi
    fi

    return $retval
}

configure_service() {
    local input_json=
    local new_dns_backend=
    local retval=0
    local current_dns_backend=
    local forwarders=
    local current_domain_level=
    local new_domain_level=
    local new_forest_level=
    current_dns_backend="$(get_dns_backend)"
    current_domain_level="$(get_functional_level "Domain")"
    input_json="$(read_stdin)"

    new_dns_backend="$(get_object_key "$input_json" ".dnsSettings.dnsBackend")"
    forwarders="$(get_json_value "$input_json" "forwarders" ".dnsSettings")"
    forwarders=$(echo "$forwarders" | tr -d '[]"\n ')
    new_domain_level="$(get_object_key "$input_json" ".configureFunctionalLevel")"
    new_forest_level="$new_domain_level"    ##TODO {arzdez} "Currently we set the same level for domain and forest, but in the future we can add separate parameter for forest functional level"

    if [ "$new_dns_backend" != "$current_dns_backend" ]; then
        backup_config /etc/samba/smb.conf "before_dns_backend_change" true
        echo "Changing DNS backend from $current_dns_backend to $new_dns_backend."
        change_dns_backend "$new_dns_backend" "$current_dns_backend" || retval=1
        if [ $retval -eq 0 ]; then
            echo "DNS backend changed successfully."
        else
            echo "ERROR: Failed to change DNS backend."
            restore_config /etc/samba/smb.conf "before_dns_backend_change" true
        fi
    fi

    if [ "$new_dns_backend" = "BIND9_DLZ" ]; then
        backup_config /etc/bind/options.conf "before_dns_backend_change" true
        backup_config /etc/bind/named.conf "before_dns_backend_change" true

        prepare_bind "$input_json" "$forwarders" || retval=1
        if [ $retval -ne 0 ]; then
            echo "ERROR: Failed to configure BIND9_DLZ backend."
            restore_config /etc/bind/options.conf "before_dns_backend_change" true
            restore_config /etc/bind/named.conf "before_dns_backend_change" true
        else
            echo "BIND9_DLZ backend configured successfully."
        fi
    else
        edit_forwarders_samba "$forwarders" || retval=1
    fi

    if [ "$new_domain_level" != "$current_domain_level" ]; then
        echo "Changing domain functional level from $current_domain_level to $new_domain_level."
        backup_config /etc/samba/smb.conf "before_domain_level_raise" true
        raise_dc_level "$new_domain_level" || {
            echo "ERROR: Failed to change DC functional level."
            retval=1
        }
        if [ $retval -eq 0 ]; then
            raise_domain_level "$new_domain_level" || {
            echo "ERROR: Failed to change domain functional level."
            retval=1
            }
        fi
        if [ $retval -eq 0 ]; then
            raise_forest_level "$new_forest_level" || {
                echo "ERROR: Failed to change forest functional level."
                retval=1
            }
        fi
    fi

    if [ $retval -eq 0 ]; then
        echo "Configuration changes applied successfully."
    else
        echo "ERROR: Failed to apply configuration changes."
        restore_config /etc/samba/smb.conf "before_domain_level_raise" true
    fi

    start_service || retval=1

    return $retval
}

backup_service() {
    # TODO {arzdez} "Implement backup"
    echo "Backup comming soon"
    return 0
}

restore_service() {
    # TODO {arzdez} "Implement restore"
    echo "Restore comming soon"
    return 0
}

start_service() {
    local retval=0
    local dns_backend=
    local samba_fail=0
    local samba_out=
    local bind_fail=0
    local bind_out=
    dns_backend="$(get_dns_backend)"

    disable_nmb_unit || true
    disable_smb_unit || true

    if [ "$dns_backend" = "BIND9_DLZ" ]; then
        if systemctl is-active --quiet bind.service; then
            bind_out="$(systemctl restart bind.service)" || bind_fail=1
            enable_unit "bind.service"
        else
            bind_out="$(systemctl start bind.service)" || bind_fail=1
            enable_unit "bind.service"
        fi
    else
        if systemctl is-active --quiet bind.service; then
            bind_out="$(systemctl disable --now bind.service)" || true
        fi
    fi

    if  systemctl is-active --quiet samba.service; then
        samba_out="$(systemctl restart samba.service)" || samba_fail=1
        enable_unit "samba.service"
    else
        samba_out="$(systemctl start samba.service)" || samba_fail=1
        enable_unit "samba.service"
    fi

    if [ $samba_fail -eq 1 ]; then
        echo -n "$samba_out"
        retval=1
    elif [ $bind_fail -eq 1 ]; then
        echo -n "$bind_out"
        retval=1
    else
        echo "Success"
    fi

    return $retval
}

stop_service() {
    local retval=0
    dns_backend="$(get_dns_backend)"

    systemctl disable --now samba.service 
    if [ "$dns_backend" = "BIND9_DLZ" ]; then
        systemctl disable --now bind.service
    fi

    return $retval
}

case "$MODE" in
    provision)
        deploy_service || GLOBAL_EXIT=1
        ;;
    demote)
        undeploy_service || GLOBAL_EXIT=1
        ;;
    status)
        update_status
        ;;
    backup)
        backup_service || GLOBAL_EXIT=1
        ;;
    restore)
        restore_service || GLOBAL_EXIT=1
        ;;
    start)
        start_service || GLOBAL_EXIT=1
        ;;
    stop)
        stop_service || GLOBAL_EXIT=1
        ;;
    configure)
        configure_service || GLOBAL_EXIT=1
        ;;
esac

exit $GLOBAL_EXIT
