#!/usr/bin/env bash
# =============================================================================
# onx-cron-slice-wrap — Wrap per-user crontab entries with systemd-run --slice
#
# v86.3 — Per-user cron systemd slice integration.
#
# Sorun:
#   /var/spool/cron/onx_<user> dosyasındaki cron entry'leri cron daemon
#   (crond) tarafından sürekli olarak `system.slice/crond.service` cgroup'u
#   altında çalıştırılır. Bu durum customer-<user>.slice limit'lerini
#   uygulamasını engeller — kullanıcının cron işi tüm CPU/RAM'i tüketebilir.
#
# Çözüm:
#   Her cron entry'sinin command kısmını
#     `systemd-run --no-block --slice=customer-<user>.slice --collect <cmd>`
#   ile sar. systemd-run --slice opsiyonu işi belirtilen cgroup içinde
#   transient unit olarak başlatır — cgroup CPU/Memory/IO limit'leri uygulanır.
#
# Input (stdin JSON):
#   { "username": "onx_leafport", "mode": "wrap" }   -- single user wrap (default)
#   { "username": "onx_leafport", "mode": "unwrap" } -- strip systemd-run prefix
#   { "username": "onx_leafport", "mode": "verify" } -- dry-run sayım (no writes)
#   { "all": true }                                   -- tüm onx_ user'lar
#   {}                                                -- tüm onx_ user'lar (all: true alias)
#
# Modes:
#   - wrap   (default): un-wrapped cron line'ları systemd-run --slice ile sar
#   - unwrap         : önceki wrap'i geri al (rollback için)
#   - verify         : write yapma, sayımları ve current state'i raporla
#
# Output (stdout JSON):
#   {
#     "ok": true,
#     "mode": "wrap",
#     "wrapped": [
#       {
#         "username": "onx_leafport",
#         "entries_total": 3,
#         "entries_wrapped": 2,
#         "entries_skipped": 1,           -- comment / env / zaten wrapped
#         "entries_wrapped_before": 0,    -- mode=verify için input state
#         "crontab_changed": true,
#         "crontab_missing": false,       -- /var/spool/cron/<user> yoksa true
#         "user_not_found": false,        -- Linux user yoksa true
#         "backup_path": "/var/spool/cron/onx_leafport.bak.v86.3.1745522345"
#       }
#     ],
#     "summary": { "users": 1, "wrapped": 2, "skipped": 1, "changed": 1 }
#   }
#
# Edge cases:
#   - Boş satır → skip
#   - Comment satırı (^#) → skip + preserve (zaten wrapped marker hariç)
#   - Env var satırı (PATH=, MAILTO=, SHELL=, KEY=value) → skip + preserve
#   - @reboot, @daily, @hourly gibi shortcut'lar → wrap edilir
#   - Halihazırda `systemd-run` içeren satır → skip (idempotent)
#   - `# v86.3:` marker satırından sonraki entry → zaten wrapped, skip
#
# Crontab transformation:
#   Önce:
#     0 2 * * * /usr/bin/php /var/www/onx_leafport/cron.php
#   Sonra:
#     # v86.3: cgroup slice wrap
#     0 2 * * * systemd-run --no-block --slice=customer-onx_leafport.slice --collect /usr/bin/php /var/www/onx_leafport/cron.php
#
# Exit codes:
#   0 ok / 1 invalid input / 2 preflight / 3 exec
#
# Sudoers entry needed:
#   apache ALL=(root) NOPASSWD: /usr/local/onoxsoft/bin/onx-cron-slice-wrap
#
# Deployed to: /usr/local/onoxsoft/bin/onx-cron-slice-wrap
# =============================================================================

set -euo pipefail

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

readonly CRON_SPOOL_DIR="/var/spool/cron"
readonly WRAP_MARKER="# v86.3: cgroup slice wrap"
readonly SYSTEMD_RUN_BIN="/usr/bin/systemd-run"

# ── Dependencies ──────────────────────────────────────────────────────────────
command -v jq       >/dev/null 2>&1 || { printf '{"error":"jq required"}\n' >&2; exit 2; }
command -v crontab  >/dev/null 2>&1 || { printf '{"error":"crontab required"}\n' >&2; exit 2; }
require_root

# ── Read & parse stdin ────────────────────────────────────────────────────────
onx_json_input

USERNAME=$(onx_json_field "username" "")
ALL_FLAG=$(onx_json_field "all" "false")
MODE=$(onx_json_field "mode" "wrap")

# Mode validate
case "${MODE}" in
    wrap|unwrap|verify) ;;
    *) onx_die 1 "invalid mode '${MODE}': must be wrap|unwrap|verify" ;;
esac

# Boş input → tüm user'lar (alias of all=true)
if [[ -z "${USERNAME}" && "${ALL_FLAG}" != "true" ]]; then
    # Hiçbir input yok; default = all
    ALL_FLAG="true"
fi

# Username verildiyse validate
if [[ -n "${USERNAME}" ]]; then
    onx_validate_username "${USERNAME}"
fi

# ── User listesi belirle ──────────────────────────────────────────────────────
declare -a USER_LIST=()

if [[ "${ALL_FLAG}" == "true" && -z "${USERNAME}" ]]; then
    # /var/spool/cron/ altında onx_ prefix'li dosyaları topla
    if [[ -d "${CRON_SPOOL_DIR}" ]]; then
        while IFS= read -r -d '' f; do
            base="$(basename "$f")"
            # onx_ prefix + sadece dosya (symlink değil) + okunabilir
            if [[ "${base}" =~ ^onx_[a-z0-9]{4,12}$ ]]; then
                USER_LIST+=("${base}")
            fi
        done < <(find "${CRON_SPOOL_DIR}" -maxdepth 1 -type f -name 'onx_*' -print0 2>/dev/null)
    fi
else
    USER_LIST=("${USERNAME}")
fi

# ── Wrap logic ────────────────────────────────────────────────────────────────
# Her entry için:
#   1. Boş veya comment → preserve
#   2. Env var (KEY=...) → preserve
#   3. Halihazırda systemd-run → preserve (idempotent)
#   4. Cron schedule + cmd → wrap

wrap_user_crontab() {
    local user="$1"
    local mode="$2"   # wrap | unwrap | verify
    local slice_name="customer-${user}.slice"
    local cron_file="${CRON_SPOOL_DIR}/${user}"

    local entries_total=0
    local entries_wrapped=0           # bu run'da yeni wrap edilen (mode=wrap) veya unwrap edilen (mode=unwrap)
    local entries_skipped=0
    local entries_wrapped_before=0    # girişte zaten systemd-run içeren entry sayısı (verify için kritik)
    local crontab_changed_flag=0
    local crontab_missing_flag=0
    local backup_path=""

    # /etc/passwd kontrolü (idempotent skip)
    if ! getent passwd "${user}" >/dev/null 2>&1; then
        jq -nc \
            --arg user "${user}" \
            --arg backup "" \
            '{username:$user, entries_total:0, entries_wrapped:0, entries_skipped:0, entries_wrapped_before:0, crontab_changed:false, crontab_missing:false, user_not_found:true, backup_path:$backup}'
        return 0
    fi

    # Crontab oku (root yetkisi var — doğrudan spool dosyasını oku)
    local raw=""
    if [[ -f "${cron_file}" ]]; then
        raw="$(cat "${cron_file}" 2>/dev/null || true)"
    else
        # /var/spool/cron/<user> yoksa user'ın crontab'i hiç ayarlanmamış → skip
        crontab_missing_flag=1
        raw=""
    fi

    # Boş crontab → no-op skip
    if [[ -z "${raw}" ]]; then
        jq -nc \
            --arg user "${user}" \
            --argjson missing "${crontab_missing_flag}" \
            '{username:$user, entries_total:0, entries_wrapped:0, entries_skipped:0, entries_wrapped_before:0, crontab_changed:false, crontab_missing:($missing==1), user_not_found:false, backup_path:""}'
        return 0
    fi

    # Backup (sadece gerçek file değişikliği yapacak modlarda)
    if [[ -f "${cron_file}" && "${MOCK_MODE}" != "1" && "${mode}" != "verify" ]]; then
        backup_path="${cron_file}.bak.v86.3.$(date +%s)"
        cp -p "${cron_file}" "${backup_path}" 2>/dev/null || true
    fi

    # Yeni crontab'i topla
    local new_crontab=""
    local prev_was_marker=0   # Önceki satır WRAP_MARKER ise marker'ı yutmak için track

    # Regex: schedule sonrası systemd-run prefix (unwrap için kapsamlı match)
    # Pattern: <SCHEDULE> /usr/bin/systemd-run --... <CMD>
    # systemd-run flag listesi: --slice=..., --no-block, --quiet, --pipe, --collect, --uid=..., --setenv=..., --scope
    # Bizimkinde --no-block --slice=customer-X.slice --collect kullanıyoruz.
    local systemd_run_re='^([[:space:]]*(@[a-z]+|[^[:space:]]+([[:space:]]+[^[:space:]]+){4}))[[:space:]]+/usr/bin/systemd-run[[:space:]]+(--[a-zA-Z0-9_=:/.,@-]+[[:space:]]+)+(.+)$'

    while IFS= read -r line || [[ -n "${line}" ]]; do
        # ── 1. Boş satır → preserve, sayma ──
        if [[ -z "${line// /}" ]]; then
            new_crontab+="${line}"$'\n'
            prev_was_marker=0
            continue
        fi

        # ── 2. Comment satırı ──
        if [[ "${line}" =~ ^[[:space:]]*# ]]; then
            # Wrap marker'ı: unwrap modunda yut, diğerlerinde preserve
            if [[ "${line}" == *"v86.3: cgroup slice wrap"* ]]; then
                if [[ "${mode}" == "unwrap" ]]; then
                    # Marker'ı atla (sonraki entry strip edilecek)
                    prev_was_marker=1
                    continue
                else
                    new_crontab+="${line}"$'\n'
                    prev_was_marker=1
                    continue
                fi
            fi
            new_crontab+="${line}"$'\n'
            prev_was_marker=0
            continue
        fi

        # ── 3. Env var (KEY=VALUE) → preserve ──
        if [[ "${line}" =~ ^[[:space:]]*[A-Z_][A-Z0-9_]*= ]]; then
            new_crontab+="${line}"$'\n'
            prev_was_marker=0
            continue
        fi

        # ── Cron entry — count + decide ──
        entries_total=$((entries_total + 1))

        local is_wrapped=0
        if [[ "${line}" == *"systemd-run"* ]]; then
            is_wrapped=1
            entries_wrapped_before=$((entries_wrapped_before + 1))
        fi

        case "${mode}" in
            verify)
                # Sadece sayım — line'ı koru
                new_crontab+="${line}"$'\n'
                if [[ ${is_wrapped} -eq 1 ]]; then
                    entries_skipped=$((entries_skipped + 1))
                fi
                prev_was_marker=0
                ;;

            wrap)
                if [[ ${is_wrapped} -eq 1 ]]; then
                    # Zaten wrapped → idempotent skip
                    new_crontab+="${line}"$'\n'
                    entries_skipped=$((entries_skipped + 1))
                    prev_was_marker=0
                    continue
                fi

                # Schedule + cmd parse
                local schedule cmd
                if [[ "${line}" =~ ^[[:space:]]*@([a-z]+)[[:space:]]+(.+)$ ]]; then
                    schedule="@${BASH_REMATCH[1]}"
                    cmd="${BASH_REMATCH[2]}"
                else
                    schedule="$(echo "${line}" | awk '{print $1, $2, $3, $4, $5}')"
                    cmd="$(echo "${line}" | awk '{for(i=6;i<=NF;i++) printf "%s%s", $i, (i==NF?"":" ")}')"
                fi

                if [[ -z "${cmd}" ]]; then
                    new_crontab+="${line}"$'\n'
                    entries_skipped=$((entries_skipped + 1))
                    prev_was_marker=0
                    continue
                fi

                # cmd içindeki single-quote'ları escape et: '  →  '\''
                local cmd_escaped="${cmd//\'/\'\\\'\'}"

                # systemd-run scope ile sar — env tut, exit code propagate
                new_crontab+="${WRAP_MARKER}"$'\n'
                new_crontab+="${schedule} ${SYSTEMD_RUN_BIN} --quiet --pipe --collect --slice=${slice_name} --uid=${user} --setenv=HOME=/home/${user} --setenv=PATH=/usr/local/bin:/usr/bin:/bin /bin/sh -c '${cmd_escaped}'"$'\n'
                entries_wrapped=$((entries_wrapped + 1))
                crontab_changed_flag=1
                prev_was_marker=0
                ;;

            unwrap)
                if [[ ${is_wrapped} -ne 1 ]]; then
                    # Wrapped değil — preserve
                    new_crontab+="${line}"$'\n'
                    entries_skipped=$((entries_skipped + 1))
                    prev_was_marker=0
                    continue
                fi

                # systemd-run prefix'i strip et — kalan cmd /bin/sh -c '...' içinde
                # Pattern: <SCHED> /usr/bin/systemd-run <FLAGS...> /bin/sh -c '<CMD>'
                # Veya:    <SCHED> /usr/bin/systemd-run <FLAGS...> <CMD>
                local stripped=""
                if [[ "${line}" =~ /bin/sh\ +-c\ +\'(.+)\'[[:space:]]*$ ]]; then
                    # Quoted form: /bin/sh -c '...'
                    local sched_part
                    if [[ "${line}" =~ ^([[:space:]]*@[a-z]+) ]]; then
                        sched_part="${BASH_REMATCH[1]}"
                    elif [[ "${line}" =~ ^([[:space:]]*[^[:space:]]+([[:space:]]+[^[:space:]]+){4}) ]]; then
                        sched_part="${BASH_REMATCH[1]}"
                    else
                        sched_part="$(echo "${line}" | awk '{print $1, $2, $3, $4, $5}')"
                    fi
                    # /bin/sh -c '<cmd>' içindeki escape'i geri al: '\''  →  '
                    local inner="${BASH_REMATCH[1]}"
                    inner="${inner//\'\\\'\'/\'}"
                    stripped="${sched_part} ${inner}"
                else
                    # Unquoted form (eski wrap çıktısı): <SCHED> systemd-run <FLAGS> <CMD>
                    # Flag listesinin sonunu bul: ilk olarak --xxx ile başlamayan token
                    stripped="$(echo "${line}" | awk '
                        {
                            n=NF; out=""; sched_count=0; in_flags=0; cmd_start=0;
                            # @ shortcut mı normal mi?
                            if ($1 ~ /^@/) { sched_count=1 } else { sched_count=5 }
                            for(i=sched_count+1; i<=n; i++) {
                                if ($i == "/usr/bin/systemd-run" || $i == "systemd-run") { in_flags=1; continue }
                                if (in_flags && substr($i,1,2) == "--") { continue }
                                if (in_flags) { cmd_start=i; break }
                            }
                            for(i=1; i<=sched_count; i++) out = out (out=="" ? "" : " ") $i
                            if (cmd_start>0) {
                                for(i=cmd_start; i<=n; i++) out = out " " $i
                            }
                            print out
                        }')"
                fi

                if [[ -z "${stripped}" ]]; then
                    # Strip başarısız — defensive preserve
                    new_crontab+="${line}"$'\n'
                    entries_skipped=$((entries_skipped + 1))
                    prev_was_marker=0
                    continue
                fi

                new_crontab+="${stripped}"$'\n'
                entries_wrapped=$((entries_wrapped + 1))   # unwrapped sayısı
                crontab_changed_flag=1
                prev_was_marker=0
                ;;
        esac
    done <<< "${raw}"

    # ── Write back ──
    if [[ "${MOCK_MODE}" == "1" || "${mode}" == "verify" ]]; then
        # Mock veya verify — write etme
        :
    elif [[ ${entries_wrapped} -gt 0 ]]; then
        # crontab -u ile yaz (validate edilir + crond reload)
        if ! printf '%s' "${new_crontab}" | crontab -u "${user}" - 2>/dev/null; then
            onx_log "WARNING: crontab -u ${user} ${mode} write failed"
            # Backup'tan restore (best-effort)
            if [[ -n "${backup_path}" && -f "${backup_path}" ]]; then
                cp -p "${backup_path}" "${cron_file}" 2>/dev/null || true
                chown "${user}:${user}" "${cron_file}" 2>/dev/null || true
                chmod 0600 "${cron_file}" 2>/dev/null || true
            fi
            onx_die 3 "crontab write failed for user=${user} mode=${mode}"
        fi
        # crontab -u kendi chown/chmod yapar ama defensive belirt
        chown "${user}:${user}" "${cron_file}" 2>/dev/null || true
        chmod 0600 "${cron_file}" 2>/dev/null || true
        onx_audit "onx-cron-slice-wrap" "user=${user} mode=${mode} wrapped=${entries_wrapped} skipped=${entries_skipped}"
    fi

    jq -nc \
        --arg user "${user}" \
        --argjson total "${entries_total}" \
        --argjson wrapped "${entries_wrapped}" \
        --argjson skipped "${entries_skipped}" \
        --argjson before "${entries_wrapped_before}" \
        --argjson changed "${crontab_changed_flag}" \
        --argjson missing "${crontab_missing_flag}" \
        --arg backup "${backup_path}" \
        '{username:$user, entries_total:$total, entries_wrapped:$wrapped, entries_skipped:$skipped, entries_wrapped_before:$before, crontab_changed:($changed==1), crontab_missing:($missing==1), user_not_found:false, backup_path:$backup}'
}

# ── Iterate over users ────────────────────────────────────────────────────────
WRAPPED_RESULTS="["
SEP=""
TOTAL_USERS=0
TOTAL_WRAPPED=0
TOTAL_SKIPPED=0
TOTAL_CHANGED=0

if [[ ${#USER_LIST[@]} -eq 0 ]]; then
    # Hiç user yok — empty result
    :
else
    for user in "${USER_LIST[@]}"; do
        # User Linux'ta var mı kontrol (idempotent skip — fn içinde de check var
        # ama burada loop sayısını şişirmemek için early-skip)
        if ! getent passwd "${user}" >/dev/null 2>&1; then
            onx_log "skip: ${user} (Linux user yok)"
            # Yine de per_user entry üret — caller hangi user'ı atladığımızı görsün
            result="$(jq -nc --arg u "${user}" '{username:$u, entries_total:0, entries_wrapped:0, entries_skipped:0, entries_wrapped_before:0, crontab_changed:false, crontab_missing:false, user_not_found:true, backup_path:""}')"
            WRAPPED_RESULTS+="${SEP}${result}"
            SEP=","
            TOTAL_USERS=$((TOTAL_USERS + 1))
            continue
        fi

        result="$(wrap_user_crontab "${user}" "${MODE}")"
        WRAPPED_RESULTS+="${SEP}${result}"
        SEP=","
        TOTAL_USERS=$((TOTAL_USERS + 1))

        # Aggregate
        w=$(echo "${result}" | jq -r '.entries_wrapped')
        s=$(echo "${result}" | jq -r '.entries_skipped')
        c=$(echo "${result}" | jq -r 'if .crontab_changed then 1 else 0 end')
        TOTAL_WRAPPED=$((TOTAL_WRAPPED + w))
        TOTAL_SKIPPED=$((TOTAL_SKIPPED + s))
        TOTAL_CHANGED=$((TOTAL_CHANGED + c))
    done
fi

WRAPPED_RESULTS+="]"

# ── Output ────────────────────────────────────────────────────────────────────
jq -nc \
    --arg mode "${MODE}" \
    --argjson wrapped "${WRAPPED_RESULTS}" \
    --argjson users "${TOTAL_USERS}" \
    --argjson tw "${TOTAL_WRAPPED}" \
    --argjson ts "${TOTAL_SKIPPED}" \
    --argjson tc "${TOTAL_CHANGED}" \
    '{
        ok: true,
        mode: $mode,
        wrapped: $wrapped,
        summary: { users: $users, wrapped: $tw, skipped: $ts, changed: $tc }
    }'
