<?php
// functions.php
declare(strict_types=1);
require_once __DIR__ . '/config.php';

// Erlaubt ein sicheres Update mit einer älteren, serverspezifischen config.php.
if (!defined('WEBP_QUALITY')) {
    define('WEBP_QUALITY', 85);
}

function ensure_storage(): void {
    if (!is_dir(dirname(DATA_FILE))) {
        @mkdir(dirname(DATA_FILE), 0775, true);
    }
    if (!is_file(DATA_FILE)) {
        file_put_contents(DATA_FILE, json_encode(['events' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }
    if (!is_dir(UPLOAD_DIR)) {
        @mkdir(UPLOAD_DIR, 0775, true);
    }
}

function load_events(): array {
    ensure_storage();
    $raw = file_get_contents(DATA_FILE);
    $data = json_decode($raw ?: '{"events":[]}', true);
    return is_array($data) && isset($data['events']) ? $data['events'] : [];
}

function save_events(array $events): void {
    ensure_storage();
    $json = json_encode(
        ['events' => array_values($events)],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
    );
    $fp = fopen(DATA_FILE, 'c+');
    if ($fp === false) {
        throw new RuntimeException('Kann Datei nicht öffnen: ' . DATA_FILE);
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        throw new RuntimeException('Kann Datenbank nicht sperren: ' . DATA_FILE);
    }

    try {
        if (!ftruncate($fp, 0) || !rewind($fp)) {
            throw new RuntimeException('Kann Datenbank nicht leeren: ' . DATA_FILE);
        }

        $length = strlen($json);
        $offset = 0;
        while ($offset < $length) {
            $written = fwrite($fp, substr($json, $offset));
            if ($written === false || $written === 0) {
                throw new RuntimeException('Kann Datenbank nicht schreiben: ' . DATA_FILE);
            }
            $offset += $written;
        }

        if (!fflush($fp)) {
            throw new RuntimeException('Kann Datenbank nicht speichern: ' . DATA_FILE);
        }
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

function by_datetime_asc(array $a, array $b): int {
    return strtotime($a['date'].' '.$a['time']) <=> strtotime($b['date'].' '.$b['time']);
}
function by_datetime_desc(array $a, array $b): int {
    return strtotime($b['date'].' '.$b['time']) <=> strtotime($a['date'].' '.$a['time']);
}

function now_iso(): string {
    return date('Y-m-d\TH:i:sP');
}

function make_slug(string $title, string $date): string {
    $t = mb_strtolower($title, 'UTF-8');
    $t = preg_replace('~[^\pL\d]+~u', '-', $t);
    $t = trim($t, '-');
    $t = preg_replace('~[^-\w]+~', '', $t);
    $t = preg_replace('~-+~', '-', $t);
    return $t . '-' . $date;
}

function event_path(array $event): string {
    if (!empty($event['slug'])) {
        return '/event.php?slug=' . rawurlencode($event['slug']);
    }
    if (!empty($event['id'])) {
        return '/event.php?id=' . rawurlencode($event['id']);
    }
    return '/event.php';
}

function event_url(array $event): string {
    return rtrim(BASE_URL, '/') . event_path($event);
}

function sanitize_text(string $s): string {
    return trim($s);
}

function is_upcoming(array $event): bool {
    $ts = strtotime($event['date'].' '.$event['time']);
    return $ts >= time();
}

function handle_image_upload(?array $file): ?string {
    if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
        return null;
    }
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Upload-Fehler (Code '.$file['error'].')');
    }

    $sourcePath = (string)($file['tmp_name'] ?? '');
    if ($sourcePath === '' || !is_file($sourcePath)) {
        throw new RuntimeException('Die hochgeladene Bilddatei fehlt.');
    }

    $allowed = ['image/jpeg', 'image/png', 'image/webp'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if ($finfo === false) {
        throw new RuntimeException('Der Dateityp konnte nicht geprüft werden.');
    }
    $mime = finfo_file($finfo, $sourcePath);
    finfo_close($finfo);
    if (!is_string($mime) || !in_array($mime, $allowed, true)) {
        throw new RuntimeException('Nur JPG/PNG/WEBP erlaubt.');
    }

    if (@getimagesize($sourcePath) === false) {
        throw new RuntimeException('Die hochgeladene Datei ist kein gültiges Bild.');
    }

    ensure_storage();
    $uploadToken = 'event_'.date('Ymd_His').'_' . bin2hex(random_bytes(4));
    $basename = $uploadToken . '.webp';
    $uploadDir = rtrim(UPLOAD_DIR, '/\\');
    $stagedSource = $uploadDir . '/.' . $uploadToken . '.upload';
    $stagedWebp = $uploadDir . '/.' . $uploadToken . '.webp.tmp';
    $destination = $uploadDir . '/' . $basename;

    if (!move_uploaded_file($sourcePath, $stagedSource)) {
        throw new RuntimeException('Konnte Datei nicht speichern.');
    }

    try {
        convert_image_to_webp($stagedSource, $mime, $stagedWebp, WEBP_QUALITY);
        $webpSize = is_file($stagedWebp) ? filesize($stagedWebp) : false;
        if ($webpSize === false || $webpSize === 0) {
            throw new RuntimeException('Die WebP-Datei konnte nicht erstellt werden.');
        }
        if (!@rename($stagedWebp, $destination)) {
            throw new RuntimeException('Die WebP-Datei konnte nicht gespeichert werden.');
        }
    } finally {
        if (is_file($stagedSource)) {
            @unlink($stagedSource);
        }
        if (is_file($stagedWebp)) {
            @unlink($stagedWebp);
        }
    }

    return '/uploads/'.$basename; // Pfad relativ zur Webroot
}

/**
 * Findet vorhandene Bilddateien im Upload-Ordner, deren Name nicht auf .webp endet.
 */
function find_non_webp_uploads(): array {
    ensure_storage();
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if ($finfo === false) {
        throw new RuntimeException('Die vorhandenen Uploads konnten nicht geprüft werden.');
    }

    $uploads = [];
    try {
        foreach (new DirectoryIterator(UPLOAD_DIR) as $file) {
            if (!$file->isFile() || $file->isDot()) {
                continue;
            }

            $name = $file->getFilename();
            // Angefangene Uploads und temporäre Konvertierungen niemals als
            // eigenständige Bestandsbilder behandeln.
            if (strncmp($name, '.', 1) === 0) {
                continue;
            }
            if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) === 'webp') {
                continue;
            }

            $mime = finfo_file($finfo, $file->getPathname());
            if (!is_string($mime) || !in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
                continue;
            }
            if (@getimagesize($file->getPathname()) === false) {
                continue;
            }

            $uploads[] = [
                'name' => $name,
                'path' => $file->getPathname(),
                'mime' => $mime,
                'size' => $file->getSize(),
            ];
        }
    } finally {
        finfo_close($finfo);
    }

    usort($uploads, static function (array $left, array $right): int {
        return strcasecmp($left['name'], $right['name']);
    });

    return $uploads;
}

function load_events_for_migration(): array {
    ensure_storage();
    $raw = file_get_contents(DATA_FILE);
    if ($raw === false) {
        throw new RuntimeException('Die Datenbank konnte nicht gelesen werden.');
    }

    try {
        $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    } catch (Throwable $exception) {
        throw new RuntimeException('Die Datenbank enthält ungültiges JSON. Migration wurde abgebrochen.', 0, $exception);
    }

    if (!is_array($data) || !isset($data['events']) || !is_array($data['events'])) {
        throw new RuntimeException('Die Datenbank hat ein ungültiges Format. Migration wurde abgebrochen.');
    }

    return array_values($data['events']);
}

/**
 * Konvertiert genau ein Bestandsbild. Dadurch kann der Adminbereich nach jeder
 * Datei einen echten Fortschritt anzeigen und lange Gateway-Timeouts vermeiden.
 */
function migrate_next_upload_to_webp(): array {
    ensure_storage();
    $lockPath = DATA_FILE . '.webp-migration.lock';
    $lockHandle = fopen($lockPath, 'c');
    if ($lockHandle === false) {
        throw new RuntimeException('Die WebP-Migration konnte nicht gesperrt werden.');
    }
    if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
        fclose($lockHandle);
        throw new RuntimeException('Eine WebP-Migration läuft bereits. Bitte kurz warten.');
    }

    try {
        return migrate_next_upload_to_webp_unlocked();
    } finally {
        flock($lockHandle, LOCK_UN);
        fclose($lockHandle);
    }
}

function migrate_next_upload_to_webp_unlocked(): array {
    $uploads = find_non_webp_uploads();
    if (!$uploads) {
        return [
            'processed' => 0,
            'references' => 0,
            'removed' => 0,
            'remaining_count' => 0,
            'source' => null,
            'target' => null,
            'deletion_failed' => false,
        ];
    }

    $upload = $uploads[0];
    $events = load_events_for_migration();
    $uploadDir = rtrim(UPLOAD_DIR, '/\\');
    $contentHash = hash_file('sha256', $upload['path']);
    if ($contentHash === false) {
        throw new RuntimeException('Prüfsumme fehlgeschlagen: ' . $upload['name']);
    }

    $stem = pathinfo($upload['name'], PATHINFO_FILENAME);
    $stem = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $stem);
    $stem = trim((string)$stem, '-_');
    if ($stem === '') {
        $stem = 'legacy-image';
    }
    $stem = substr($stem, 0, 160);

    $targetName = $stem . '-converted-' . substr($contentHash, 0, 10) . '.webp';
    $targetPath = $uploadDir . '/' . $targetName;
    $temporaryPath = null;
    $targetCreated = false;
    $preserveTargetOnFailure = false;
    $databaseBackup = null;

    try {
        if (is_file($targetPath)) {
            $targetMime = mime_content_type($targetPath);
            if ($targetMime !== 'image/webp' || @getimagesize($targetPath) === false) {
                throw new RuntimeException('Vorhandene Zieldatei ist kein gültiges WebP: ' . $targetName);
            }
        } else {
            $temporaryPath = $uploadDir . '/.' . $targetName . '.' . bin2hex(random_bytes(4)) . '.tmp';
            convert_image_to_webp($upload['path'], $upload['mime'], $temporaryPath, WEBP_QUALITY);

            if (mime_content_type($temporaryPath) !== 'image/webp' || @getimagesize($temporaryPath) === false) {
                throw new RuntimeException('Konvertierung konnte nicht validiert werden: ' . $upload['name']);
            }
            if (!@rename($temporaryPath, $targetPath)) {
                throw new RuntimeException('WebP-Datei konnte nicht gespeichert werden: ' . $targetName);
            }
            $temporaryPath = null;
            $targetCreated = true;
        }

        $updatedReferences = 0;
        foreach ($events as &$event) {
            $image = (string)($event['image'] ?? '');
            if ($image === '') {
                continue;
            }

            $path = parse_url($image, PHP_URL_PATH);
            $sourceName = rawurldecode(basename(is_string($path) ? $path : $image));
            if ($sourceName === $upload['name']) {
                $event['image'] = '/uploads/' . $targetName;
                $updatedReferences++;
            }
        }
        unset($event);

        if ($updatedReferences > 0) {
            $databaseBackup = DATA_FILE . '.migration-' . bin2hex(random_bytes(6)) . '.bak';
            if (!@copy(DATA_FILE, $databaseBackup)) {
                throw new RuntimeException('Die Datenbank konnte vor der Migration nicht gesichert werden.');
            }

            try {
                save_events($events);
            } catch (Throwable $exception) {
                if (!@copy($databaseBackup, DATA_FILE)) {
                    $preserveTargetOnFailure = true;
                    $backupToKeep = $databaseBackup;
                    $databaseBackup = null;
                    throw new RuntimeException(
                        'Datenbankaktualisierung und Wiederherstellung sind fehlgeschlagen. Backup: ' . $backupToKeep,
                        0,
                        $exception
                    );
                }
                throw $exception;
            }
        }
    } catch (Throwable $exception) {
        if ($temporaryPath !== null && is_file($temporaryPath)) {
            @unlink($temporaryPath);
        }
        if ($targetCreated && !$preserveTargetOnFailure && is_file($targetPath)) {
            @unlink($targetPath);
        }
        throw $exception;
    } finally {
        if ($databaseBackup !== null && is_file($databaseBackup)) {
            @unlink($databaseBackup);
        }
    }

    $removed = @unlink($upload['path']) ? 1 : 0;
    $remaining = find_non_webp_uploads();

    return [
        'processed' => 1,
        'references' => $updatedReferences,
        'removed' => $removed,
        'remaining_count' => count($remaining),
        'source' => $upload['name'],
        'target' => $targetName,
        'deletion_failed' => $removed === 0,
    ];
}

/**
 * Kompatibler Komplettlauf für CLI-Aufrufe. Das Admin-Interface verwendet
 * migrate_next_upload_to_webp(), damit jeder Schritt separat bestätigt wird.
 */
function migrate_uploads_to_webp(): array {
    $processed = 0;
    $references = 0;
    $removed = 0;

    do {
        $step = migrate_next_upload_to_webp();
        $processed += $step['processed'];
        $references += $step['references'];
        $removed += $step['removed'];
    } while ($step['processed'] > 0 && !$step['deletion_failed'] && $step['remaining_count'] > 0);

    $remaining = array_map(
        static function (array $upload): string {
            return $upload['name'];
        },
        find_non_webp_uploads()
    );

    return [
        'processed' => $processed,
        'references' => $references,
        'removed' => $removed,
        'remaining' => $remaining,
    ];
}

/**
 * Konvertiert ein geprüftes JPG-, PNG- oder WebP-Bild in eine WebP-Datei.
 * Unterstützt GD mit WebP-Support sowie Imagick als Alternative.
 */
function convert_image_to_webp(string $source, string $mime, string $destination, int $quality = 85): void {
    $quality = max(0, min(100, $quality));
    $gdLoaders = [
        'image/jpeg' => 'imagecreatefromjpeg',
        'image/png' => 'imagecreatefrompng',
        'image/webp' => 'imagecreatefromwebp',
    ];
    $gdLoader = $gdLoaders[$mime] ?? null;

    if ($gdLoader !== null && function_exists($gdLoader) && function_exists('imagewebp')) {
        convert_image_to_webp_with_gd($source, $mime, $destination, $quality, $gdLoader);
        return;
    }

    if (class_exists('Imagick')) {
        convert_image_to_webp_with_imagick($source, $destination, $quality);
        return;
    }

    throw new RuntimeException(
        'WebP-Konvertierung ist auf dem Server nicht verfügbar. Bitte PHP-GD mit WebP-Support oder Imagick aktivieren.'
    );
}

function convert_image_to_webp_with_gd(
    string $source,
    string $mime,
    string $destination,
    int $quality,
    string $loader
): void {
    $image = @$loader($source);
    if ($image === false) {
        throw new RuntimeException('Das Bild konnte nicht gelesen werden.');
    }

    try {
        if ($mime === 'image/jpeg') {
            $image = apply_jpeg_exif_orientation($image, $source);
        }

        if (function_exists('imagepalettetotruecolor')
            && function_exists('imageistruecolor')
            && !imageistruecolor($image)
        ) {
            imagepalettetotruecolor($image);
        }
        imagealphablending($image, true);
        imagesavealpha($image, true);

        if (!@imagewebp($image, $destination, $quality)) {
            throw new RuntimeException('Das Bild konnte nicht als WebP gespeichert werden.');
        }
    } finally {
        imagedestroy($image);
    }
}

function apply_jpeg_exif_orientation($image, string $source) {
    if (!function_exists('exif_read_data')) {
        return $image;
    }

    $exif = @exif_read_data($source, 'IFD0', true);
    $orientation = (int)($exif['IFD0']['Orientation'] ?? $exif['Orientation'] ?? 1);

    if (in_array($orientation, [2, 4, 5, 7], true)) {
        $flipMode = in_array($orientation, [2, 5, 7], true) ? IMG_FLIP_HORIZONTAL : IMG_FLIP_VERTICAL;
        if (!imageflip($image, $flipMode)) {
            throw new RuntimeException('Die Bildausrichtung konnte nicht korrigiert werden.');
        }
    }

    $angle = 0;
    if ($orientation === 3) {
        $angle = 180;
    } elseif (in_array($orientation, [5, 6], true)) {
        $angle = -90;
    } elseif (in_array($orientation, [7, 8], true)) {
        $angle = 90;
    }
    if ($angle === 0) {
        return $image;
    }

    $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127);
    $rotated = imagerotate($image, $angle, $transparent);
    if ($rotated === false) {
        throw new RuntimeException('Die Bildausrichtung konnte nicht korrigiert werden.');
    }
    imagesavealpha($rotated, true);
    imagedestroy($image);

    return $rotated;
}

function convert_image_to_webp_with_imagick(
    string $source,
    string $destination,
    int $quality
): void {
    $sourceImage = new Imagick();
    $image = null;

    try {
        $sourceImage->readImage($source);
        $sourceImage->setIteratorIndex(0);
        $image = $sourceImage->getImage();

        if (method_exists($image, 'autoOrientImage')) {
            $image->autoOrientImage();
        }
        $image->setImageFormat('webp');
        $image->setImageCompressionQuality($quality);
        $image->stripImage();

        if (!$image->writeImage($destination)) {
            throw new RuntimeException('Das Bild konnte nicht als WebP gespeichert werden.');
        }
    } catch (Throwable $exception) {
        if ($exception instanceof RuntimeException) {
            throw $exception;
        }
        throw new RuntimeException('Das Bild konnte nicht in WebP konvertiert werden.', 0, $exception);
    } finally {
        if ($image instanceof Imagick) {
            $image->clear();
            $image->destroy();
        }
        $sourceImage->clear();
        $sourceImage->destroy();
    }
}

function render_event_jsonld_graph(array $events): string {
    $graph = [];
    foreach ($events as $e) {
        $startTime = $e['time'] ?? '19:30';
        $start = $e['date'] . 'T' . $startTime . ':00+02:00'; // Berlin Zeit im Sommer; vereinfacht
        $price = $e['price'] ?? '';
        $priceValue = normalize_price_value($price);
        $validFrom = $e['created_at'] ?? $e['updated_at'] ?? null;
        $endDate = build_event_end_date($e, $startTime);
        $offers = $price !== '' ? array_filter([
            "@type" => "Offer",
            "url" => event_url($e),
            "price" => $priceValue,
            "priceCurrency" => "EUR",
            "availability" => "https://schema.org/InStock",
            "validFrom" => $validFrom
        ]) : null;

        $graph[] = array_filter([
            "@type" => "Event",
            "name" => $e['title'],
            "url" => event_url($e),
            "startDate" => $start,
            "endDate" => $endDate,
            "eventStatus" => "https://schema.org/EventScheduled",
            "eventAttendanceMode" => "https://schema.org/OfflineEventAttendanceMode",
            "performer" => !empty($e['speaker']) ? [
                "@type" => "Person",
                "name" => $e['speaker']
            ] : null,
            "location" => [
                "@type" => "Place",
                "name"  => $e['venue_name'] ?? "Veranstaltungsort",
                "address" => [
                    "@type" => "PostalAddress",
                    "streetAddress" => $e['venue_address'] ?? "",
                    "addressLocality" => "Walschleben",
                    "postalCode" => "99189",
                    "addressCountry" => "DE"
                ],
            ],
            "image" => isset($e['image']) && $e['image'] !== '' ? [ BASE_URL . $e['image'] ] : [ BASE_URL . DEFAULT_OG_IMAGE ],
            "description" => $e['description'] ?? "",
            "organizer" => [
                "@type" => "Organization",
                "name" => SITE_NAME,
                "url"  => BASE_URL
            ],
            "offers" => $offers
        ]);
    }
    return json_encode([
        "@context" => "https://schema.org",
        "@graph"   => $graph
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}

function h(?string $s): string {
    return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function normalize_price_value(string $price): ?string {
    $value = preg_replace('/[^\d,\.]/', '', $price);
    if ($value === '') {
        return null;
    }
    if (substr_count($value, ',') > 0 && substr_count($value, '.') > 0) {
        $value = str_replace('.', '', $value);
        $value = str_replace(',', '.', $value);
    } else {
        $value = str_replace(',', '.', $value);
    }
    return rtrim($value, '.');
}

function build_event_end_date(array $event, string $fallbackTime): ?string {
    $endDate = trim((string)($event['end_date'] ?? ''));
    $endTime = trim((string)($event['end_time'] ?? ''));
    $time = $endTime !== '' ? $endTime : $fallbackTime;
    if ($endDate !== '') {
        return $endDate . 'T' . $time . ':00+02:00';
    }
    // Falls keine Endzeit gepflegt ist, nehmen wir das Startdatum, damit Schema-Validatoren zufrieden sind.
    if (!empty($event['date'])) {
        return $event['date'] . 'T' . $time . ':00+02:00';
    }
    return null;
}

/**
 * Erkennt, ob HTML-Tags enthalten sind.
 */
function has_html_tags(string $s): bool {
    return (bool) preg_match('/<\s*\w+[^>]*>/', $s);
}

/**
 * Erlaubte, sichere HTML-Ausgabe (Whitelist + Attribut-Filter).
 * Erlaubt: p, br, ul/ol/li, b/strong, i/em, u, a, blockquote, h3-h5
 * Entfernt: on* Handler, javascript: URLs, style/iframe/script usw.
 * Fügt bei Links rel="noopener nofollow ugc" hinzu und erlaubt nur http(s).
 */
function render_rich_text(string $input): string {
    $input = trim($input);

    // Fall A: Kein HTML -> wir schützen & konvertieren Zeilenumbrüche
    if (!has_html_tags($input)) {
        return nl2br(h($input));
    }

    // Fall B: HTML vorhanden -> nur erlaubte Tags behalten
    $allowed = '<p><br><ul><ol><li><b><strong><i><em><u><a><blockquote><h3><h4><h5>';
    $html = strip_tags($input, $allowed);

    // Gefährliche Event-Handler entfernen (on*)
    $html = preg_replace('/\s+on\w+\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $html);

    // style-Attribute entfernen (vermeidet versteckte JS/CSS-Tricks)
    $html = preg_replace('/\s+style\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $html);

    // href bereinigen: nur http/https erlauben, sonst entfernen
    // Außerdem rel/target ergänzen
    $html = preg_replace_callback(
        '/<a\s+([^>]*href\s*=\s*(["\'])(.*?)\2[^>]*)>/i',
        function ($m) {
            $attr = $m[1];
            $url  = $m[3];

            // javascript:, data:, mailto: usw. blocken (nur http/https)
            if (!preg_match('~^https?://~i', $url)) {
                // href wegnehmen
                $attr = preg_replace('/\s*href\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $attr);
                $url  = null;
            }

            // target & rel setzen
            if ($url) {
                // target="_blank"
                if (!preg_match('/\btarget\s*=/i', $attr)) {
                    $attr .= ' target="_blank"';
                }
                // rel ergänzen
                if (preg_match('/\brel\s*=\s*("|\')(.*?)\1/i', $attr, $r)) {
                    $rels = array_map('trim', explode(' ', $r[2] ?: ''));
                    foreach (['noopener','nofollow','ugc'] as $need) {
                        if (!in_array($need, $rels, true)) $rels[] = $need;
                    }
                    $attr = preg_replace('/\brel\s*=\s*("|\')(.*?)\1/i', ' rel="'.implode(' ', $rels).'"', $attr);
                } else {
                    $attr .= ' rel="noopener nofollow ugc"';
                }
            }

            return '<a ' . trim($attr) . '>';
        },
        $html
    );

    // Restliche "href='javascript:…'"-Muster sicherheitshalber neutralisieren
    $html = preg_replace('/href\s*=\s*(["\'])\s*javascript:[^"\']*\1/i', '', $html);

    // Doppelte/überflüssige Leerzeichen in Tags aufräumen
    $html = preg_replace('/\s{2,}/', ' ', $html);

    return $html;
}
