#!/usr/bin/env bash
set -Eeuo pipefail

CONFIG_DIR="${LEGO_CONFIG_DIR:-/etc/lego}"
CONFIG_FILE="${LEGO_CONFIG_FILE:-${CONFIG_DIR}/.lego.yml}"
PRE_DIR="${LEGO_PRE_DIR:-${CONFIG_DIR}/pre}"
POST_DIR="${LEGO_POST_DIR:-${CONFIG_DIR}/post}"
DEPLOY_DIR="${LEGO_DEPLOY_DIR:-${CONFIG_DIR}/deploy}"
LOCAL_HOOK_DIR="${LEGO_LOCAL_HOOK_DIR:-${CONFIG_DIR}/hooks}"
MASTER_PRE_HOOK="${LEGO_MASTER_PRE_HOOK:-/usr/libexec/lego/pre-hook}"
MASTER_POST_HOOK="${LEGO_MASTER_POST_HOOK:-/usr/libexec/lego/post-hook}"
MASTER_DEPLOY_HOOK="${LEGO_MASTER_DEPLOY_HOOK:-/usr/libexec/lego/deploy-hook}"
DEFAULT_INTERNAL_ACME_URL="${LEGO_INTERNAL_ACME_URL:-adv-ca.advance2000.com}"
DEFAULT_CONTACT_EMAIL="${DEFAULT_CONTACT_EMAIL:-nocv1@advance2000.com}"
AVAILABLE_PRE_HOOK_DIR="${LEGO_AVAILABLE_PRE_HOOK_DIR:-/usr/share/lego/hooks/pre}"
AVAILABLE_POST_HOOK_DIR="${LEGO_AVAILABLE_POST_HOOK_DIR:-/usr/share/lego/hooks/post}"
AVAILABLE_DEPLOY_HOOK_DIR="${LEGO_AVAILABLE_DEPLOY_HOOK_DIR:-/usr/share/lego/hooks/deploy}"
PERMISSIONS_HELPER="${LEGO_PERMISSIONS_HELPER:-/usr/libexec/lego/enforce-permissions}"
DETECTED_SERVICE_DEFAULT=""

log() { printf '%s\n' "$*"; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

require_root() {
    [[ ${EUID} -eq 0 ]] || die "lego-init must be run as root"
}

trim() {
    local value="$1"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    printf '%s' "${value}"
}

normalize_service() {
    local service
    service="$(trim "$1")"
    [[ -n "${service}" ]] || return 0

    case "${service,,}" in
        none|no|n)
            return 0
            ;;
    esac

    [[ "${service}" == *.service ]] || service="${service}.service"
    [[ "${service}" =~ ^[A-Za-z0-9_.@:-]+\.service$ ]] ||
        die "Invalid systemd service name: ${service}"

    printf '%s\n' "${service}"
}

# Use grep ERE to avoid Bash parser ambiguity around the end anchor.
is_valid_fqdn() {
    local domain="$1"
    [[ ${#domain} -le 253 ]] || return 1
    [[ "${domain}" != *..* ]] || return 1
    grep -Eq '^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$' <<<"${domain}"
}

yaml_quote() {
    local value="$1"
    value="${value//\'/\'\'}"
    printf "'%s'" "${value}"
}

is_valid_email() {
    local address="$1"
    [[ "${address}" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]
}

configure_mail_transport() {
    command -v postconf >/dev/null 2>&1 ||
        die "postconf was not found; install the postfix dependency"
    command -v sendmail >/dev/null 2>&1 ||
        die "sendmail was not found; install the postfix dependency"

    install -d -o root -g lego -m 2750 "${CONFIG_DIR}"

    # Configure Postfix for local submissions and direct MX delivery.
    mkdir -p /etc/postfix
    touch /etc/postfix/main.cf
    postconf -e 'inet_interfaces = loopback-only'
    postconf -e 'relayhost ='
    postconf -e 'myorigin = $myhostname'
    postconf -e 'smtp_tls_security_level = may'

    systemctl enable --now postfix.service
    systemctl restart postfix.service
}

write_service_hooks() {
    local service="$1"
    local hook_name="systemd-${service%.service}"
    local stop_script="${LOCAL_HOOK_DIR}/stop-${hook_name}"
    local start_script="${LOCAL_HOOK_DIR}/start-${hook_name}"

    install -d -o root -g lego -m 2750 "${PRE_DIR}" "${POST_DIR}" "${LOCAL_HOOK_DIR}"

    cat >"${stop_script}" <<EOF_STOP
#!/usr/bin/env bash
set -Eeuo pipefail
systemctl stop $(printf '%q' "${service}")
EOF_STOP

    cat >"${start_script}" <<EOF_START
#!/usr/bin/env bash
set -Eeuo pipefail
systemctl start $(printf '%q' "${service}")
EOF_START

    chmod 0750 "${stop_script}" "${start_script}"
    ln -sfn "${stop_script}" "${PRE_DIR}/20-${hook_name}"
    ln -sfn "${start_script}" "${POST_DIR}/20-${hook_name}"
}

enable_packaged_pre_hook() {
    local name="$1"
    local order="${2:-50}"
    local source="${AVAILABLE_PRE_HOOK_DIR}/${name}"

    [[ -x "${source}" ]] || die "Packaged pre hook not found: ${source}"
    install -d -o root -g lego -m 2750 "${PRE_DIR}"
    ln -sfn "${source}" "${PRE_DIR}/${order}-${name}"
}

enable_packaged_post_hook() {
    local name="$1"
    local order="${2:-50}"
    local source="${AVAILABLE_POST_HOOK_DIR}/${name}"

    [[ -x "${source}" ]] || die "Packaged post hook not found: ${source}"
    install -d -o root -g lego -m 2750 "${POST_DIR}"
    ln -sfn "${source}" "${POST_DIR}/${order}-${name}"
}

enable_packaged_deploy_hook() {
    local name="$1"
    local order="${2:-50}"
    local source="${AVAILABLE_DEPLOY_HOOK_DIR}/${name}"

    [[ -x "${source}" ]] || die "Packaged deploy hook not found: ${source}"
    install -d -o root -g lego -m 2750 "${DEPLOY_DIR}"
    ln -sfn "${source}" "${DEPLOY_DIR}/${order}-${name}"
}

detect_and_configure_integrations() {
    local answer

    # Visualware MyConnection Server normally keeps its HTTPS keystore here.
    # The data directory is a more stable fingerprint than a hostname or unit
    # name, both of which vary between installations.
    if [[ -d /opt/vw/data ]] &&
       [[ -f /opt/vw/data/https.ini || -f /opt/vw/data/example-https.ini ||
          -f /opt/vw/data/certs.jks ]]; then
        printf '\nVisualware MyConnection Server detected.\n'
        enable_packaged_deploy_hook visualware 50
        log "Enabled packaged deploy hook: visualware"

        # Suggest a unit whose definition starts software below /opt/vw.  The
        # operator still sees and confirms the unit at the normal service prompt.
        local unit fragment
        while IFS= read -r unit; do
            unit="${unit%.service}"
            fragment="$(systemctl show -p FragmentPath --value "${unit}.service" 2>/dev/null || true)"
            if [[ "${unit,,}" == *visualware* || "${unit,,}" == *mcs* ]] ||
               { [[ -r "${fragment}" ]] && grep -Fq '/opt/vw' "${fragment}"; }; then
                DETECTED_SERVICE_DEFAULT="${unit}.service"
                break
            fi
        done < <(systemctl list-unit-files --type=service --no-legend 2>/dev/null |
                 awk '{print $1}')
    fi

    if command -v pvenode >/dev/null 2>&1 &&
       systemctl cat pveproxy.service >/dev/null 2>&1; then
        printf '
Proxmox VE detected.
'
        read -r -p "Activate renewed certificates for the Proxmox port 8006 interface? [Y/n]: " answer
        case "${answer,,}" in
            ""|y|yes)
                enable_packaged_deploy_hook proxmox 50
                log "Enabled packaged deploy hook: proxmox"
                ;;
            n|no)
                ;;
            *)
                die "Invalid Proxmox integration selection: ${answer}"
                ;;
        esac
    fi

    if command -v gitlab-ctl >/dev/null 2>&1; then
        printf '
Omnibus GitLab detected.
'
        read -r -p "Stop GitLab before TLS-ALPN-01 and start it afterward? [Y/n]: " answer
        case "${answer,,}" in
            ""|y|yes)
            
                if ! getent group lego >/dev/null 2>&1; then
                    groupadd --system lego
                    log "Created lego group"
                fi

                if id gitlab-www >/dev/null 2>&1; then
                    if id -nG gitlab-www |
                        tr ' ' '\n' |
                        grep -Fxq lego; then
                        log "gitlab-www is already a member of the lego group"
                    else
                        usermod -aG lego gitlab-www
                        log "Added gitlab-www to the lego group"
                    fi
                else
                    log "GitLab web user gitlab-www was not found; skipping group membership"
                fi

                enable_packaged_pre_hook gitlab 50
                log "Enabled packaged pre hook: gitlab"

                enable_packaged_post_hook gitlab 50
                log "Enabled packaged post hook: gitlab"

                enable_packaged_deploy_hook gitlab 50
                log "Enabled packaged deploy hook: gitlab"
                ;;
            n|no)
                ;;
            *)
                die "Invalid GitLab integration selection: ${answer}"
                ;;
        esac
    fi
    
    if command -v squid >/dev/null 2>&1 &&
       systemctl cat squid.service >/dev/null 2>&1; then
        printf '
Squid detected.
'
        read -r -p "Activate renewed certificates for Squid? [Y/n]: " answer
        case "${answer,,}" in
            ""|y|yes)
                enable_packaged_deploy_hook squid 50
                log "Enabled packaged deploy hook: squid"
                ;;
            n|no)
                ;;
            *)
                die "Invalid Squid integration selection: ${answer}"
                ;;
        esac
    fi
}

install_mail_dependencies_debian() {
    local mailname

    command -v apt-get >/dev/null 2>&1 ||
        die "apt-get is unavailable"

    mailname="$(hostname -f 2>/dev/null || hostname)"

    export DEBIAN_FRONTEND=noninteractive

    if ! dpkg-query -W -f='${Status}' postfix 2>/dev/null |
         grep -q '^install ok installed$'; then

        command -v debconf-set-selections >/dev/null 2>&1 ||
            die "debconf-set-selections is unavailable"

        printf '%s\n' \
            "postfix postfix/mailname string ${mailname}" \
            "postfix postfix/main_mailer_type select Local only" |
            debconf-set-selections
    fi

    apt-get update

    apt-get install -y --no-install-recommends \
        postfix
}

install_mail_dependencies_rpm() {
    if command -v dnf >/dev/null 2>&1; then
        dnf install -y postfix
    elif command -v yum >/dev/null 2>&1; then
        yum install -y postfix
    else
        die "Neither dnf nor yum is available"
    fi
}

install_mail_dependencies() {
    if command -v apt-get >/dev/null 2>&1; then
        install_mail_dependencies_debian
    elif command -v dnf >/dev/null 2>&1 ||
         command -v yum >/dev/null 2>&1; then
        install_mail_dependencies_rpm
    else
        die "Unsupported package manager"
    fi
}

main() {
    require_root

    local domain_input ca_choice internal=false acme_url="" service=""
    local tls_skip_verify_choice tls_skip_verify=false
    local pfx_choice pfx_enabled=false pfx_password=""
    local contact_email=""
    local short_name_choice add_short_names=false
    local detected_fqdn=""
    local -a fqdn_domains=() domains=()
    local -A seen=()

    detected_fqdn="$(hostname -f 2>/dev/null || true)"
    detected_fqdn="${detected_fqdn,,}"
    detected_fqdn="${detected_fqdn%.}"

    if ! is_valid_fqdn "${detected_fqdn}"; then
        detected_fqdn=""
    fi

    if [[ -n "${detected_fqdn}" ]]; then
        read -r -p "Domains (space or comma separated FQDNs) [${detected_fqdn}]: " domain_input
        domain_input="${domain_input:-${detected_fqdn}}"
    else
        read -r -p "Domains (space or comma separated FQDNs): " domain_input
    fi

    domain_input="${domain_input//,/ }"

    local item domain short
    for item in ${domain_input}; do
        domain="${item,,}"
        domain="${domain%.}"
        is_valid_fqdn "${domain}" || die "Invalid FQDN: ${item}"

        if [[ -z "${seen[${domain}]:-}" ]]; then
            fqdn_domains+=("${domain}")
            seen["${domain}"]=1
        fi
    done

    ((${#fqdn_domains[@]} > 0)) || die "At least one FQDN is required"

    printf '\nCertificate authority:\n'
    printf '  1) Public Let\x27s Encrypt\n'
    printf '  2) Internal ACME server\n'
    read -r -p "Selection [2]: " ca_choice
    ca_choice="${ca_choice:-2}"

    case "${ca_choice,,}" in
        1|public|external|letsencrypt|letsencrypt-production)
            internal=false
            ;;
        2|internal)
            internal=true
            read -r -p "Internal ACME address (https:// assumed) [${DEFAULT_INTERNAL_ACME_URL}]: " acme_url
            acme_url="${acme_url:-${DEFAULT_INTERNAL_ACME_URL}}"
            acme_url="$(trim "${acme_url}")"

            # lego v5 requires an HTTPS ACME server URL. Keep interactive input
            # concise by allowing users to enter only host[/path]. Existing
            # automation that already supplies https:// continues to work.
            case "${acme_url}" in
                https://*) ;;
                *://*) die "Internal ACME server must use HTTPS" ;;
                *) acme_url="https://${acme_url}" ;;
            esac
            [[ "${acme_url}" =~ ^https://[^[:space:]]+$ ]] || die "Invalid ACME address"

            read -r -p "Skip TLS certificate verification for the internal ACME server? [y/N]: " tls_skip_verify_choice
            case "${tls_skip_verify_choice,,}" in
                y|yes)
                    tls_skip_verify=true
                    ;;
                ""|n|no)
                    tls_skip_verify=false
                    ;;
                *)
                    die "Invalid TLS verification selection: ${tls_skip_verify_choice}"
                    ;;
            esac

            read -r -p "Include short hostnames for internal certificates? [Y/n]: " short_name_choice
            case "${short_name_choice,,}" in
                ""|y|yes)
                    add_short_names=true
                    ;;
                n|no)
                    add_short_names=false
                    ;;
                *)
                    die "Invalid short-name selection: ${short_name_choice}"
                    ;;
            esac
            ;;
        *)
            die "Unknown certificate authority selection: ${ca_choice}"
            ;;
    esac

    read -r -p "Generate a PKCS#12/PFX file? [y/N]: " pfx_choice
    case "${pfx_choice,,}" in
        y|yes)
            pfx_enabled=true
            read -r -s -p "PFX password (leave blank for no password): " pfx_password
            printf '\n'
            ;;
        ""|n|no)
            pfx_enabled=false
            ;;
        *)
            die "Invalid PFX selection: ${pfx_choice}"
            ;;
    esac

    read -r -p \
        "Contact email for ACME registration and renewal notifications [${DEFAULT_CONTACT_EMAIL}]: " \
        contact_email
    contact_email="$(trim "${contact_email:-${DEFAULT_CONTACT_EMAIL}}")"
    is_valid_email "${contact_email}" || die "Invalid email address: ${contact_email}"

    for domain in "${fqdn_domains[@]}"; do
        domains+=("${domain}")

        if ${internal} && ${add_short_names}; then
            short="${domain%%.*}"
            if [[ -n "${short}" && -z "${seen[${short}]:-}" ]]; then
                domains+=("${short}")
                seen["${short}"]=1
            fi
        fi
    done

    getent group lego >/dev/null 2>&1 || die "Required group does not exist: lego"
    install -d -o root -g lego -m 2750 "${CONFIG_DIR}" "${PRE_DIR}" "${POST_DIR}" "${DEPLOY_DIR}" "${LOCAL_HOOK_DIR}"
    install -d -o root -g lego -m 2770 "${CONFIG_DIR}/accounts" "${CONFIG_DIR}/certificates"
    enable_packaged_deploy_hook permissions 99
    if ${internal}; then
        enable_packaged_deploy_hook trust-issuer 10
        log "Enabled packaged deploy hook: trust-issuer"
    fi

    detect_and_configure_integrations
    install_mail_dependencies
    configure_mail_transport
    rm -f -- "${CONFIG_DIR}/failure-email"

    if [[ -n "${DETECTED_SERVICE_DEFAULT}" ]]; then
        read -r -p "Service to stop/start for TLS-ALPN-01 [${DETECTED_SERVICE_DEFAULT}]: " service
        service="${service:-${DETECTED_SERVICE_DEFAULT}}"
    else
        read -r -p "Service to stop/start for TLS-ALPN-01 [none]: " service
    fi
    service="$(normalize_service "${service}")"
    if [[ -n "${service}" ]]; then
        systemctl cat "${service}" >/dev/null 2>&1 ||
            die "Systemd service not found: ${service}"
        write_service_hooks "${service}"
    fi

    local tmp_file cert_id
    cert_id="${fqdn_domains[0]}"
    tmp_file="$(mktemp "${CONFIG_DIR}/.lego.yml.XXXXXX")"
    trap 'rm -f -- "${tmp_file:-}"' EXIT

    {
        printf 'storage: %s\n\n' "$(yaml_quote "${CONFIG_DIR}")"

        if ${internal}; then
            printf 'servers:\n'
            printf '  default:\n'
            printf '    url: %s\n' "$(yaml_quote "${acme_url}")"
            if ${tls_skip_verify}; then
                printf '    tlsSkipVerify: true\n'
            fi
            printf '\n'
            printf 'accounts:\n'
            printf '  default:\n'
            printf '    server: default\n'
            printf '    email: %s\n' "$(yaml_quote "${contact_email}")"
            printf '    acceptsTermsOfService: true\n\n'
        else
            printf 'accounts:\n'
            printf '  default:\n'
            printf '    server: letsencrypt\n'
            printf '    email: %s\n' "$(yaml_quote "${contact_email}")"
            printf '    acceptsTermsOfService: true\n\n'
        fi

        printf 'certificates:\n'
        printf '  %s:\n' "$(yaml_quote "${cert_id}")"
        printf '    account: default\n'
        printf '    challenge: tls-alpn-01\n'
        printf '    keyType: RSA2048\n'
        printf '    noBundle: true\n'
        if ${pfx_enabled}; then
            printf '    pfx:\n'
            printf '      password: %s\n' "$(yaml_quote "${pfx_password}")"
            printf '      format: PBMAC1\n'
        fi
        printf '    renew:\n'
        printf '      days: 5\n'
        printf '      disableRandomSleep: true\n'
        printf '    domains:\n'
        for domain in "${domains[@]}"; do
            printf '      - %s\n' "$(yaml_quote "${domain}")"
        done

        printf '\nhooks:\n'
        printf '  pre:\n'
        printf '    command: %s\n' "$(yaml_quote "${MASTER_PRE_HOOK}")"
        printf '    timeout: 5m\n'
        printf '  post:\n'
        printf '    command: %s\n' "$(yaml_quote "${MASTER_POST_HOOK}")"
        printf '    timeout: 5m\n'
        printf '  deploy:\n'
        printf '    command: %s\n' "$(yaml_quote "${MASTER_DEPLOY_HOOK}")"
        printf '    timeout: 5m\n'
    } >"${tmp_file}"

    chmod 0640 "${tmp_file}"
    chown root:lego "${tmp_file}"
    mv -f -- "${tmp_file}" "${CONFIG_FILE}"
    "${PERMISSIONS_HELPER}"
    trap - EXIT

    log ""
    log "Created ${CONFIG_FILE}"
    log "Challenge: TLS-ALPN-01"
    log "Certificate authority: $(${internal} && printf 'internal ACME' || printf "Let's Encrypt")"
    if ${internal}; then
        log "Internal ACME URL: ${acme_url}"
        if ${tls_skip_verify}; then
            log "Internal ACME TLS verification: disabled (bootstrap mode)"
        else
            log "Internal ACME TLS verification: enabled"
        fi
        if ${add_short_names}; then
            log "Short hostnames: included"
        else
            log "Short hostnames: not included"
        fi
    fi
    log "Certificate name: ${cert_id}"
    if ${pfx_enabled}; then
        if [[ -n "${pfx_password}" ]]; then
            log "PFX output: enabled (password protected)"
        else
            log "PFX output: enabled (empty password)"
        fi
    else
        log "PFX output: disabled"
    fi
    log "Domains:"
    printf '  %s\n' "${domains[@]}"

    if [[ -n "${service}" ]]; then
        log "Service hooks: stop/start ${service}"
    else
        log "Service hooks: none"
    fi
    log "ACME contact and failure notifications: ${contact_email}"
    log "Mail transport: Postfix direct delivery"
    log "Test failure email with:"
    log "  systemctl start lego-failure-email@lego-update.service"

    log ""
    if command -v systemctl >/dev/null 2>&1; then
        systemctl daemon-reload
        systemctl enable --now lego-update.timer
        log "Renewal timer: enabled (daily at 01:30 local time)"
    else
        log "Renewal timer: not enabled because systemctl is unavailable"
    fi

    log ""
    log "Request or renew the certificate manually with:"
    log "  lego-update"
    log ""
    log "View the renewal schedule with:"
    log "  systemctl list-timers lego-update.timer"
}

main "$@"
