#!/usr/bin/bash
# lkdcctl - Unified localkdc management tool
set -euo pipefail

# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

error()
{
    if [[ -t 2 ]]; then
        printf "⛔ \033[0;31m%s\033[0m\n" "$*" >&2
    else
        printf "%s\n" "$*" >&2
    fi
}

info()
{
    if [[ -t 1 ]]; then
        printf "▶️  \033[0;32m%s\033[0m\n" "$*"
    else
        printf "%s\n" "$*"
    fi
}

localkdc_get_realm()
{
    local _realm

    if [[ ! -r "/etc/localkdc/realm" ]]; then
        error "/etc/localkdc/realm doesn't exist." \
              "Please run lkdcctl setup"
        return 1
    fi
    _realm=$(<"/etc/localkdc/realm")
    if [[ -z "${_realm}" ]]; then
        error "Failed to get REALM from /etc/localkdc/realm"
        return 1
    fi

    printf "%s\n" "${_realm}"
}

# ---------------------------------------------------------------------------
# Subcommand: setup
# ---------------------------------------------------------------------------

cmd_setup()
{
    if [[ "${1:-}" = "--help" ]]; then
        usage_setup
        return 0
    fi

    # --- Variables ---

    # uuidgen --namespace @dns --sha1 --name "kirmes-localkdc"
    LOCALKDC_APP_UUID="35b20ad5-c75c-5eaa-9fcd-8f3cb8e33185"

    LOCALKDC_HOSTNAME="$(hostname --short | tr '[:upper:]' '[:lower:]')"

    # Add the hostname and mdns name to the list of fqdns we want to add
    LOCALKDC_HOSTNAME_ALL="${LOCALKDC_HOSTNAME} ${LOCALKDC_HOSTNAME}.local localhost $(hostname --all-fqdns)"
    # Remove duplicates from space-separated list
    LOCALKDC_HOSTNAME_FQDNS="$(printf "%s\n" "${LOCALKDC_HOSTNAME_ALL}" | \
        awk -v RS=' ' '!seen[$0]++')"

    SYSCONF_DIR="/etc/localkdc"
    LOCALSTATE_DIR="/var/kerberos/localkdc"
    DATA_DIR="/usr/share/localkdc"
    KRB5_CONFIG_DIR="/etc/krb5.conf.d"

    LOCAL_GETCERT="$(command -v local-getcert || true)"
    KDB5_UTIL="$(command -v kdb5_util || true)"
    KADMIN_LOCAL="$(command -v kadmin.local || true)"
    SYSTEMD_ID128="$(command -v systemd-id128 || true)"
    SMBD="$(command -v smbd 2>/dev/null || true)"
    SSSD="$(command -v sssd 2>/dev/null || true)"

    LOCALKDC_MACHINE_APP_UUID="$("${SYSTEMD_ID128}" \
        --value \
        --uuid \
        --app-specific="${LOCALKDC_APP_UUID}" \
        machine-id | \
        tr '[:lower:]' '[:upper:]')"
    if [[ -z "${LOCALKDC_MACHINE_APP_UUID}" ]]; then
        error "Failed to generate machine-app UUID"
        exit 1
    fi

    LOCALKDC_REALM="LOCALKDC.${LOCALKDC_MACHINE_APP_UUID}"
    LOCALKDC_FQDN="${LOCALKDC_HOSTNAME}.localkdc.site"

    # Generate a random master password
    # Temporarily disable pipefail to avoid SIGPIPE from head
    set +o pipefail
    MASTER_PASSWORD="$(tr -cd '[:alnum:]' < /dev/urandom | fold -w256 | head -n1)"
    set -o pipefail
    if [[ -z "${MASTER_PASSWORD}" ]]; then
        error "Failed to generate master password"
        exit 1
    fi

    # --- Helper functions ---

    check_requirements()
    {
        if [[ -z "${LOCALKDC_HOSTNAME}" ]]; then
            error "Failed to find a hostname"
            return 1
        fi

        if [[ -z "${LOCAL_GETCERT}" ]]; then
            error "Failed to find local-getcert," \
                  "please install certmonger"
            return 1
        fi

        if [[ -z "${KDB5_UTIL}" ]]; then
            error "Failed to find kdb5_util," \
                  "please install krb5-server"
            return 1
        fi

        if [[ -z "${KADMIN_LOCAL}" ]]; then
            error "Failed to find kadmin.local," \
                  "please install krb5-server"
            return 1
        fi

        if [[ -z "${SYSTEMD_ID128}" ]]; then
            error "Failed to find systemd-id128, please install systemd"
            return 1
        fi
    }

    install_kdc_conf()
    {
        if ! mkdir -p "${LOCALSTATE_DIR}"; then
            error "Failed to create directory ${LOCALSTATE_DIR}"
            return 1
        fi

        if ! sed -e "s/%REALM%/${LOCALKDC_REALM}/" \
            "${DATA_DIR}/templates/kdc.conf" > "${LOCALSTATE_DIR}/kdc.conf";
        then
            error "Failed to create ${LOCALSTATE_DIR}/kdc.conf"
            return 1
        fi

        if ! sed -e "s/%REALM%/${LOCALKDC_REALM}/" \
            "${DATA_DIR}/templates/kadmin.conf" \
            >"${LOCALSTATE_DIR}/kadmin.conf";
        then
            error "Failed to create ${LOCALSTATE_DIR}/kadmin.conf"
            return 1
        fi
    }

    install_krb5_conf()
    {
        if ! mkdir -p "${KRB5_CONFIG_DIR}"; then
            error "Failed to create directory ${KRB5_CONFIG_DIR}"
            return 1
        fi

        if ! sed -e "s/%REALM%/${LOCALKDC_REALM}/" \
              "${DATA_DIR}/templates/krb5.conf.hostname" \
              > "${KRB5_CONFIG_DIR}/localkdc"; then
            error "Failed to create ${KRB5_CONFIG_DIR}/localkdc"
            return 1
        fi
    }

    configure_samba()
    {
        if [[ -z "${SMBD}" ]]; then
            info "localkdc: Samba not installed, skipping Samba configuration"
            return 0
        fi

        cat > /etc/samba/localkdc.conf << EOF
localkdc realm = ${LOCALKDC_REALM}
kerberos method = secrets and keytabs
keytab file list = /etc/samba/localkdc.keytab
EOF
    }

    configure_localkdc_pam_auth()
    {
        # Write the RADIUS shared secret to the file referenced by kdc.conf's
        # [otp] localkdc-pam section.  Both the KDC OTP plugin and
        # localkdc-pam-auth read the secret from this file so that the two
        # sides always use the same value.
        if ! printf "%s\n" "${LOCALKDC_REALM}" | \
                install -m 0600 /dev/stdin "${LOCALSTATE_DIR}/pam-radius.secret"; then
            error "Failed to install ${LOCALSTATE_DIR}/pam-radius.secret"
            return 1
        fi

        # Expose the path (not the secret value) via the environment file read
        # by the localkdc-pam-auth service.  The binary reads the secret from
        # the file at startup.
        if ! install -d -m 0755 /etc/localkdc; then
            error "Failed to create /etc/localkdc"
            return 1
        fi
        if ! printf "RADIUS_SECRET=%s/pam-radius.secret\n" "${LOCALSTATE_DIR}" \
                > /etc/localkdc/radius.env; then
            error "Failed to write /etc/localkdc/radius.env"
            return 1
        fi

        if ! install -m 0644 /etc/pki/ca-trust/source/anchors/localkdc-ca.crt \
                        /etc/localkdc/localkdc-ca.crt; then
            error "Failed to install CA certificate to /etc/localkdc/localkdc-ca.crt"
            return 1
        fi
    }

    configure_sssd()
    {
        # sssd.conf must be mode 0600 or SSSD will refuse to start.
        # Only write it when not already present — the admin may have customised it.
        if [[ ! -f /etc/sssd/sssd.conf ]]; then
            if ! install -d -m 0711 /etc/sssd; then
                error "Failed to create /etc/sssd"
                return 1
            fi
            if ! install -m 0600 "${DATA_DIR}/templates/sssd.conf" \
                    /etc/sssd/sssd.conf; then
                error "Failed to install /etc/sssd/sssd.conf"
                return 1
            fi
        else
            info "localkdc: /etc/sssd/sssd.conf already exists, not overwriting"
        fi

        # Install the localkdc domain snippet with the realm substituted.
        if ! install -d -m 0711 /etc/sssd/conf.d; then
            error "Failed to create /etc/sssd/conf.d"
            return 1
        fi
        if ! sed -e "s/%REALM%/${LOCALKDC_REALM}/" \
                "${DATA_DIR}/templates/sssd-localkdc.conf" | \
                install -m 0600 /dev/stdin /etc/sssd/conf.d/localkdc.conf; then
            error "Failed to install /etc/sssd/conf.d/localkdc.conf"
            return 1
        fi
    }

    configure_localkdc()
    {
        local _kadmin_input_file=""
        local _master_pass_file
        local _cert_requested=0
        local _db_created=0
        local _realm_created=0

        export KRB5_KDC_PROFILE="${LOCALSTATE_DIR}/kadmin.conf"

        cleanup_on_error() {
            if [[ "${_cert_requested}" -eq 1 ]]; then
                printf "Cleaning up certmonger certificate...\n" >&2
                getcert stop-tracking \
                    -k "${LOCALSTATE_DIR}/kdc.key" \
                    -f "${LOCALSTATE_DIR}/kdc.crt" 2>/dev/null || true
            fi
            if [[ "${_db_created}" -eq 1 ]]; then
                printf "Cleaning up Kerberos database...\n" >&2
                rm -rf "${LOCALSTATE_DIR}/db" 2>/dev/null || true
            fi
            if [[ "${_realm_created}" -eq 1 ]]; then
                printf "Cleaning up realm file...\n" >&2
                rm -f "${SYSCONF_DIR}/realm" 2>/dev/null || true
            fi
            if [[ -n "${_kadmin_input_file}" ]]; then
                rm -f "${_kadmin_input_file}" 2>/dev/null || true
            fi
        }

        trap cleanup_on_error ERR

        if ! install -d -m 0755 "${SYSCONF_DIR}"; then
            error "Failed to create directory ${SYSCONF_DIR}"
            return 1
        fi

        if ! printf "%s\n" "${LOCALKDC_REALM}" | \
             install -m 0644 /dev/stdin "${SYSCONF_DIR}/realm"; then
            error "Failed to install realm file"
            return 1
        fi
        _realm_created=1

        # Prepare Anonymous PKINIT configuration for the realm
        if ! "${LOCAL_GETCERT}" request \
            -K "krbtgt/${LOCALKDC_REALM}@${LOCALKDC_REALM}" \
            -N "cn=${LOCALKDC_FQDN}" \
            -F /etc/pki/ca-trust/source/anchors/localkdc-ca.crt \
            -k "${LOCALSTATE_DIR}/kdc.key" \
            -f "${LOCALSTATE_DIR}/kdc.crt" \
            -U{id-kp-serverAuth,id-pkinit-KPKdc} \
            -u{digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment}
        then
            error "${LOCAL_GETCERT}: Failed to create pkinit certificates"
            return 1
        fi
        _cert_requested=1

        _master_pass_file="${LOCALSTATE_DIR}/.master.pass"
        if ! printf "%s\n" "${MASTER_PASSWORD}" | \
             install -m 0600 /dev/stdin "${_master_pass_file}"; then
            error "Failed to install master password file"
            return 1
        fi

        # Create krb5 database
        rm -rf "${LOCALSTATE_DIR}/db/" 2>/dev/null || true
        if ! mkdir -p "${LOCALSTATE_DIR}/db/"; then
            error "Failed to create database directory"
            return 1
        fi

        if ! cat "${_master_pass_file}" "${_master_pass_file}" | \
             "${KDB5_UTIL}" -r "${LOCALKDC_REALM}" create -s; then
            error "Failed to create Kerberos database"
            return 1
        fi
        _db_created=1

        _kadmin_input_file="$(mktemp)"
        if [[ -z "${_kadmin_input_file}" ]]; then
            error "Failed to create temporary file"
            return 1
        fi

        cat > "${_kadmin_input_file}" <<EOF
addprinc -randkey WELLKNOWN/ANONYMOUS
addprinc -randkey "host/${LOCALKDC_FQDN}"
addprinc -randkey "cifs/${LOCALKDC_FQDN}"
addprinc -randkey "HTTP/${LOCALKDC_FQDN}"

ktadd -norandkey host/${LOCALKDC_FQDN}

ktadd -k /etc/samba/localkdc.keytab -norandkey host/${LOCALKDC_FQDN}
ktadd -k /etc/samba/localkdc.keytab -norandkey cifs/${LOCALKDC_FQDN}

ktadd -k /etc/localkdc-http.keytab -norandkey host/${LOCALKDC_FQDN}
ktadd -k /etc/localkdc-http.keytab -norandkey HTTP/${LOCALKDC_FQDN}

EOF

        # shellcheck disable=SC2086 # Word splitting of space/newline-separated list is intentional
        for h in ${LOCALKDC_HOSTNAME_FQDNS}; do
            if [[ "${h}" = "${LOCALKDC_FQDN}" ]]; then
                continue
            fi

            cat >> "${_kadmin_input_file}" <<EOF
add_alias host/${h} host/${LOCALKDC_FQDN}@${LOCALKDC_REALM}
add_alias cifs/${h} cifs/${LOCALKDC_FQDN}@${LOCALKDC_REALM}
add_alias HTTP/${h} HTTP/${LOCALKDC_FQDN}@${LOCALKDC_REALM}

ktadd -norandkey host/${h}
ktadd -k /etc/samba/localkdc.keytab -norandkey host/${h}
ktadd -k /etc/samba/localkdc.keytab -norandkey cifs/${h}
ktadd -k /etc/localkdc-http.keytab -norandkey host/${h}
ktadd -k /etc/localkdc-http.keytab -norandkey HTTP/${h}

EOF
        done

        # Create keytab files
        if ! "${KADMIN_LOCAL}" -r "${LOCALKDC_REALM}" \
             < "${_kadmin_input_file}"; then
            error "${KADMIN_LOCAL}: Failed to create database entries"
            return 1
        fi

        printf "\n"

        rm -f "${_kadmin_input_file}"

        unset KRB5_KDC_PROFILE

        # Success — disable cleanup
        _cert_requested=0
        _db_created=0
        _realm_created=0
        _kadmin_input_file=""
        trap - ERR
    }

    # --- Run setup steps ---

    info "Checking requirements..."
    check_requirements || exit 1

    info "Installing KDC configuration..."
    install_kdc_conf || exit 1

    info "Installing Kerberos 5 configuration..."
    install_krb5_conf || exit 1

    info "Configuring LocalKDC..."
    configure_localkdc || exit 1

    info "Configuring PAM authentication service..."
    configure_localkdc_pam_auth || exit 1

    info "Configuring Samba..."
    configure_samba || exit 1

    info "Configuring SSSD..."
    configure_sssd || exit 1

    systemctl enable --now localkdc.socket
    systemctl enable --now localkdc-pam-auth.socket

    if [[ -n "${SSSD}" ]]; then
        authselect select sssd ||:
        authselect is-feature-enabled with-gssapi || \
            authselect enable-feature with-gssapi
        systemctl enable --now sssd.service ||:
    fi

    info "Successfully set up localkdc"
}

# ---------------------------------------------------------------------------
# Subcommand: kadmin
# ---------------------------------------------------------------------------

cmd_kadmin()
{
    if [[ "${1:-}" = "--help" ]]; then
        usage_kadmin
        return 0
    fi

    local _kadmin_local
    local _realm
    local _kdc_profile

    _kadmin_local="$(command -v kadmin.local || true)"
    if [[ -z "${_kadmin_local}" ]]; then
        error "Failed to find kadmin.local, please install krb5-server"
        exit 1
    fi

    if ! _realm=$(localkdc_get_realm); then
        exit 1
    fi

    _kdc_profile="/var/kerberos/localkdc/kdc.conf"
    if [[ ! -f "${_kdc_profile}" ]]; then
        error "KDC config file not found: ${_kdc_profile}"
        exit 1
    fi

    KRB5_KDC_PROFILE="${_kdc_profile}"
    export KRB5_KDC_PROFILE

    exec "${_kadmin_local}" -r "${_realm}" "$@"
}

# ---------------------------------------------------------------------------
# Subcommand: kinit
# ---------------------------------------------------------------------------

cmd_kinit()
{
    if [[ "${1:-}" = "--help" ]]; then
        usage_kinit
        return 0
    fi

    if [[ $# -lt 1 ]]; then
        usage_kinit
        exit 1
    fi

    local _user="${1}"
    local _realm
    local _kinit

    if ! _realm=$(localkdc_get_realm); then
        exit 1
    fi

    _kinit="$(command -v kinit || true)"
    if [[ -z "${_kinit}" ]]; then
        error "Failed to find kinit, please install krb5-workstation"
        exit 1
    fi

    exec "${_kinit}" "${_user}@${_realm}"
}

# ---------------------------------------------------------------------------
# Subcommand: useradd
# ---------------------------------------------------------------------------

cmd_useradd()
{
    if [[ "${1:-}" = "--help" ]]; then
        usage_useradd
        return 0
    fi

    if [[ $# -lt 1 ]]; then
        usage_useradd
        exit 1
    fi

    local _user="${1}"
    local _userdbctl
    local _out
    local _normalized_user
    local _user_entry
    local _enctypes
    local _options

    _userdbctl="$(command -v userdbctl || true)"
    if [[ -z "${_userdbctl}" ]]; then
        error "Can't find userdbctl, please install systemd"
        exit 1
    fi

    if ! _out="$("${_userdbctl}" --output=classic user "${_user}")"; then
        error "Can't find user [${_user}] on this system!"
        exit 1
    fi

    if ! _normalized_user=$(awk -F ':' '{ print $1 }' <<< "${_out}"); then
        error "Failed to parse user output"
        exit 1
    fi

    if [[ -z "${_normalized_user}" ]]; then
        error "Can't parse normalized user:"
        error "${_out}"
        exit 1
    fi

    _user_entry="userdb:${_normalized_user}"

    _enctypes="aes256-cts-hmac-sha384-192:special, \
aes128-cts-hmac-sha256-128:special, \
aes256-cts-hmac-sha1-96:special, \
aes128-cts-hmac-sha1-96:special"
    _options=("-allow_tix" "+requires_preauth")

    info "Adding Kerberos principal for user: ${_normalized_user}"
    cmd_kadmin addprinc -e "${_enctypes}" \
         "${_options[@]}" "${_user_entry}"
}

# ---------------------------------------------------------------------------
# Help / usage
# ---------------------------------------------------------------------------

usage()
{
    printf "Usage: lkdcctl <command> [options]\n"
    printf "\n"
    printf "Commands:\n"
    printf "    setup       Initial localkdc setup (KDB, certificates, keytabs, SSSD, PAM auth)\n"
    printf "    kadmin      Run kadmin.local against the localkdc database\n"
    printf "    kinit       Acquire Kerberos ticket for a local user\n"
    printf "    useradd     Add a system user to the localkdc Kerberos database\n"
    printf "\n"
    printf "Options:\n"
    printf "    -h, --help  Show this help message\n"
    printf "\n"
    printf "Examples:\n"
    printf "    lkdcctl setup\n"
    printf "    lkdcctl kadmin listprincs\n"
    printf "    lkdcctl kinit alice\n"
    printf "    lkdcctl useradd bob\n"
}

usage_setup()
{
    printf "Usage: lkdcctl setup\n"
    printf "\n"
    printf "Performs initial localkdc setup: generates realm UUID, requests PKINIT\n"
    printf "certificates via certmonger, creates KDB, adds service principals and\n"
    printf "keytabs for host/cifs/HTTP services, configures RADIUS secret for\n"
    printf "localkdc-pam-auth, configures SSSD, and enables required systemd units.\n"
    printf "\n"
    printf "Examples:\n"
    printf "    lkdcctl setup\n"
}

usage_kadmin()
{
    printf "Usage: lkdcctl kadmin [kadmin.local options]\n"
    printf "\n"
    printf "Runs kadmin.local with KRB5_KDC_PROFILE set to the localkdc state directory.\n"
    printf "All arguments are passed through to kadmin.local.\n"
    printf "\n"
    printf "Examples:\n"
    printf "    lkdcctl kadmin listprincs\n"
    printf "    lkdcctl kadmin addprinc -randkey host/myhost.example.com\n"
}

usage_kinit()
{
    printf "Usage: lkdcctl kinit USERNAME\n"
    printf "\n"
    printf "Acquires a Kerberos ticket for USERNAME in the localkdc realm.\n"
    printf "\n"
    printf "Examples:\n"
    printf "    lkdcctl kinit alice\n"
}

usage_useradd()
{
    printf "Usage: lkdcctl useradd USERNAME\n"
    printf "\n"
    printf "Adds a system user to the localkdc Kerberos database as userdb:USERNAME.\n"
    printf "The user must already exist on the system (checked via userdbctl).\n"
    printf "\n"
    printf "Examples:\n"
    printf "    lkdcctl useradd bob\n"
}

# ---------------------------------------------------------------------------
# Main dispatch
# ---------------------------------------------------------------------------

case "${1:-}" in
    setup)
        shift
        cmd_setup "$@"
        ;;
    kadmin)
        shift
        cmd_kadmin "$@"
        ;;
    kinit)
        shift
        cmd_kinit "$@"
        ;;
    useradd)
        shift
        cmd_useradd "$@"
        ;;
    help | -h | --help)
        usage
        ;;
    *)
        usage
        exit 1
        ;;
esac
