#!/usr/bin/env bash
# =============================================================================
# onx-ols-vhost-cert-link (v85.1) — Link OLS vhost SSL to central acme.sh cert
#
# v78.1 backlog item: Tüm OLS vhost'ları merkezi acme.sh multi-SAN cert kullansın.
# Mevcut durum: her vhost ayrı certFile path. Bu script vhconf.conf'taki vhssl
# block'unu acme.sh cert path'iyle güncelliyor — Apache SNI/multi-SAN paritesi.
#
# Strateji (parent fallback chain):
#   1. /root/.acme.sh/<domain>_ecc/fullchain.cer   (ECC tercih)
#   2. /root/.acme.sh/<domain>/fullchain.cer       (RSA fallback)
#   3. Parent domain (subdomain heuristic) için aynı 1-2 tarama
#      (webmail.X.com.tr → X.com.tr; multi-SAN cert tek dosya hepsi için)
#   4. /etc/letsencrypt/live/<domain>/fullchain.pem (legacy fallback)
#
# Mode:
#   - vhost_name=onx_user-domain  → tek vhost link
#   - mode=bulk                   → /usr/local/lsws/conf/vhosts/onx_* hepsini link
#
# Idempotent:
#   - vhssl block zaten doğru cert'i gösteriyor → skip + json "linked":false
#   - vhssl block yoksa (HTTP-only) → skip + warning
#   - cert bulunamazsa → skip o vhost, log + bulk modda devam et
#
# Input (stdin JSON):
#   {"vhost_name": "onx_user-domain.com"}      tek vhost
#   {"mode": "bulk"}                            tüm onx_* vhost'lar
#   {"mode": "bulk", "include_panel": true}    onoxsoft-panel da dahil (default false — panel cert
#                                              ayrı yönetiliyor, panel.onoxsoft.com.tr.fullchain)
#
# Output (stdout JSON):
#   {
#     "ok": true,
#     "mode": "single"|"bulk",
#     "linked": 3,        ← cert path değişen vhost sayısı
#     "skipped": 5,       ← zaten doğru / vhssl yok / cert yok
#     "vhosts": [{"name": "...", "domain": "...", "status": "linked|same|no-cert|no-vhssl"}],
#     "reloaded": true,
#     "message": "..."
#   }
#
# Exit codes:
#   0  — success (even if 0 linked — bulk modda her vhost ayrı raporlanır)
#   1  — invalid input (vhost_name veya mode yok, vhost dosyası yok)
#   2  — preflight fail (lsws yok)
#   3  — execution fail (lswsctrl restart fail, kritik dosya yazma fail)
#
# Sudoers:
#   apache ALL=(root) NOPASSWD: /usr/local/onoxsoft/bin/onx-ols-vhost-cert-link
#
# Deployed to: /usr/local/onoxsoft/bin/onx-ols-vhost-cert-link
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

# ── Constants ────────────────────────────────────────────────────────────────
LSWS_BASE="/usr/local/lsws"
VHOST_BASE="${LSWS_BASE}/conf/vhosts"
LSWS_CTL="${LSWS_BASE}/bin/lswsctrl"
ACME_BASE="/root/.acme.sh"
LE_BASE="/etc/letsencrypt/live"

# ── Preflight ────────────────────────────────────────────────────────────────
[[ -d "${LSWS_BASE}" ]] || onx_die 2 "OpenLiteSpeed not installed: ${LSWS_BASE} not found"
[[ -d "${VHOST_BASE}" ]] || onx_die 2 "OLS vhost base missing: ${VHOST_BASE}"
[[ -x "${LSWS_CTL}" ]] || onx_die 2 "lswsctrl not found: ${LSWS_CTL}"

# ── Read input ───────────────────────────────────────────────────────────────
onx_json_input
VHOST_NAME=$(onx_json_field "vhost_name")
# v89: accept legacy/alternate field name "vhost" (some older callers / docs
# used the shorter key — keep both working to avoid silent empty-input fails).
if [[ -z "${VHOST_NAME}" ]]; then
    VHOST_NAME=$(onx_json_field "vhost")
fi
MODE=$(onx_json_field "mode" "single")
INCLUDE_PANEL=$(onx_json_field "include_panel" "false")

if [[ "${MODE}" == "single" ]] && [[ -z "${VHOST_NAME}" ]]; then
    onx_die 1 "vhost_name required (or set mode=bulk)"
fi

# ── Helper: resolve central cert for a domain ────────────────────────────────
# Echoes "cert_path|key_path" or empty on miss.
resolve_central_cert() {
    local dom="$1"
    local cert="" key=""

    # 1+2. Direct acme.sh ECC → RSA
    for suffix in "_ecc" ""; do
        local p="${ACME_BASE}/${dom}${suffix}"
        if [[ -f "${p}/fullchain.cer" ]] && [[ -f "${p}/${dom}.key" ]]; then
            cert="${p}/fullchain.cer"
            key="${p}/${dom}.key"
            break
        fi
    done

    # 3. Parent domain fallback — multi-SAN cert tek dosya birden fazla subdomain kapsar
    # (ör. webmail.leafport.com.tr → /root/.acme.sh/leafport.com.tr_ecc/fullchain.cer
    #  içinde "Subject Alternative Name: DNS:leafport.com.tr, DNS:webmail.leafport.com.tr...")
    if [[ -z "${cert}" ]]; then
        local parent
        # Compound TLD (com.tr, co.uk, com.au, org.tr) → son 3 segment kök
        # Diğer (.com, .net, .org) → son 2 segment kök
        local dot_count
        dot_count=$(echo -n "$dom" | tr -cd '.' | wc -c)
        if [[ "$dom" == *.com.tr || "$dom" == *.co.uk || "$dom" == *.com.au || "$dom" == *.org.tr ]] \
           && [[ "$dot_count" -ge 3 ]]; then
            parent=$(echo "$dom" | awk -F. '{print $(NF-2)"."$(NF-1)"."$NF}')
        elif [[ "$dot_count" -ge 2 ]]; then
            parent=$(echo "$dom" | awk -F. '{print $(NF-1)"."$NF}')
        else
            parent=""
        fi

        if [[ -n "${parent}" ]] && [[ "${parent}" != "${dom}" ]]; then
            for suffix in "_ecc" ""; do
                local p="${ACME_BASE}/${parent}${suffix}"
                if [[ -f "${p}/fullchain.cer" ]] && [[ -f "${p}/${parent}.key" ]]; then
                    # SAN listesinde dom var mı kontrol et — yoksa multi-SAN değil,
                    # link etme (cert mismatch warning olur)
                    if openssl x509 -in "${p}/fullchain.cer" -noout -text 2>/dev/null \
                       | grep -qE "DNS:${dom//./\\.}([^a-zA-Z0-9._-]|$)"; then
                        cert="${p}/fullchain.cer"
                        key="${p}/${parent}.key"
                        break
                    fi
                fi
            done
        fi
    fi

    # 4. Let's Encrypt /etc/letsencrypt/live fallback
    if [[ -z "${cert}" ]]; then
        if [[ -f "${LE_BASE}/${dom}/fullchain.pem" ]] && [[ -f "${LE_BASE}/${dom}/privkey.pem" ]]; then
            cert="${LE_BASE}/${dom}/fullchain.pem"
            key="${LE_BASE}/${dom}/privkey.pem"
        fi
    fi

    if [[ -n "${cert}" ]]; then
        printf '%s|%s' "${cert}" "${key}"
    fi
}

# ── Helper: rewrite vhssl block in vhconf.conf ───────────────────────────────
# Returns: "linked" | "same" | "no-vhssl"
#
# vhssl block format:
#   vhssl {
#     keyFile     /path/to/key
#     certFile    /path/to/fullchain
#     ...
#   }
relink_vhost_cert() {
    local vh_conf="$1" cert="$2" key="$3"
    local tmp="${vh_conf}.relink.$$"

    # vhssl block var mı?
    if ! grep -qE '^[[:space:]]*vhssl[[:space:]]*\{' "${vh_conf}"; then
        echo "no-vhssl"
        return 0
    fi

    # Mevcut certFile path'ini çek
    local cur_cert cur_key
    cur_cert=$(awk '/^[[:space:]]*vhssl[[:space:]]*\{/,/^[[:space:]]*\}/' "${vh_conf}" \
               | awk '/^[[:space:]]*certFile[[:space:]]+/ {print $2; exit}')
    cur_key=$(awk '/^[[:space:]]*vhssl[[:space:]]*\{/,/^[[:space:]]*\}/' "${vh_conf}" \
              | awk '/^[[:space:]]*keyFile[[:space:]]+/ {print $2; exit}')

    if [[ "${cur_cert}" == "${cert}" ]] && [[ "${cur_key}" == "${key}" ]]; then
        echo "same"
        return 0
    fi

    # Backup + atomic write
    cp -p "${vh_conf}" "${vh_conf}.bak.cert-link.$$"

    # Python ile vhssl block içindeki keyFile + certFile satırlarını rewrite et
    # (sed awk multi-line block için zayıf, Python re ile clean)
    python3 - "${vh_conf}" "${tmp}" "${cert}" "${key}" <<'PYEOF'
import re, sys

src, dst, cert, key = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
with open(src) as f:
    content = f.read()

# vhssl { ... } block içindeki keyFile + certFile satırlarını değiştir
def rewrite_block(m):
    body = m.group(1)
    body = re.sub(r'^(\s*keyFile\s+).*$', lambda mm: mm.group(1) + key, body, flags=re.M)
    body = re.sub(r'^(\s*certFile\s+).*$', lambda mm: mm.group(1) + cert, body, flags=re.M)
    return 'vhssl {' + body + '}'

new = re.sub(r'vhssl\s*\{([^}]*)\}', rewrite_block, content, flags=re.DOTALL)
with open(dst, 'w') as f:
    f.write(new)
PYEOF

    # Move (atomic) + cleanup backup if matches new
    install -m 0644 "${tmp}" "${vh_conf}"
    rm -f "${tmp}"
    echo "linked"
}

# ── Build vhost list ─────────────────────────────────────────────────────────
declare -a VHOSTS=()

if [[ "${MODE}" == "bulk" ]]; then
    while IFS= read -r -d '' vh_dir; do
        local_name=$(basename "${vh_dir}")
        # Panel (onoxsoft-panel) skip default — panel cert ayrı yönetiliyor
        if [[ "${local_name}" == "onoxsoft-panel" ]] && [[ "${INCLUDE_PANEL}" != "true" ]]; then
            continue
        fi
        VHOSTS+=("${local_name}")
    done < <(find "${VHOST_BASE}" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
else
    # Single mode — vhost_name verildi
    VHOSTS+=("${VHOST_NAME}")
fi

# ── Process each vhost ───────────────────────────────────────────────────────
LINKED=0
SKIPPED=0
declare -a VHOST_RESULTS=()

# Empty vhost list (bulk modda hiç onx_* yok) — early exit OK output ile
if [[ "${#VHOSTS[@]}" -eq 0 ]]; then
    cat <<JSONEOF
{
  "ok": true,
  "mode": "${MODE}",
  "linked": 0,
  "skipped": 0,
  "reloaded": false,
  "vhosts": [],
  "result": null,
  "message": "no OLS vhosts found (bulk mode) — nothing to link"
}
JSONEOF
    exit 0
fi

for vh_name in "${VHOSTS[@]}"; do
    vh_conf="${VHOST_BASE}/${vh_name}/vhconf.conf"
    if [[ ! -f "${vh_conf}" ]]; then
        if [[ "${MODE}" == "single" ]]; then
            onx_die 1 "vhost not found: ${vh_conf}"
        fi
        SKIPPED=$((SKIPPED + 1))
        VHOST_RESULTS+=("{\"name\":\"${vh_name}\",\"status\":\"missing\"}")
        continue
    fi

    # Domain'i vhost adından parse et (onx_USER-DOMAIN veya onoxsoft-panel)
    if [[ "${vh_name}" == "onoxsoft-panel" ]]; then
        domain="panel.onoxsoft.com.tr"
    else
        # onx_user-domain.com.tr → strip onx_ prefix, then split first dash
        stripped="${vh_name#onx_}"
        domain="${stripped#*-}"
    fi

    if [[ -z "${domain}" ]]; then
        SKIPPED=$((SKIPPED + 1))
        VHOST_RESULTS+=("{\"name\":\"${vh_name}\",\"status\":\"bad-name\"}")
        continue
    fi

    # Cert resolve
    cert_pair=$(resolve_central_cert "${domain}")
    if [[ -z "${cert_pair}" ]]; then
        SKIPPED=$((SKIPPED + 1))
        VHOST_RESULTS+=("{\"name\":\"${vh_name}\",\"domain\":\"${domain}\",\"status\":\"no-cert\"}")
        onx_log "no central cert for ${domain} (no acme.sh + no LE) — skipped"
        continue
    fi

    cert_path="${cert_pair%|*}"
    key_path="${cert_pair#*|}"

    # Relink vhssl block
    result=$(relink_vhost_cert "${vh_conf}" "${cert_path}" "${key_path}")
    if [[ "${result}" == "linked" ]]; then
        LINKED=$((LINKED + 1))
        VHOST_RESULTS+=("{\"name\":\"${vh_name}\",\"domain\":\"${domain}\",\"status\":\"linked\",\"cert\":\"${cert_path}\"}")
        onx_log "linked ${vh_name} -> ${cert_path}"
    else
        SKIPPED=$((SKIPPED + 1))
        VHOST_RESULTS+=("{\"name\":\"${vh_name}\",\"domain\":\"${domain}\",\"status\":\"${result}\"}")
    fi
done

# ── Reload OLS only if anything actually changed ─────────────────────────────
RELOADED="false"
if [[ "${LINKED}" -gt 0 ]]; then
    # OLS vhssl cert path değişikliği için reload yeterli (TLS context yeniden yüklenir).
    # SNI cert cache temizleme listener seviyesinde restart gerektirir ama vhssl direkt
    # virtualhost'a bağlı — reload sufficient.
    if "${LSWS_CTL}" restart >/dev/null 2>&1; then
        RELOADED="true"
    else
        onx_log "warning: lswsctrl restart failed after relinking ${LINKED} vhost(s)"
        # Rollback yok — backup'lar bırakıldı (vh_conf.bak.cert-link.*),
        # manuel restore mümkün. Bulk modda partial başarı tolere edilir.
    fi
fi

# ── Build JSON output ────────────────────────────────────────────────────────
# vhosts array'i jq ile birleştir
VHOSTS_JSON="[$(IFS=,; echo "${VHOST_RESULTS[*]}")]"

# Single-mode tek vhost result'unu top-level field olarak da expose et (geriye uyum)
if [[ "${MODE}" == "single" ]] && [[ "${#VHOST_RESULTS[@]}" -gt 0 ]]; then
    SINGLE_RESULT="${VHOST_RESULTS[0]}"
else
    SINGLE_RESULT="null"
fi

cat <<JSONEOF
{
  "ok": true,
  "mode": "${MODE}",
  "linked": ${LINKED},
  "skipped": ${SKIPPED},
  "reloaded": ${RELOADED},
  "vhosts": ${VHOSTS_JSON},
  "result": ${SINGLE_RESULT},
  "message": "ols vhost cert-link: ${LINKED} linked, ${SKIPPED} skipped"
}
JSONEOF

exit 0
