#!/usr/bin/env bash

scriptversion=2606141410

set -euo pipefail

VERSIONER_DIR="/srv/.versioner"

WATCH_DIRS=()
EXCLUDE_PATHS=()

INCLUDES=(
    # Shell / Unix
    "*.sh"
    "*.bash"
    "*.zsh"
    "*.ksh"
    "*.fish"
    "*.csh"
    "*.tcsh"
    "*.dash"
    "*.profile"
    "*.bashrc"
    "*.zshrc"
    "*.env"
    "*.env.example"

    # Windows / DOS / PowerShell
    "*.bat"
    "*.cmd"
    "*.ps1"
    "*.psm1"
    "*.psd1"
    "*.vbs"
    "*.vbe"
    "*.wsf"
    "*.wsh"

    # Python / Perl / Ruby / Lua / Tcl
    "*.py"
    "*.pyw"
    "*.pyi"
    "*.pl"
    "*.pm"
    "*.t"
    "*.rb"
    "*.rake"
    "*.gemspec"
    "*.lua"
    "*.tcl"

    # PHP / Web
    "*.php"
    "*.phtml"
    "*.php5"
    "*.php7"
    "*.php8"
    "*.inc"
    "*.twig"
    "*.blade.php"

    # JavaScript / TypeScript / Node / Frontend
    "*.js"
    "*.mjs"
    "*.cjs"
    "*.jsx"
    "*.ts"
    "*.tsx"
    "*.vue"
    "*.svelte"
    "*.astro"

    # HTML / CSS / Templates
    "*.html"
    "*.htm"
    "*.xhtml"
    "*.css"
    "*.scss"
    "*.sass"
    "*.less"
    "*.hbs"
    "*.handlebars"
    "*.mustache"
    "*.ejs"
    "*.njk"
    "*.liquid"

    # Java / JVM / Android
    "*.java"
    "*.kt"
    "*.kts"
    "*.groovy"
    "*.gradle"
    "*.scala"
    "*.clj"
    "*.cljs"
    "*.cljc"

    # C / C++ / Objective-C
    "*.c"
    "*.h"
    "*.cc"
    "*.cpp"
    "*.cxx"
    "*.hpp"
    "*.hh"
    "*.hxx"
    "*.m"
    "*.mm"

    # C# / .NET
    "*.cs"
    "*.csx"
    "*.fs"
    "*.fsi"
    "*.fsx"
    "*.vb"
    "*.vbproj"
    "*.csproj"
    "*.fsproj"
    "*.sln"

    # Go / Rust / Swift / Zig / D
    "*.go"
    "*.rs"
    "*.swift"
    "*.zig"
    "*.d"

    # Haskell / OCaml / Erlang / Elixir
    "*.hs"
    "*.lhs"
    "*.ml"
    "*.mli"
    "*.erl"
    "*.hrl"
    "*.ex"
    "*.exs"

    # R / Julia / MATLAB / Statistik
    "*.r"
    "*.R"
    "*.Rmd"
    "*.jl"
    "*.m"

    # SQL / Datenbanken
    "*.sql"
    "*.psql"
    "*.pgsql"
    "*.mysql"
    "*.sqlite"
    "*.ddl"
    "*.dml"

    # Markup / Dokumentation
    "*.txt"
    "*.md"
    "*.markdown"
    "*.rst"
    "*.adoc"
    "*.asciidoc"
    "*.tex"
    "*.bib"

    # Daten / Konfiguration
    "*.json"
    "*.jsonc"
    "*.xml"
    "*.toml"
    "*.yml"
    "*.yaml"
    "*.ini"
    "*.conf"
    "*.cfg"
    "*.config"
    "*.properties"
    "*.props"
    "*.env"
    "*.dotenv"
    "*.cnf"

    # systemd / Linux / Dienste
    "*.service"
    "*.timer"
    "*.socket"
    "*.mount"
    "*.automount"
    "*.target"
    "*.path"
    "*.slice"
    "*.scope"

    # Webserver / Proxy / Infrastruktur
    "*.nginx"
    "*.apache"
    "*.htaccess"
    "*.htpasswd"
    "*.vhost"

    # Docker / Container / Orchestrierung
    "Dockerfile"
    "Dockerfile.*"
    "*.dockerfile"
    "docker-compose.yml"
    "docker-compose.yaml"
    "compose.yml"
    "compose.yaml"
    "*.containerfile"
    "Containerfile"

    # Kubernetes / Helm / Terraform / IaC
    "*.tf"
    "*.tfvars"
    "*.hcl"
    "*.nomad"
    "*.tpl"
    "*.helm"
    "*.kube"
    "*.k8s"

    # Ansible / Salt / Puppet / Chef
    "*.j2"
    "*.jinja"
    "*.jinja2"
    "*.sls"
    "*.pp"
    "*.erb"
    "*.recipe"

    # Make / Build / Projektsteuerung
    "Makefile"
    "makefile"
    "*.mk"
    "GNUmakefile"
    "CMakeLists.txt"
    "*.cmake"
    "*.gradle"
    "build.gradle"
    "settings.gradle"
    "pom.xml"
    "build.xml"
    "ant.xml"

    # Paketmanager / Lock- und Manifestdateien, textbasiert
    "package.json"
    "package-lock.json"
    "pnpm-lock.yaml"
    "yarn.lock"
    "composer.json"
    "composer.lock"
    "requirements.txt"
    "requirements-dev.txt"
    "pyproject.toml"
    "Pipfile"
    "Pipfile.lock"
    "poetry.lock"
    "Gemfile"
    "Gemfile.lock"
    "go.mod"
    "go.sum"
    "Cargo.toml"
    "Cargo.lock"

    # Editor / Tooling / CI
    ".editorconfig"
    ".gitignore"
    ".gitattributes"
    ".dockerignore"
    ".eslintignore"
    ".prettierignore"
    ".prettierrc"
    ".eslintrc"
    ".babelrc"
    ".npmrc"
    ".yarnrc"
    ".nvmrc"
    ".php-cs-fixer.php"
    ".gitlab-ci.yml"
    ".github"
    "*.code-workspace"

    # Cron / Hosts / klassische Linux-Textdateien
    "crontab"
    "*.cron"
    "hosts"
    "fstab"
    "exports"
    "sudoers"
    "*.rules"

    # Sonstige Script-/Spezialsprachen
    "*.awk"
    "*.sed"
    "*.expect"
    "*.exp"
    "*.vim"
    "*.vimrc"
    "*.el"
    "*.lisp"
    "*.cl"
    "*.scm"
    "*.rkt"
    "*.nim"
    "*.nimble"
    "*.cr"
    "*.dart"
    "*.sol"
    "*.proto"
    "*.graphql"
    "*.gql"
    "*.csv"
    "*.tsv"
)

EXCLUDES=(
    # Script-/Cron-Ausgaben
    "cronlog.txt"
    "cron.log"
    "crontab.log"

    # typische Laufzeit-/Statusdateien
    "pid.txt"
    "status.txt"
    "state.txt"
    "last_run.txt"
    "last-run.txt"
    "last_error.txt"
    "last-error.txt"
    "error.txt"
    "errors.txt"

    # Debug-/Trace-Dateien
    "debug.txt"
    "trace.txt"
    "output.txt"
    "stdout.txt"
    "stderr.txt"

    # temporäre Editor-/Merge-Dateien ohne Endungslogik
    "4913"
    "DEADJOE"

    # typische Tool-/System-Dateien
    ".DS_Store"
    "Thumbs.db"
    "desktop.ini"

    # Versioner-nahe Hilfsdateien
    "index.tsv"
    "meta"
)

KEEP_LIST_DEFAULT=5
MAX_PATCH_CHAIN=100
MAX_PATCH_PERCENT=50

FILES_DIR="$VERSIONER_DIR/files"
CURRENT_DIR="$VERSIONER_DIR/current"
STATE_DIR="$VERSIONER_DIR/state"
LOCKFILE="$VERSIONER_DIR/versioner.lock"

mkdir -p "$FILES_DIR" "$CURRENT_DIR" "$STATE_DIR"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

die() {
    echo "Fehler: $*" >&2
    exit 1
}

parse_scan_args() {
    WATCH_DIRS=()

    while (( $# > 0 )); do
        case "$1" in
            -i|--include-dir)
                [[ $# -ge 2 ]] || die "$1 benötigt einen Ordner"
                [[ -n "$2" ]] || die "$1 darf nicht leer sein"
                WATCH_DIRS+=("$2")
                shift 2
                ;;

            -e|--exclude-dir)
                [[ $# -ge 2 ]] || die "$1 benötigt einen ausgeschlossenen Pfadbestandteil wie /cache/"
                [[ -n "$2" ]] || die "$1 darf nicht leer sein"
                [[ "$2" == /*/ ]] || die "$1 muss vorne und hinten / haben, Beispiel: -e /cache/"
                [[ "$2" != "/" ]] || die "$1 darf nicht nur / sein"

                EXCLUDE_PATHS+=("$2")
                shift 2
                ;;

            -h|--help|help)
                usage
                exit 0
                ;;

            *)
                die "Unbekannter scan-Parameter: $1"
                ;;
        esac
    done

    if (( ${#WATCH_DIRS[@]} == 0 )); then
        die "scan benötigt mindestens ein -i DIR"
    fi
}

hash_file() {
    sha256sum "$1" | awk '{print $1}'
}

rel_path() {
    local path="$1"
    path="${path#/}"
    echo "$path"
}

store_dir_for_path() {
    local path="$1"
    echo "$FILES_DIR/$(rel_path "$path")"
}

current_path_for_path() {
    local path="$1"
    echo "$CURRENT_DIR/$(rel_path "$path")"
}

index_file_for_path() {
    local path="$1"
    echo "$(store_dir_for_path "$path")/index.tsv"
}

meta_file_for_path() {
    local path="$1"
    echo "$(store_dir_for_path "$path")/meta"
}

ensure_parent() {
    mkdir -p "$(dirname "$1")"
}

last_version() {
    local index="$1"
    [[ -f "$index" ]] || { echo 0; return; }
    awk -F '\t' 'NF >= 1 {v=$1} END {print v+0}' "$index"
}

last_hash() {
    local index="$1"
    [[ -f "$index" ]] || return 0
    awk -F '\t' 'NF >= 4 {h=$4} END {print h}' "$index"
}

last_type() {
    local index="$1"
    [[ -f "$index" ]] || return 0
    awk -F '\t' 'NF >= 3 {t=$3} END {print t}' "$index"
}

is_excluded() {
    local file="$1"
    local base

    base="$(basename "$file")"

    for pattern in "${EXCLUDES[@]}"; do
        [[ "$base" == $pattern ]] && return 0
    done

    return 1
}

is_path_excluded() {
    local file="$1"
    local exclude
    local path

    path="/${file#/}"

    for exclude in "${EXCLUDE_PATHS[@]}"; do
        [[ -z "$exclude" ]] && continue

        if [[ "$path" == *"$exclude"* ]]; then
            return 0
        fi
    done

    return 1
}

is_included() {
    local file="$1"
    local base

    base="$(basename "$file")"

    for pattern in "${INCLUDES[@]}"; do
        [[ "$base" == $pattern ]] && return 0
    done

    return 1
}

patch_chain_length() {
    local index="$1"
    [[ -f "$index" ]] || { echo 0; return; }

    awk -F '\t' '
        $3 == "full" { c=0 }
        $3 == "patch" { c++ }
        END { print c+0 }
    ' "$index"
}

write_meta() {
    local path="$1"
    local dir
    dir="$(store_dir_for_path "$path")"

    mkdir -p "$dir"

    {
        echo "path=$path"
        echo "created=$(date '+%Y-%m-%d %H:%M:%S')"
    } > "$dir/meta"
}

save_full() {
    local src="$1"
    local reason="${2:-full}"
    save_full_from_file "$src" "$src" "$reason"
}

save_full_from_file() {
    local src="$1"
    local content_file="$2"
    local reason="${3:-full}"

    local dir index version ts hash mode uid gid size artifact

    dir="$(store_dir_for_path "$src")"
    index="$(index_file_for_path "$src")"

    mkdir -p "$dir"
    write_meta "$src"

    version=$(( $(last_version "$index") + 1 ))
    ts="$(date '+%s')"

    hash="$(hash_file "$content_file")"
    mode="$(stat -c '%a' "$content_file")"
    uid="$(stat -c '%u' "$content_file")"
    gid="$(stat -c '%g' "$content_file")"
    size="$(stat -c '%s' "$content_file")"

    artifact="$(printf '%05d-%s.full.gz' "$version" "$ts")"

    gzip -c "$content_file" > "$dir/$artifact"

    printf "%05d\t%s\tfull\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \
        "$version" "$ts" "$hash" "$mode" "$uid" "$gid" "$size" "$artifact" "$reason" >> "$index"

    update_current_from_file "$src" "$content_file"

    log "$src → Version $version als FULL gespeichert ($reason)"
}

save_patch_or_full() {
    local src="$1"
    local current index dir version ts hash mode uid gid size artifact
    local patch_tmp patch_size src_size percent chain
    local snapshot tmp_restore

    current="$(current_path_for_path "$src")"
    index="$(index_file_for_path "$src")"
    dir="$(store_dir_for_path "$src")"

    snapshot="$(mktemp)"

    if ! cp -a "$src" "$snapshot" 2>/dev/null; then
        rm -f "$snapshot"
        return
    fi

    if ! cmp -s "$src" "$snapshot"; then
        log "$src → übersprungen, Datei änderte sich während Snapshot"
        rm -f "$snapshot"
        return
    fi

    if [[ ! -f "$current" || ! -f "$index" ]]; then
        save_full_from_file "$src" "$snapshot" "initial"
        rm -f "$snapshot"
        return
    fi

    hash="$(hash_file "$snapshot")"

    if [[ "$hash" == "$(last_hash "$index")" ]]; then
        rm -f "$snapshot"
        return
    fi

    chain="$(patch_chain_length "$index")"

    if (( chain >= MAX_PATCH_CHAIN )); then
        save_full_from_file "$src" "$snapshot" "patch-chain-limit"
        rm -f "$snapshot"
        return
    fi

    mkdir -p "$dir"
    write_meta "$src"

    version=$(( $(last_version "$index") + 1 ))
    ts="$(date '+%s')"

    mode="$(stat -c '%a' "$snapshot")"
    uid="$(stat -c '%u' "$snapshot")"
    gid="$(stat -c '%g' "$snapshot")"
    size="$(stat -c '%s' "$snapshot")"

    patch_tmp="$(mktemp)"
    diff -u "$current" "$snapshot" > "$patch_tmp" || true

    patch_size="$(stat -c '%s' "$patch_tmp")"
    src_size="$(stat -c '%s' "$snapshot")"

    if (( src_size == 0 )); then
        rm -f "$patch_tmp"
        save_full_from_file "$src" "$snapshot" "empty-file"
        rm -f "$snapshot"
        return
    fi

    percent=$(( patch_size * 100 / src_size ))

    if (( percent > MAX_PATCH_PERCENT )); then
        rm -f "$patch_tmp"
        save_full_from_file "$src" "$snapshot" "patch-too-large-${percent}pct"
        rm -f "$snapshot"
        return
    fi

    artifact="$(printf '%05d-%s.patch.gz' "$version" "$ts")"
    gzip -c "$patch_tmp" > "$dir/$artifact"
    rm -f "$patch_tmp"

    printf "%05d\t%s\tpatch\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \
        "$version" "$ts" "$hash" "$mode" "$uid" "$gid" "$size" "$artifact" "patch" >> "$index"

    tmp_restore="$(mktemp)"

    if build_version "$src" "$version" "$tmp_restore" && diff -q "$snapshot" "$tmp_restore" >/dev/null 2>&1; then
        rm -f "$tmp_restore"
        update_current_from_file "$src" "$snapshot"
        log "$src → Version $version als PATCH gespeichert"
    else
        rm -f "$tmp_restore"
        replace_last_patch_with_full "$src" "$snapshot" "patch-verify-failed"
    fi

    rm -f "$snapshot"
}

save_deleted() {
    local src="$1"
    local index version ts last

    index="$(index_file_for_path "$src")"
    [[ -f "$index" ]] || return 0

    last="$(last_type "$index")"
    [[ "$last" == "deleted" ]] && return 0

    version=$(( $(last_version "$index") + 1 ))
    ts="$(date '+%s')"

    printf "%05d\t%s\tdeleted\t-\t-\t-\t-\t0\t-\tdeleted\n" \
        "$version" "$ts" >> "$index"

    log "$src → Version $version als DELETED markiert"
}

scan_deleted_files() {
    while IFS= read -r -d '' meta; do
        local path

        path="$(grep -a '^path=' "$meta" | cut -d= -f2-)"
        [[ -z "$path" ]] && continue

        if [[ ! -e "$path" ]]; then
            save_deleted "$path"
        fi
    done < <(find "$FILES_DIR" -type f -name meta -print0 2>/dev/null)
}

update_current() {
    local src="$1"
    local current
    current="$(current_path_for_path "$src")"
    ensure_parent "$current"
    cp -a "$src" "$current"
}

find_file_by_name() {
    local query="$1"
    local found=()

    if [[ "$query" = /* && -f "$query" ]]; then
        echo "$query"
        return
    fi

    while IFS= read -r -d '' meta; do
        local path
        path="$(grep -a '^path=' "$meta" | cut -d= -f2-)"
        [[ -z "$path" ]] && continue

        if [[ "$(basename "$path")" == "$query" || "$path" == "$query" ]]; then
            found+=("$path")
        fi
    done < <(find "$FILES_DIR" -type f -name meta -print0 2>/dev/null)

    if (( ${#found[@]} == 0 )); then
        die "Datei nicht gefunden im Versioner: $query"
    fi

    if (( ${#found[@]} > 1 )); then
        echo "Mehrdeutig:" >&2
        printf '  %s\n' "${found[@]}" >&2
        die "Bitte vollständigen Pfad verwenden."
    fi

    echo "${found[0]}"
}

build_version() {
    local path="$1"
    local target_version="$2"
    local out="$3"
    local index dir nearest_full artifact line tmp_patch expected_hash expected_mode expected_uid expected_gid
    local version ts type sha mode uid gid size art reason

    index="$(index_file_for_path "$path")"
    dir="$(store_dir_for_path "$path")"

    [[ -f "$index" ]] || die "Kein Index für $path"

    nearest_full="$(
        awk -F '\t' -v target="$target_version" '
            $1+0 <= target && $3 == "full" { v=$1 }
            END { print v }
        ' "$index"
    )"

    [[ -n "$nearest_full" ]] || die "Keine Full-Version <= $target_version gefunden"

    artifact="$(
        awk -F '\t' -v v="$nearest_full" '$1+0 == v+0 { print $9 }' "$index"
    )"

    gzip -dc "$dir/$artifact" > "$out"

    while IFS=$'\t' read -r version ts type sha mode uid gid size art reason; do
        [[ "$version" =~ ^[0-9]+$ ]] || continue
        (( 10#$version <= 10#$nearest_full )) && continue
        (( 10#$version > target_version )) && break

        if [[ "$type" == "patch" ]]; then
            tmp_patch="$(mktemp)"
            gzip -dc "$dir/$art" > "$tmp_patch"
            patch --quiet "$out" "$tmp_patch"
            rm -f "$tmp_patch"
        elif [[ "$type" == "full" ]]; then
            gzip -dc "$dir/$art" > "$out"
        fi
    done < "$index"

    expected_hash="$(
        awk -F '\t' -v v="$target_version" '$1+0 == v+0 { print $4 }' "$index"
    )"

    [[ -n "$expected_hash" ]] || die "Version $target_version existiert nicht"

    if [[ "$(hash_file "$out")" != "$expected_hash" ]]; then
        die "Restore-Prüfung fehlgeschlagen: Hash passt nicht"
    fi
}

replace_last_patch_with_full() {
    local src="$1"
    local snapshot="$2"
    local reason="${3:-patch-verify-failed}"

    local index dir version ts hash mode uid gid size artifact old_artifact tmp_index

    index="$(index_file_for_path "$src")"
    dir="$(store_dir_for_path "$src")"

    version="$(last_version "$index")"
    ts="$(date '+%s')"
    hash="$(hash_file "$snapshot")"
    mode="$(stat -c '%a' "$snapshot")"
    uid="$(stat -c '%u' "$snapshot")"
    gid="$(stat -c '%g' "$snapshot")"
    size="$(stat -c '%s' "$snapshot")"
    artifact="$(printf '%05d-%s.full.gz' "$version" "$ts")"

    old_artifact="$(
        awk -F '\t' -v v="$version" '$1+0 == v+0 { print $9 }' "$index"
    )"

    [[ -n "$old_artifact" && -f "$dir/$old_artifact" ]] && rm -f "$dir/$old_artifact"

    gzip -c "$snapshot" > "$dir/$artifact"

    tmp_index="$(mktemp)"

    awk -F '\t' -v OFS='\t' -v v="$version" \
        -v ts="$ts" \
        -v hash="$hash" \
        -v mode="$mode" \
        -v uid="$uid" \
        -v gid="$gid" \
        -v size="$size" \
        -v artifact="$artifact" \
        -v reason="$reason" '
        $1+0 == v+0 {
            print sprintf("%05d", v), ts, "full", hash, mode, uid, gid, size, artifact, reason
            next
        }
        { print }
    ' "$index" > "$tmp_index"

    mv "$tmp_index" "$index"

    update_current_from_file "$src" "$snapshot"

    log "$src → Version $version als FULL ersetzt ($reason)"
}

update_current_from_file() {
    local src="$1"
    local snapshot="$2"
    local current

    current="$(current_path_for_path "$src")"
    ensure_parent "$current"
    cp -a "$snapshot" "$current"
}

restore_version() {
    local query="$1"
    local version="$2"
    local path tmp backup mode uid gid type

    path="$(find_file_by_name "$query")"

        type="$(
        awk -F '\t' -v v="$version" '$1+0 == v+0 { print $3 }' "$(index_file_for_path "$path")"
    )"

    if [[ "$type" == "deleted" ]]; then
        backup="${path}.versioner-backup-$(date '+%Y%m%d-%H%M%S')"

        if [[ -f "$path" ]]; then
            cp -a "$path" "$backup"
            rm -f "$path"
            log "$path wurde auf Version $version zurückgesetzt: gelöscht"
            log "Vorherige Datei liegt als Backup hier: $backup"
        else
            log "$path ist bereits gelöscht"
        fi

        return
    fi

    tmp="$(mktemp)"

    build_version "$path" "$version" "$tmp"

    mode="$(
        awk -F '\t' -v v="$version" '$1+0 == v+0 { print $5 }' "$(index_file_for_path "$path")"
    )"
    uid="$(
        awk -F '\t' -v v="$version" '$1+0 == v+0 { print $6 }' "$(index_file_for_path "$path")"
    )"
    gid="$(
        awk -F '\t' -v v="$version" '$1+0 == v+0 { print $7 }' "$(index_file_for_path "$path")"
    )"

    backup="${path}.versioner-backup-$(date '+%Y%m%d-%H%M%S')"

    if [[ -f "$path" ]]; then
        cp -a "$path" "$backup"
    fi

    cp "$tmp" "$path"
    chmod "$mode" "$path" 2>/dev/null || true
    chown "$uid:$gid" "$path" 2>/dev/null || true

    rm -f "$tmp"

    log "$path wurde auf Version $version zurückgesetzt"
    [[ -f "$backup" ]] && log "Vorherige Datei liegt als Backup hier: $backup"
}

list_versions() {
    local query="$1"
    local count="${2:-$KEEP_LIST_DEFAULT}"
    local path index

    path="$(find_file_by_name "$query")"
    index="$(index_file_for_path "$path")"

    echo "$path"
    echo

    tail -n "$count" "$index" | while IFS=$'\t' read -r version ts type sha mode uid gid size artifact reason; do
        printf "v%d | %s | %s | %s | %s Bytes | %s\n" \
            "$((10#$version))" \
            "$(date -d "@$ts" '+%Y-%m-%d %H:%M:%S')" \
            "$type" \
            "$sha" \
            "$size" \
            "$reason"
    done
}

verify_file() {
    local path="$1"
    local index latest latest_type tmp snapshot

    index="$(index_file_for_path "$path")"
    [[ -f "$index" ]] || return 0
    [[ -f "$path" ]] || return 0

    latest="$(last_version "$index")"
    (( latest > 0 )) || return 0

    latest_type="$(last_type "$index")"

    tmp="$(mktemp)"
    snapshot="$(mktemp)"
    cp -a "$path" "$snapshot"

    if build_version "$path" "$latest" "$tmp" && diff -q "$snapshot" "$tmp" >/dev/null 2>&1; then
        rm -f "$tmp" "$snapshot"
        return 0
    fi

    rm -f "$tmp"

    if [[ "$latest_type" == "patch" ]]; then
        replace_last_patch_with_full "$path" "$snapshot" "verify-replaced-broken-patch"
    else
        save_full_from_file "$path" "$snapshot" "verify-repair-full"
    fi

    rm -f "$snapshot"
}

verify_all() {
    while IFS= read -r -d '' meta; do
        local path
        path="$(grep -a '^path=' "$meta" | cut -d= -f2-)"
        [[ -f "$path" ]] || continue
        verify_file "$path"
    done < <(find "$FILES_DIR" -type f -name meta -print0 2>/dev/null)

    log "Verify abgeschlossen"
}

collect_candidates() {
    local dir file

    for dir in "${WATCH_DIRS[@]}"; do
        [[ -d "$dir" ]] || continue

        find "$dir" \
            -path "$VERSIONER_DIR" -prune -o \
            -type f \
            -print0 2>/dev/null |
        while IFS= read -r -d '' file; do

            if is_excluded "$file"; then
                continue
            fi

            if ! is_included "$file"; then
                continue
            fi

            if [[ ! -r "$file" ]]; then
                continue
            fi

            if is_path_excluded "$file"; then
                continue
            fi

            printf '%s\0' "$file"
        done
    done
}

scan() {
    (( ${#WATCH_DIRS[@]} > 0 )) || die "Keine WATCH_DIRS gesetzt. Nutze: $0 scan -i DIR [-i DIR ...] [-e PATTERN ...]"

    exec 9>"$LOCKFILE"
    flock -n 9 || die "Versioner läuft bereits"

    local file now
    declare -A before

    while IFS= read -r -d '' file; do
        before["$file"]="$(stat -c '%s:%Y' "$file" 2>/dev/null || true)"
    done < <(collect_candidates)

    sleep 2

    for file in "${!before[@]}"; do
        [[ -f "$file" ]] || continue
        [[ -r "$file" ]] || continue

        now="$(stat -c '%s:%Y' "$file" 2>/dev/null || true)"

        if [[ -z "$now" || "${before[$file]}" != "$now" ]]; then
            log "$file → übersprungen, Datei änderte sich gerade"
            continue
        fi

        save_patch_or_full "$file"
    done

    scan_deleted_files
}

usage() {
    cat <<EOF
Nutzung:

  $0 scan -i /srv/scripte -e /exec-log/ -e /cache/ -e /tmp/
      Überwacht / versioniert alle geänderten Dateien in den angegebenen Ordnern.

  $0 DATEI
      Zeigt die letzten 5 Versionen einer Datei.

  $0 DATEI ANZAHL
      Zeigt die letzten ANZAHL Versionen einer Datei.

  $0 restore DATEI VERSION
      Stellt VERSION wieder her und ersetzt die Originaldatei.

  $0 verify
      Prüft alle Restore-Ketten gegen die aktuelle Originaldatei.

Beispiele:

  $0 scan -i /srv/scripte
  $0 scan -i /srv/scripte -i /var/www -i /home/mariobeh/script-data
  $0 scan -i /srv/scripte -e node_modules -e vendor -e cache
  $0 webcamloader.sh
  $0 webcamloader.sh 20
  $0 restore webcamloader.sh 173
  $0 verify
EOF
}

case "${1:-help}" in
    scan)
        shift
        parse_scan_args "$@"
        scan
        ;;
    restore)
        [[ $# -eq 3 ]] || usage
        restore_version "$2" "$3"
        ;;
    verify)
        verify_all
        ;;
    help|-h|--help)
        usage
        ;;
    *)
        if [[ $# -eq 1 ]]; then
            list_versions "$1" "$KEEP_LIST_DEFAULT"
        elif [[ $# -eq 2 ]]; then
            list_versions "$1" "$2"
        else
            usage
            exit 1
        fi
        ;;
esac