#!/usr/bin/env bash
# =============================================================================
# onx-panel-self-update — Panel self-update uygulayıcı (root, BRICK-SAFE)
#
# İmzalı bir release tarball'ını indirip sha256 doğrular, snapshot (kod + DB)
# ALIR, extract eder, composer/migrate/build/reload uygular, health-check yapar;
# herhangi bir adım VEYA health-check başarısız olursa OTOMATİK ROLLBACK yapar
# (snapshot kodu + DB geri yüklenir, eski VERSION yazılır, php-fpm reload).
#
# Çağıran: PHP App\Domain\System\Services\PanelUpdater (imzayı zaten doğruladı).
# Bu script tarball BÜTÜNLÜĞÜNÜ (sha256) ikinci kez doğrular (defense-in-depth).
#
# GÜVENLİK KONTRATI (referee bunu denetlesin):
#   1. flock — tek-örnek (eşzamanlı update yok).
#   2. php artisan down EN BAŞTA; HER çıkış yolunda php artisan up.
#   3. Snapshot ZORUNLU extract'ten ÖNCE: rsync kod + mysqldump DB.
#   4. İndirilen tarball sha256'sı manifest sha256 ile EŞLEŞMELİ; değilse
#      extract'ten ÖNCE ABORT (snapshot'a/koda DOKUNMADAN).
#   5. Extract: tar --exclude .env/storage/bootstrap-cache (defense-in-depth).
#   6. Sıra: composer(lock değiştiyse) → migrate --force → npm build →
#      optimize:clear → VERSION yaz → php-fpm reload.
#   7. Health-check: app YENİDEN AYAĞA kaldırıldıktan SONRA curl <health_url>;
#      200/302 değilse → OTOMATİK ROLLBACK.
#   8. JSON çıktı: {ok, rolled_back, from, to, steps, error}.
#   9. Eski snapshot'ları (3'ten fazla) prune.
#
# Input (stdin JSON):
#   { "version":"1.1.0", "url":"https://.../release.tar.gz",
#     "sha256":"<64hex>", "health_url":"https://panel/up",
#     "app_dir":"/opt/onoxsoft"        (ops; default /opt/onoxsoft),
#     "fpm_unit":"php82-php-fpm"        (ops; default php82-php-fpm) }
#
# Output (stdout JSON, exit 0): {"ok":bool,"rolled_back":bool,"from":..,"to":..,
#                                "steps":[{"name":..,"status":..}],"error":..}
# Exit codes:
#   0  — JSON yayınlandı (başarı VEYA rollback ile ele alındı)
#   1  — geçersiz girdi (version/sha256 regex)
#   2  — preflight (root değil / komut yok / başka update sürüyor)
#   3  — extract-öncesi abort (indirme/sha — panel DOKUNULMAMIŞ → PHP 'failed')
#
# Deployed to: /usr/local/onoxsoft/bin/onx-panel-self-update
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

require_root
require_cmd tar
require_cmd rsync
require_cmd curl
require_cmd sha256sum
require_cmd php
require_cmd systemctl

onx_json_input

VERSION="$(onx_json_field version)"
URL="$(onx_json_field url)"
SHA="$(onx_json_field sha256)"
HEALTH="$(onx_json_field health_url)"
APP_DIR="$(onx_json_field app_dir '/opt/onoxsoft')"
FPM_UNIT="$(onx_json_field fpm_unit 'php82-php-fpm')"

# ── Girdi doğrulama (php artisan down'dan ÖNCE → fail'de panel dokunulmamış) ──
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$ ]] || onx_die 1 "geçersiz version: $VERSION"
[[ "$SHA" =~ ^[a-f0-9]{64}$ ]] || onx_die 1 "geçersiz sha256: $SHA"
[[ -n "$URL" ]] || onx_die 1 "url zorunlu"
[[ "$URL" =~ ^https?:// ]] || onx_die 1 "url http(s) olmalı"
[[ -d "$APP_DIR" ]] || onx_die 2 "app_dir yok: $APP_DIR"
[[ -f "$APP_DIR/artisan" ]] || onx_die 2 "app_dir bir Laravel kökü değil (artisan yok): $APP_DIR"
# fpm_unit basit isim guard (komut enjeksiyonu önle)
[[ "$FPM_UNIT" =~ ^[A-Za-z0-9._@-]+$ ]] || onx_die 1 "geçersiz fpm_unit"

TS="$(date +%Y%m%d-%H%M%S)"
WORK="/var/lib/onox/updates/${VERSION}"
SNAP="${APP_DIR}.bak-${TS}"
LOCK="/var/run/onox-self-update.lock"

# DB adı .env'den (DB_DATABASE). cut -d= -f2- → '=' içeren değerleri de korur.
DB_NAME="$(grep -E '^DB_DATABASE=' "$APP_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"'"'" || true)"

# ── 1) Tek-örnek kilidi ──────────────────────────────────────────────────────
exec 9>"$LOCK" || onx_die 2 "kilit dosyası açılamadı: $LOCK"
flock -n 9 || onx_die 2 "başka bir güncelleme sürüyor (kilitli)"

mkdir -p "$WORK"

onx_log "self-update: START version=$VERSION app_dir=$APP_DIR db=$DB_NAME snapshot=$SNAP"

# ── adım izleme (JSON steps[] için) ──────────────────────────────────────────
STEPS_JSON="[]"
_step() {  # _step <name> <status>  — adım izleme SALT bilgi amaçlı; ASLA update'i
           # aborte etmemeli (jq hatası bile). `|| true` + set +e suspend yok.
    local next
    next="$(printf '%s' "$STEPS_JSON" | jq -c --arg n "$1" --arg s "$2" '. + [{name:$n, status:$s}]' 2>/dev/null)" || next="$STEPS_JSON"
    STEPS_JSON="${next:-$STEPS_JSON}"
    onx_log "self-update: step $1 = $2"
    return 0
}

# ── 2) Bakım modu (EN BAŞTA) + her çıkışta up garantisi ─────────────────────
( cd "$APP_DIR" && php artisan down --retry=60 >/dev/null 2>&1 || true )
_step "maintenance_down" "ok"

_restore_up() { ( cd "$APP_DIR" && php artisan up >/dev/null 2>&1 || true ); }

_emit() {  # _emit <ok:true|false> <rolled_back:true|false> <error-or-empty>
    jq -nc --arg from "$VERSION_BEFORE" --arg to "$VERSION" \
        --argjson ok "$1" --argjson rb "$2" --arg err "${3:-}" \
        --argjson steps "$STEPS_JSON" \
        '{ok:$ok, rolled_back:$rb, from:$from, to:$to, steps:$steps,
          error:(if $err=="" then null else $err end)}'
}

# Mevcut (eski) sürüm — rollback'te ve raporda kullanılır.
VERSION_BEFORE="$(tr -d '\r\n' < "$APP_DIR/VERSION" 2>/dev/null || echo 'unknown')"
[[ -n "$VERSION_BEFORE" ]] || VERSION_BEFORE="unknown"

# ── 3) İndir + sha256 (extract ÖNCESİ; snapshot'a/koda DOKUNMA) ──────────────
# Bu aşamadaki fail → panel DOKUNULMAMIŞ. up yap + non-zero exit (PHP 'failed').
if ! curl -fsSL --max-time 600 -o "${WORK}/release.tar.gz" "$URL"; then
    _restore_up
    onx_die 3 "indirme başarısız: $URL"
fi
DL_SHA="$(sha256sum "${WORK}/release.tar.gz" | awk '{print $1}')"
if [[ "$DL_SHA" != "$SHA" ]]; then
    rm -f "${WORK}/release.tar.gz" 2>/dev/null || true
    _restore_up
    onx_die 3 "sha256 uyuşmuyor (beklenen=$SHA aldı=$DL_SHA) — tampered olabilir, abort"
fi
_step "download_verify" "ok"

# ── 4) Snapshot (kod + DB) — extract'ten ÖNCE, ZORUNLU ───────────────────────
# Kod snapshot: .env/storage/bootstrap-cache/node_modules/.git HARİÇ (rollback'te
# de aynı exclude → kullanıcı state'i korunur).
RSYNC_EXCLUDES=(--exclude='.env' --exclude='storage' --exclude='bootstrap/cache'
                --exclude='node_modules' --exclude='.git')
if ! rsync -a --delete "${RSYNC_EXCLUDES[@]}" "$APP_DIR"/ "$SNAP"/; then
    _restore_up
    onx_die 3 "kod snapshot başarısız (rsync) — abort"
fi
# DB snapshot — root credentials (panel user dump yetkisine sahip olmayabilir).
# Boş/başarısız dump → rollback DB restore atlanır ama kod rollback yine yapılır.
DB_DUMP_OK="false"
if [[ -n "$DB_NAME" ]]; then
    if mysqldump_root --single-transaction --routines --triggers --no-tablespaces "$DB_NAME" > "${WORK}/db.sql" 2>/dev/null \
        && [[ -s "${WORK}/db.sql" ]]; then
        DB_DUMP_OK="true"
    else
        onx_log "self-update: mysqldump WARN (db=$DB_NAME) — rollback DB restore devre dışı"
    fi
else
    onx_log "self-update: DB_DATABASE okunamadı (.env) — DB snapshot atlandı"
fi
_step "snapshot" "ok"

# ── ROLLBACK fonksiyonu (apply fail VEYA health fail) ────────────────────────
ROLLED_BACK="false"
_rollback() {
    ROLLED_BACK="true"
    onx_log "self-update: ROLLBACK $VERSION → $VERSION_BEFORE"
    # Kodu snapshot'tan geri (aynı exclude → .env/storage korunur).
    rsync -a --delete "${RSYNC_EXCLUDES[@]}" "$SNAP"/ "$APP_DIR"/ 2>/dev/null || onx_log "ROLLBACK kod rsync WARN"
    # DB restore (yalnız geçerli dump alındıysa).
    if [[ "$DB_DUMP_OK" == "true" && -n "$DB_NAME" && -s "${WORK}/db.sql" ]]; then
        mysql_exec_root_stdin "$DB_NAME" < "${WORK}/db.sql" 2>/dev/null || onx_log "ROLLBACK DB restore WARN"
    fi
    # Eski VERSION'ı geri yaz (snapshot exclude etmediği için zaten geri geldi;
    # yine de garanti).
    printf '%s\n' "$VERSION_BEFORE" > "$APP_DIR/VERSION" 2>/dev/null || true
    ( cd "$APP_DIR" && php artisan optimize:clear >/dev/null 2>&1 || true )
    systemctl reload "$FPM_UNIT" 2>/dev/null || systemctl restart "$FPM_UNIT" 2>/dev/null || true
    _restore_up
    _step "rollback" "done"
}

# Apply adımı fail → rollback + JSON yayınla + exit 0 (PHP 'rolled_back' kaydeder).
# KRİTİK: set -e, bir komut `&&`/`||` listesinin parçasıysa VEYA `{ } || ...`
# brace-group'unun SOLUNDAYSA O BLOK İÇİNDE DEVRE DIŞI kalır. Bu yüzden apply
# adımlarını TEK BİR `{ ...; } || _rollback` brace'ine SARMIYORUZ (orada ara-adım
# fail'i yutulur, broken update devam ederdi). Her kritik adımı `|| _fail_rollback`
# ile AÇIKÇA guard ediyoruz; her biri kendi başına rollback'i tetikler.
_fail_rollback() {  # _fail_rollback <error-mesajı>
    onx_log "self-update: apply FAIL — ${1:-bilinmeyen}"
    _rollback
    onx_audit "onx-panel-self-update" "FAILED+ROLLEDBACK version=$VERSION from=$VERSION_BEFORE step=${1:-?} snapshot=$SNAP"
    _emit false true "${1:-apply adımı başarısız} — otomatik geri alındı"
    exit 0
}

# Health-check fail → rollback. Apply-fail ile AYNI güvenlik: `||` sağ-tarafı
# fonksiyon → gövdesinde set -e ASKIYA alınır, böylece ara-komut fail'i
# rollback'i yarıda kesip JSON'suz/down bırakamaz.
_health_fail_rollback() {  # _health_fail_rollback <http_code>
    onx_log "self-update: health-check FAIL (${1:-000}) → rollback"
    _rollback
    onx_audit "onx-panel-self-update" "HEALTHFAIL+ROLLEDBACK version=$VERSION code=${1:-000} snapshot=$SNAP"
    _emit false true "health-check başarısız (HTTP ${1:-000}) — otomatik geri alındı"
    exit 0
}

# ── 5) Extract + apply (HER adım açıkça guard'lı; set -e'ye GÜVENMİYORUZ) ─────
# composer.lock sha'sını extract ÖNCESİ al (değişti mi karşılaştır).
OLD_LOCK_SHA="$(sha256sum "$APP_DIR/composer.lock" 2>/dev/null | awk '{print $1}' || echo none)"

# 5a) Extract — .env/storage/bootstrap/cache hariç (tarball içermese de DiD).
tar -xzf "${WORK}/release.tar.gz" -C "$APP_DIR" \
    --exclude='.env' --exclude='storage/*' --exclude='bootstrap/cache/*' \
    || _fail_rollback "extract (tar) başarısız"
_step "extract" "ok"

cd "$APP_DIR" || _fail_rollback "app_dir'e cd başarısız"

# 5b) composer — yalnız composer.lock değiştiyse (zaman tasarrufu + risk azalt).
NEW_LOCK_SHA="$(sha256sum composer.lock 2>/dev/null | awk '{print $1}' || echo none)"
if [[ "$OLD_LOCK_SHA" != "$NEW_LOCK_SHA" ]]; then
    # composer.lock DEĞİŞTİ → bağımlılıklar güncellenmeli. composer yoksa eski vendor/ ile
    # yeni kod = yarım-apply (health /up boot edebilir ama gerçek route'lar 500) → skip DEĞİL, rollback.
    command -v composer >/dev/null 2>&1 || _fail_rollback "composer.lock değişti ama composer yok (yarım-apply riski)"
    composer install --no-dev --no-interaction --optimize-autoloader --no-progress \
        || _fail_rollback "composer install başarısız"
    _step "composer" "ok"
else
    _step "composer" "skipped (lock değişmedi)"
fi

# 5c) DB migrasyonları (geri alınamaz olanlar olabilir → DB snapshot bu yüzden ZORUNLU).
php artisan migrate --force || _fail_rollback "migrate --force başarısız"
_step "migrate" "ok"

# 5d) Frontend build (sunucuda; ship edilen .vue kaynağı → public/build).
# npm yoksa eski asset manifest'iyle kalır = bozuk UI (yeni Vue sayfaları 404/boş) → skip DEĞİL, rollback.
command -v npm >/dev/null 2>&1 || _fail_rollback "npm yok — frontend build edilemez (asset manifest eski kalır)"
npm run build || _fail_rollback "npm run build başarısız"
_step "npm_build" "ok"

# 5e) Cache temizle + yeni VERSION yaz.
php artisan optimize:clear || _fail_rollback "optimize:clear başarısız"
printf '%s\n' "$VERSION" > "$APP_DIR/VERSION" || _fail_rollback "VERSION yazılamadı"
_step "optimize_clear" "ok"

# 5f) PHP-FPM reload (opcache + yeni kod aktif).
systemctl reload "$FPM_UNIT" || systemctl restart "$FPM_UNIT" || _fail_rollback "php-fpm reload/restart başarısız"
_step "fpm_reload" "ok"

# ── 6) Health-check — app'i ÖNCE ayağa kaldır (down iken /up 503 verir!) ─────
_restore_up
_step "maintenance_up" "ok"
# Health-check: FPM graceful reload + opcache warm birkaç saniye sürebilir → tek sleep yerine
# ~12sn boyunca 6 kez yokla; ilk 200/302'de çık (yeni worker'lar geldi). Yön GÜVENLİ: süre
# sonunda hâlâ 200/302 değilse rollback (gerçek bozulma). False-negative (reload gecikmesi) elenir.
CODE="000"
for _hc in 1 2 3 4 5 6; do
    sleep 2
    CODE="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 15 "$HEALTH" 2>/dev/null || echo 000)"
    [[ "$CODE" == "200" || "$CODE" == "302" ]] && break
done
# `[[ ... ]] || _health_fail_rollback` → fonksiyon `||` sağında çağrıldığı için
# gövdesinde set -e askıya alınır (rollback yarıda kesilmez).
[[ "$CODE" == "200" || "$CODE" == "302" ]] || _health_fail_rollback "$CODE"
_step "health_check" "ok"

# ── 7) Başarı: eski snapshot'ları prune (3'ten fazlasını sil) + audit ────────
# En yeni 3 snapshot kalır; gerisi silinir.
ls -1dt "${APP_DIR}".bak-* 2>/dev/null | tail -n +4 | xargs -r rm -rf 2>/dev/null || true
# Eski indirilen tarball'ı temizle (disk).
rm -f "${WORK}/release.tar.gz" 2>/dev/null || true

onx_audit "onx-panel-self-update" "applied=$VERSION from=$VERSION_BEFORE snapshot=$SNAP"
_emit true false ""
exit 0
