<?php
/**
 * WP Multi – Modul: Gast-Autoren
 *
 * Automatisch aus wp-multi.php ausgelagert. Wird über den Loader in
 * wp-multi.php eingebunden (nur wenn das WP Multi Toolkit aktiv ist).
 */

if (!defined('ABSPATH')) { exit; }

/*
* Gast Autoren
*/

// Gast-Autor Eingabefeld in der Sidebar im Admin-Bereich hinzufügen
function wp_multi_add_guest_author_field() {
    add_meta_box(
        'guest_author_meta_box',
        __('Gast-Autor', 'wp-multi'),
        'wp_multi_guest_author_field',
        ['post', 'page', 'dein_custom_post_type'], 
        'side',
        'high'
    );
}
add_action('add_meta_boxes', 'wp_multi_add_guest_author_field');

// Callback-Funktion, die das Eingabefeld anzeigt
function wp_multi_guest_author_field($post) {
    $guest_author = get_post_meta($post->ID, '_guest_author', true);
    wp_nonce_field('wp_multi_guest_author_nonce', 'wp_multi_guest_author_nonce_field');
    ?>
    <label for="guest_author"><?php _e('Gast-Autor Name:', 'wp-multi'); ?></label>
    <input type="text" id="guest_author" name="guest_author" value="<?php echo esc_attr($guest_author); ?>" class="widefat" />
    <?php
}

// Speichern der Gast-Autor Daten
function wp_multi_save_guest_author_meta($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    if (!isset($_POST['wp_multi_guest_author_nonce_field']) || !wp_verify_nonce($_POST['wp_multi_guest_author_nonce_field'], 'wp_multi_guest_author_nonce')) return;
    if (!current_user_can('edit_post', $post_id)) return;
    if (isset($_POST['guest_author'])) {
        $guest_author = sanitize_text_field(wp_unslash($_POST['guest_author']));
        update_post_meta($post_id, '_guest_author', $guest_author);
    } else {
        delete_post_meta($post_id, '_guest_author');
    }
}
add_action('save_post', 'wp_multi_save_guest_author_meta');

// Gast-Autor anzeigen anstelle des regulären Autors im Frontend
function wp_multi_display_guest_author($author_name) {
    if ((is_single() || is_archive() || is_home()) && !is_admin()) {  
        $post = get_post();
        if ($post) {
            $guest_author = get_post_meta($post->ID, '_guest_author', true);
            if (!empty($guest_author)) {
                $author_name = $guest_author;
            }
        }
    }
    return $author_name;
}
add_filter('the_author', 'wp_multi_display_guest_author');

// Anzeige des Gast-Autors in der Beitragsübersicht (Backend)
function wp_multi_add_guest_author_column($columns) {
    if (isset($columns['author'])) {
        $columns['guest_author'] = __('Gast-Autor', 'wp-multi');
    }
    return $columns;
}
add_filter('manage_posts_columns', 'wp_multi_add_guest_author_column');

function wp_multi_display_guest_author_column($column_name, $post_id) {
    if ($column_name == 'guest_author') {
        $guest_author = get_post_meta($post_id, '_guest_author', true);
        echo !empty($guest_author) ? esc_html($guest_author) : __('Kein Gast-Autor', 'wp-multi');
    }
}
add_action('manage_posts_custom_column', 'wp_multi_display_guest_author_column', 10, 2);

// Der Gast-Autoren-Menüpunkt wird zentral in wp_multi_register_admin_menus() registriert.

/**
 * Ermittelt den Aktivitätsstatus eines Gast-Autors anhand seiner letzten Geschichte.
 *
 * 🟢 Aktiv                    – letzte Geschichte innerhalb der letzten 60 Tage
 * 🟡 Inaktiv                  – letzte Geschichte 61–180 Tage her
 * 🔴 Stark inaktiv            – letzte Geschichte länger als 180 Tage her
 * ⭐ Ehemaliger Vielschreiber – mindestens 10 Geschichten, aber seit über 180 Tagen inaktiv
 *
 * Der Rang steigt mit dem "Rückhol-Potenzial": absteigend sortiert stehen die
 * ehemaligen Vielschreiber ganz oben, die aktiven Autoren ganz unten.
 *
 * @param string $last_post_date Datum der letzten Geschichte (MySQL-Format, Seitenzeit).
 * @param int    $post_count     Anzahl veröffentlichter Geschichten.
 * @return array|null Array mit key, emoji, label, class, rank und days – oder null ohne Datum.
 */
function wp_multi_get_guest_author_activity($last_post_date, $post_count = 0) {
    if (empty($last_post_date)) {
        return null;
    }

    // Über GMT rechnen, damit die Zeitzone der Seite das Ergebnis nicht verschiebt
    $last_ts = strtotime(get_gmt_from_date($last_post_date));
    if (!$last_ts) {
        return null;
    }

    $days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS);

    if ($days <= 60) {
        $status = ['key' => 'active', 'emoji' => '🟢', 'label' => __('Aktiv', 'wp-multi'), 'class' => 'wpm-badge--success', 'rank' => 1];
    } elseif ($days <= 180) {
        $status = ['key' => 'inactive', 'emoji' => '🟡', 'label' => __('Inaktiv', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2];
    } elseif ((int) $post_count >= 10) {
        $status = ['key' => 'former_prolific', 'emoji' => '⭐', 'label' => __('Ehemaliger Vielschreiber', 'wp-multi'), 'class' => 'wpm-badge--star', 'rank' => 4];
    } else {
        $status = ['key' => 'dormant', 'emoji' => '🔴', 'label' => __('Stark inaktiv', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 3];
    }

    $status['days'] = $days;

    return $status;
}

/**
 * Liefert die Beschriftungen aller Aktivitätsstatus für Filter und Legende.
 */
function wp_multi_get_guest_author_status_labels() {
    return [
        'former_prolific' => '⭐ ' . __('Ehemaliger Vielschreiber', 'wp-multi'),
        'dormant'         => '🔴 ' . __('Stark inaktiv', 'wp-multi'),
        'inactive'        => '🟡 ' . __('Inaktiv', 'wp-multi'),
        'active'          => '🟢 ' . __('Aktiv', 'wp-multi'),
    ];
}

/**
 * Liefert die Rangleiter der Autoren, absteigend nach Schwelle.
 *
 * 🎉 Newcomer ab 5 – ⭐ Vielschreiber ab 10 – 🏆 Etablierter Autor ab 25 –
 * 👑 Star-Autor ab 50 – 💎 Legende ab 100 Geschichten.
 *
 * @return array Zuordnung "Schwelle => [emoji, Bezeichnung]".
 */
function wp_multi_get_guest_author_ranks() {
    return [
        100 => ['💎', __('Legende', 'wp-multi')],
        50  => ['👑', __('Star-Autor', 'wp-multi')],
        25  => ['🏆', __('Etablierter Autor', 'wp-multi')],
        10  => ['⭐', __('Vielschreiber', 'wp-multi')],
        5   => ['🎉', __('Newcomer', 'wp-multi')],
    ];
}

/**
 * Ermittelt den höchsten erreichten Rang eines Autors.
 *
 * Es wird immer nur der höchste erreichte Rang zurückgegeben, nie mehrere.
 *
 * @param int $post_count Anzahl veröffentlichter Geschichten.
 * @return array|null Array mit threshold, emoji, label und label_full – oder null ohne Rang.
 */
function wp_multi_get_guest_author_milestone($post_count) {
    $post_count = (int) $post_count;

    foreach (wp_multi_get_guest_author_ranks() as $threshold => $rank) {
        if ($post_count >= $threshold) {
            return [
                'threshold'  => $threshold,
                'emoji'      => $rank[0],
                'label'      => $rank[1],
                /* translators: 1: Rangbezeichnung, 2: Schwelle des Rangs */
                'label_full' => sprintf(__('%1$s (ab %2$d Geschichten)', 'wp-multi'), $rank[1], $threshold),
            ];
        }
    }

    return null;
}

/**
 * Ermittelt den nächsten noch nicht erreichten Rang.
 *
 * @param int $post_count Anzahl veröffentlichter Geschichten.
 * @return array|null Array mit threshold, emoji, label und missing – oder null nach dem letzten Rang.
 */
function wp_multi_get_guest_author_next_milestone($post_count) {
    $post_count = (int) $post_count;

    // Aufsteigend, damit der nächste offene Rang zuerst greift
    $ranks = array_reverse(wp_multi_get_guest_author_ranks(), true);

    foreach ($ranks as $threshold => $rank) {
        if ($post_count < $threshold) {
            return [
                'threshold' => $threshold,
                'emoji'     => $rank[0],
                'label'     => $rank[1],
                'missing'   => $threshold - $post_count,
            ];
        }
    }

    return null;
}

/**
 * Zählt die veröffentlichten Beiträge eines Gast-Autors.
 *
 * Nutzt dieselbe Regel wie die Autoren-Übersicht, damit Übersicht und
 * Autorenprofil nie unterschiedliche Zahlen anzeigen.
 *
 * @param string $guest_author Name des Gast-Autors.
 * @return int
 */
function wp_multi_get_guest_author_post_count($guest_author) {
    if (empty($guest_author)) {
        return 0;
    }

    global $wpdb;

    return (int) $wpdb->get_var($wpdb->prepare("
        SELECT COUNT(*)
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
        WHERE pm.meta_key = '_guest_author'
        AND pm.meta_value = %s
        AND p.post_status = 'publish'
    ", $guest_author));
}

/**
 * Liefert die Aufrufe je Gast-Autor aus der Analytics-Tabelle.
 *
 * Gezählt werden alle Besucher (siehe wp_multi_post_view_activity()); Bots sind
 * ausgenommen und ein Besucher zählt je Beitrag höchstens einmal pro halbe Stunde.
 *
 * @return array|null Zuordnung "Autorenname => Aufrufe" oder null ohne Analytics-Tabelle.
 */
function wp_multi_get_guest_author_views() {
    if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
        return null;
    }

    global $wpdb;
    $table = WP_MULTI_ANALYTICS_TABLE;

    if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
        return null;
    }

    $results = $wpdb->get_results("
        SELECT pm.meta_value AS guest_author, COUNT(*) AS views
        FROM {$table} a
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author'
        WHERE a.action = 'view'
        AND pm.meta_value != ''
        GROUP BY pm.meta_value
    ");

    $views = [];
    foreach ((array) $results as $row) {
        $views[$row->guest_author] = (int) $row->views;
    }

    return $views;
}

/**
 * Liefert die Reiter der Gast-Autoren-Seite.
 */
function wp_multi_get_guest_author_tabs() {
    return [
        'authors' => __('Gast-Autoren', 'wp-multi'),
        'series'  => __('Liegengebliebene Serien', 'wp-multi'),
        'views'   => __('Aufrufe je Geschichte', 'wp-multi'),
    ];
}

// Callback-Funktion für die Gast-Autor-Übersicht
function wp_multi_guest_author_overview_page() {
    $tabs = wp_multi_get_guest_author_tabs();
    $current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'authors';
    if (!isset($tabs[$current_tab])) {
        $current_tab = 'authors';
    }

    $base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php'));

    wpmt_admin_page_open(
        __('Gast-Autoren', 'wp-multi'),
        __('Alle Gast-Autoren mit Anzahl ihrer veröffentlichten Beiträge und ihrem Aktivitätsstatus', 'wp-multi')
    );
    ?>
    <h2 class="nav-tab-wrapper wpm-tabs">
        <?php foreach ($tabs as $tab_key => $tab_label) : ?>
            <a href="<?php echo esc_url(add_query_arg('tab', $tab_key, $base_url)); ?>"
               class="nav-tab<?php echo ($current_tab === $tab_key) ? ' nav-tab-active' : ''; ?>">
                <?php echo esc_html($tab_label); ?>
            </a>
        <?php endforeach; ?>
    </h2>
    <?php

    $detail_author = isset($_GET['guest_author']) ? sanitize_text_field(wp_unslash($_GET['guest_author'])) : '';

    if ($detail_author !== '') {
        wp_multi_render_guest_author_detail($base_url, $detail_author, $current_tab);
    } elseif ($current_tab === 'series') {
        // Ein Fehler in der Serien-Erkennung darf den Rest der Seite nicht mitreißen
        try {
            wp_multi_render_stalled_series($base_url);
        } catch (Throwable $e) {
            wpmt_admin_card_open(__('Liegengebliebene Serien', 'wp-multi'));
            echo '<p class="wpm-muted">' . esc_html__('Die Serien-Erkennung konnte nicht ausgeführt werden.', 'wp-multi') . '</p>';
            wpmt_admin_card_close();
        }
    } elseif ($current_tab === 'views') {
        wp_multi_render_post_views_table($base_url);
    } else {
        wp_multi_render_guest_author_table($base_url);
    }

    wpmt_admin_page_close();
}

/**
 * Rendert die Aufrufe einzelner Geschichten mit dem Datum des letzten Aufrufs.
 *
 * @param string $base_url Basis-URL der Übersichtsseite.
 */
function wp_multi_render_post_views_table($base_url) {
    wpmt_admin_card_open(__('Aufrufe je Geschichte', 'wp-multi'));

    if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
        echo '<p class="wpm-muted">' . esc_html__('Das Analytics-Modul ist nicht aktiv, deshalb liegen keine Aufrufe vor.', 'wp-multi') . '</p>';
        wpmt_admin_card_close();
        return;
    }

    global $wpdb;
    $table = WP_MULTI_ANALYTICS_TABLE;

    if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
        echo '<p class="wpm-muted">' . esc_html__('Die Analytics-Tabelle existiert noch nicht. Sie wird bei der Aktivierung des Plugins angelegt.', 'wp-multi') . '</p>';
        wpmt_admin_card_close();
        return;
    }

    // Sortierparameter aus URL lesen (Whitelist gegen SQL-Injection)
    $valid_orderby = ['views' => 'views', 'last_view' => 'last_view', 'title' => 'p.post_title'];
    // Standard: zuletzt gelesene Geschichten oben
    $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'last_view';
    if (!isset($valid_orderby[$orderby])) {
        $orderby = 'last_view';
    }
    $order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC';
    $sql_orderby = $valid_orderby[$orderby];

    // Lange Listen bremsen die Seite aus – Anzahl per Filter anpassbar
    $limit = (int) apply_filters('wp_multi_post_views_limit', 100);
    $sort_url = add_query_arg('tab', 'views', $base_url);

    $posts = $wpdb->get_results($wpdb->prepare("
        SELECT a.post_id, p.post_title, p.post_status,
               COUNT(*) AS views,
               MAX(a.timestamp) AS last_view,
               pm.meta_value AS guest_author
        FROM {$table} a
        INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id
        LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
        WHERE a.action = 'view'
        GROUP BY a.post_id, p.post_title, p.post_status, pm.meta_value
        ORDER BY {$sql_orderby} {$order}, p.post_title ASC
        LIMIT %d
    ", $limit));

    $total_views = 0;
    foreach ((array) $posts as $row) {
        $total_views += (int) $row->views;
    }

    $date_format = get_option('date_format') . ' ' . get_option('time_format');
    ?>
    <p class="wpm-muted">
        <?php
        printf(
            /* translators: 1: Anzahl Geschichten, 2: Summe der Aufrufe */
            esc_html__('%1$s Geschichten mit insgesamt %2$s Aufrufen. Gezählt werden alle Besucher ohne Bots, je Besucher einmal pro halbe Stunde.', 'wp-multi'),
            esc_html(number_format_i18n(count((array) $posts))),
            esc_html(number_format_i18n($total_views))
        );
        ?>
    </p>
    <div class="wpm-table-wrap">
        <table class="wpm-table">
            <thead>
                <tr>
                    <th><?php wp_multi_render_sort_link(__('Geschichte', 'wp-multi'), 'title', $orderby, $order, $sort_url, 'ASC'); ?></th>
                    <th><?php _e('Autor', 'wp-multi'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Aufrufe', 'wp-multi'), 'views', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Letzter Aufruf', 'wp-multi'), 'last_view', $orderby, $order, $sort_url, 'DESC'); ?></th>
                </tr>
            </thead>
            <tbody>
                <?php if ($posts) : ?>
                    <?php foreach ($posts as $row) : ?>
                        <tr>
                            <td>
                                <a href="<?php echo esc_url(get_permalink((int) $row->post_id)); ?>" target="_blank" rel="noopener">
                                    <?php echo esc_html($row->post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?>
                                </a>
                                <?php if ($row->post_status !== 'publish') : ?>
                                    <span class="wpm-muted">(<?php echo esc_html($row->post_status); ?>)</span>
                                <?php endif; ?>
                            </td>
                            <td><?php wp_multi_render_guest_author_link($row->guest_author, $base_url, 'views'); ?></td>
                            <td><span class="wpm-badge"><?php echo esc_html(number_format_i18n($row->views)); ?></span></td>
                            <td>
                                <?php
                                // Die Analytics-Tabelle speichert in GMT, deshalb vor der Ausgabe umrechnen
                                echo esc_html(mysql2date($date_format, get_date_from_gmt($row->last_view)));
                                ?>
                                <span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime($row->last_view)))); ?>)</span>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php else : ?>
                    <tr><td colspan="4"><?php _e('Noch keine Aufrufe erfasst.', 'wp-multi'); ?></td></tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
    <?php if (count((array) $posts) >= $limit) : ?>
        <p class="wpm-muted">
            <?php
            /* translators: %d: Anzahl der angezeigten Geschichten */
            printf(esc_html__('Angezeigt werden die ersten %d Einträge dieser Sortierung.', 'wp-multi'), $limit);
            ?>
        </p>
    <?php endif; ?>
    <?php
    wpmt_admin_card_close();
}

/**
 * Liefert Serien-Kennzahlen je Gast-Autor.
 *
 * Gezählt werden die Teile, die der Autor selbst zu einer Serie beigetragen hat –
 * nicht die Gesamtlänge der Serie. Wer zu einer fremden Serie einen Teil beisteuert,
 * gilt deshalb nicht als Serienautor.
 *
 * @return array|null Zuordnung "Name => ['series_count' => int, 'max_parts' => int]" oder null ohne Serien.
 */
function wp_multi_get_guest_author_series_stats() {
    $source = wp_multi_detect_series_source();

    if (!$source) {
        return null;
    }

    global $wpdb;
    $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';

    $results = $wpdb->get_results($wpdb->prepare("
        SELECT x.guest_author,
               SUM(CASE WHEN x.parts >= 2 THEN 1 ELSE 0 END) AS series_count,
               MAX(x.parts) AS max_parts
        FROM (
            SELECT pm.meta_value AS guest_author, tt.term_id, COUNT(DISTINCT p.ID) AS parts
            FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
            INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
            INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql}
            WHERE p.post_status = 'publish'
            GROUP BY pm.meta_value, tt.term_id
        ) x
        GROUP BY x.guest_author
    ", $source['taxonomy']));

    $stats = [];
    foreach ((array) $results as $row) {
        $stats[$row->guest_author] = [
            'series_count' => (int) $row->series_count,
            'max_parts'    => (int) $row->max_parts,
        ];
    }

    return $stats;
}

/**
 * Durchschnittliche Aufrufe je Geschichte über alle Gast-Autoren hinweg.
 *
 * Maßstab für "gute Aufrufe" in der Reaktivierungsbewertung. Wird einmal je
 * Seitenaufruf berechnet, damit Übersicht und Detailansicht identisch bewerten.
 *
 * @return float
 */
function wp_multi_get_average_guest_author_views() {
    static $average = null;

    if ($average !== null) {
        return $average;
    }

    $table = wp_multi_get_analytics_table();

    if (!$table) {
        $average = 0.0;
        return $average;
    }

    global $wpdb;

    $views = (int) $wpdb->get_var("
        SELECT COUNT(*)
        FROM {$table} a
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
        WHERE a.action = 'view'
    ");

    $posts = (int) $wpdb->get_var("
        SELECT COUNT(*)
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
        WHERE p.post_status = 'publish'
    ");

    $average = $posts ? ($views / $posts) : 0.0;

    return $average;
}

/**
 * Bewertet, wie lohnend es ist, einen Autor zurückzuholen.
 *
 * 🔥 Sehr interessant – ab 10 Geschichten, mindestens ein halbes Jahr still,
 *                       mit einer eigenen Serie von mindestens 3 Teilen
 * ⭐ Interessant      – ab 5 Geschichten, mindestens ein halbes Jahr still,
 *                       und überdurchschnittlich gelesen
 * 🟡 Beobachten       – ab 3 Geschichten und mindestens ein halbes Jahr still
 * ⚪ Keine Priorität  – 1–2 Geschichten, lange still
 * –  Kein Bedarf      – schreibt noch
 *
 * @param array $data       Kennzahlen des Autors (post_count, days, views, series_count, max_parts).
 * @param float $avg_views  Durchschnittliche Aufrufe je Geschichte auf der ganzen Seite.
 * @return array Array mit key, emoji, label, class und rank.
 */
function wp_multi_get_guest_author_reactivation($data, $avg_views = 0) {
    $post_count = isset($data['post_count']) ? (int) $data['post_count'] : 0;
    $days = isset($data['days']) ? (int) $data['days'] : 0;
    $views = isset($data['views']) ? (int) $data['views'] : 0;
    $series_count = isset($data['series_count']) ? (int) $data['series_count'] : 0;
    $max_parts = isset($data['max_parts']) ? (int) $data['max_parts'] : 0;

    $min_days = (int) apply_filters('wp_multi_reactivation_min_days', 180);

    // Wer noch schreibt, muss nicht zurückgeholt werden
    if ($days < $min_days) {
        return ['key' => 'current', 'emoji' => '–', 'label' => __('Kein Bedarf', 'wp-multi'), 'class' => '', 'rank' => 0];
    }

    if ($post_count >= 10 && $series_count > 0 && $max_parts >= 3) {
        return ['key' => 'hot', 'emoji' => '🔥', 'label' => __('Sehr interessant', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 4];
    }

    // "Mehrere Geschichten mit guten Aufrufen": im Schnitt deutlich über dem
    // Seitendurchschnitt gelesen. Ohne Analytics-Daten greift die Regel nicht.
    $good_views_factor = (float) apply_filters('wp_multi_reactivation_views_factor', 1.5);
    $has_good_views = ($avg_views > 0 && $post_count > 0 && ($views / $post_count) >= ($avg_views * $good_views_factor));

    if ($post_count >= 5 && $has_good_views) {
        return ['key' => 'interesting', 'emoji' => '⭐', 'label' => __('Interessant', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 3];
    }

    if ($post_count >= 3) {
        return ['key' => 'watch', 'emoji' => '🟡', 'label' => __('Beobachten', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2];
    }

    return ['key' => 'low', 'emoji' => '⚪', 'label' => __('Keine Priorität', 'wp-multi'), 'class' => '', 'rank' => 1];
}

/**
 * Liefert die Beschriftungen aller Reaktivierungsstufen für den Filter.
 */
function wp_multi_get_reactivation_labels() {
    return [
        'hot'         => '🔥 ' . __('Sehr interessant', 'wp-multi'),
        'interesting' => '⭐ ' . __('Interessant', 'wp-multi'),
        'watch'       => '🟡 ' . __('Beobachten', 'wp-multi'),
        'low'         => '⚪ ' . __('Keine Priorität', 'wp-multi'),
        'current'     => '– ' . __('Kein Bedarf', 'wp-multi'),
    ];
}

/**
 * Gibt eine Filterzeile mit Zählern aus.
 *
 * @param string $title    Beschriftung der Zeile.
 * @param string $param    Name des URL-Parameters.
 * @param array  $labels   Zuordnung "Wert => Beschriftung".
 * @param array  $counts   Zuordnung "Wert => Anzahl".
 * @param string $active   Aktuell aktiver Wert ('' = alle).
 * @param int    $total    Gesamtzahl für "Alle".
 * @param string $link_url Basis-URL für die Links.
 */
function wp_multi_render_filter_row($title, $param, $labels, $counts, $active, $total, $link_url) {
    ?>
    <p class="wpm-muted">
        <?php echo esc_html($title); ?>
        <a href="<?php echo esc_url(remove_query_arg($param, $link_url)); ?>" class="wpm-filter-link<?php echo ($active === '') ? ' wpm-filter-active' : ''; ?>">
            <?php if ($active === '') : ?><strong><?php endif; ?>
            <?php printf(esc_html__('Alle (%d)', 'wp-multi'), (int) $total); ?>
            <?php if ($active === '') : ?></strong><?php endif; ?>
        </a>
        <?php foreach ($labels as $key => $label) : ?>
            |
            <a href="<?php echo esc_url(add_query_arg($param, $key, $link_url)); ?>" class="wpm-filter-link<?php echo ($active === $key) ? ' wpm-filter-active' : ''; ?>">
                <?php if ($active === $key) : ?><strong><?php endif; ?>
                <?php echo esc_html($label); ?> (<?php echo isset($counts[$key]) ? (int) $counts[$key] : 0; ?>)
                <?php if ($active === $key) : ?></strong><?php endif; ?>
            </a>
        <?php endforeach; ?>
    </p>
    <?php
}

/**
 * Rendert die Autorentabelle inklusive Filter und Sortierung.
 *
 * @param string $base_url Basis-URL der Übersichtsseite.
 */
function wp_multi_render_guest_author_table($base_url) {
    // Sortierparameter aus URL lesen
    $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'guest_author';
    $order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'ASC';

    // Statusfilter und Reaktivierungsfilter aus URL lesen ('' = alle anzeigen)
    $status_labels = wp_multi_get_guest_author_status_labels();
    $filter = isset($_GET['activity']) ? sanitize_key($_GET['activity']) : '';
    if (!isset($status_labels[$filter])) {
        $filter = '';
    }

    $reactivation_labels = wp_multi_get_reactivation_labels();
    $potential = isset($_GET['potential']) ? sanitize_key($_GET['potential']) : '';
    if (!isset($reactivation_labels[$potential])) {
        $potential = '';
    }

    // Aufrufe und Serien je Autor – jeweils null, wenn die Quelle fehlt
    $views = wp_multi_get_guest_author_views();
    $show_views = is_array($views);
    $series_stats = wp_multi_get_guest_author_series_stats();
    $show_series = is_array($series_stats);

    // Gültige Sortierfelder. 'status', 'views', 'series' und 'potential' werden in
    // PHP sortiert, da sie nicht aus der Hauptabfrage stammen.
    $valid_orderby = ['guest_author' => 'pm.meta_value', 'posts' => 'post_count', 'date' => 'last_post_date'];
    $php_orderby = ['status', 'views', 'series', 'potential'];
    if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) {
        $orderby = 'guest_author';
    }
    $sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'pm.meta_value';
    $sql_order = $order;

    // URLs für Sortier- und Filter-Links (Reiter, Filter und Sortierung bleiben erhalten)
    $base_url = add_query_arg('tab', 'authors', $base_url);
    $sort_url = $base_url;
    if ($filter) {
        $sort_url = add_query_arg('activity', $filter, $sort_url);
    }
    if ($potential) {
        $sort_url = add_query_arg('potential', $potential, $sort_url);
    }

    $filter_url = add_query_arg(['orderby' => $orderby, 'order' => $order], $base_url);
    $status_url = $potential ? add_query_arg('potential', $potential, $filter_url) : $filter_url;
    $potential_url = $filter ? add_query_arg('activity', $filter, $filter_url) : $filter_url;

    global $wpdb;
    $query = "
        SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
        WHERE pm.meta_key = '_guest_author'
        AND pm.meta_value != ''
        AND p.post_status = 'publish'
        GROUP BY pm.meta_value
        ORDER BY {$sql_orderby} {$sql_order}, pm.meta_value ASC
    ";
    $guest_authors = $wpdb->get_results($query);

    // Durchschnittliche Aufrufe je Geschichte – Maßstab für "gute Aufrufe"
    $avg_views = wp_multi_get_average_guest_author_views();

    // Kennzahlen ergänzen, Autoren je Filterwert zählen und einschränken
    $status_counts = array_fill_keys(array_keys($status_labels), 0);
    $reactivation_counts = array_fill_keys(array_keys($reactivation_labels), 0);
    $rows = [];
    $position = 0;

    foreach ((array) $guest_authors as $author) {
        $author->activity = wp_multi_get_guest_author_activity($author->last_post_date, $author->post_count);
        $author->views = $show_views && isset($views[$author->guest_author]) ? $views[$author->guest_author] : 0;
        $author->series_count = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['series_count'] : 0;
        $author->max_parts = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['max_parts'] : 0;
        $author->position = $position++; // Reihenfolge aus der DB (korrekte Sortierung inkl. Umlauten)

        $author->reactivation = wp_multi_get_guest_author_reactivation([
            'post_count'   => (int) $author->post_count,
            'days'         => $author->activity ? $author->activity['days'] : 0,
            'views'        => $author->views,
            'series_count' => $author->series_count,
            'max_parts'    => $author->max_parts,
        ], $avg_views);

        if ($author->activity) {
            $status_counts[$author->activity['key']]++;
        }
        $reactivation_counts[$author->reactivation['key']]++;

        if ($filter !== '' && (!$author->activity || $author->activity['key'] !== $filter)) {
            continue;
        }
        if ($potential !== '' && $author->reactivation['key'] !== $potential) {
            continue;
        }

        $rows[] = $author;
    }

    // Sortierung nach berechneten Spalten: bei Gleichstand die DB-Reihenfolge behalten
    if (in_array($orderby, $php_orderby, true)) {
        usort($rows, function ($a, $b) use ($orderby, $order) {
            switch ($orderby) {
                case 'views':
                    $value_a = (int) $a->views;
                    $value_b = (int) $b->views;
                    break;
                case 'series':
                    $value_a = (int) $a->series_count;
                    $value_b = (int) $b->series_count;
                    break;
                case 'potential':
                    $value_a = (int) $a->reactivation['rank'];
                    $value_b = (int) $b->reactivation['rank'];
                    break;
                default:
                    $value_a = $a->activity ? $a->activity['rank'] : 0;
                    $value_b = $b->activity ? $b->activity['rank'] : 0;
            }

            if ($value_a === $value_b) {
                return $a->position - $b->position;
            }

            $diff = $value_a - $value_b;

            return ($order === 'DESC') ? -$diff : $diff;
        });
    }

    $colspan = 4 + ($show_views ? 1 : 0) + ($show_series ? 1 : 0);

    wpmt_admin_card_open();

    wp_multi_render_filter_row(
        __('Status:', 'wp-multi'),
        'activity',
        $status_labels,
        $status_counts,
        $filter,
        count((array) $guest_authors),
        $status_url
    );

    wp_multi_render_filter_row(
        __('Reaktivierung:', 'wp-multi'),
        'potential',
        $reactivation_labels,
        $reactivation_counts,
        $potential,
        count((array) $guest_authors),
        $potential_url
    );
    ?>
    <div class="wpm-table-wrap">
        <table class="wpm-table">
            <thead>
                <tr>
                    <th><?php wp_multi_render_sort_link(__('Gast-Autor', 'wp-multi'), 'guest_author', $orderby, $order, $sort_url, 'ASC'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Beiträge', 'wp-multi'), 'posts', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <?php if ($show_series) : ?>
                        <th title="<?php esc_attr_e('Serien mit mindestens zwei eigenen Teilen. Die Länge der längsten steht im Tooltip der Zahl.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Serien', 'wp-multi'), 'series', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <?php endif; ?>
                    <?php if ($show_views) : ?>
                        <th title="<?php esc_attr_e('Aufrufe aller Besucher ohne Bots, je Besucher einmal pro halbe Stunde.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Aufrufe', 'wp-multi'), 'views', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <?php endif; ?>
                    <th title="<?php esc_attr_e('Das Symbol zeigt den Aktivitätsstatus.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Letzte Geschichte', 'wp-multi'), 'date', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Reaktivierung', 'wp-multi'), 'potential', $orderby, $order, $sort_url, 'DESC'); ?></th>
                </tr>
            </thead>
            <tbody>
                <?php if ($rows) : ?>
                    <?php foreach ($rows as $author) : ?>
                        <?php $activity = $author->activity; ?>
                        <tr>
                            <td><?php wp_multi_render_guest_author_link($author->guest_author, $base_url, 'authors'); ?></td>
                            <td><?php echo esc_html(number_format_i18n($author->post_count)); ?></td>
                            <?php if ($show_series) : ?>
                                <td>
                                    <?php if ($author->series_count) : ?>
                                        <span title="<?php printf(esc_attr__('längste Serie: %d Teile', 'wp-multi'), (int) $author->max_parts); ?>"><?php echo esc_html(number_format_i18n($author->series_count)); ?></span>
                                    <?php else : ?>
                                        <span class="wpm-muted">—</span>
                                    <?php endif; ?>
                                </td>
                            <?php endif; ?>
                            <?php if ($show_views) : ?>
                                <td><?php echo esc_html(number_format_i18n($author->views)); ?></td>
                            <?php endif; ?>
                            <td>
                                <?php if ($activity) : ?>
                                    <span title="<?php echo esc_attr($activity['label']); ?>"><?php echo esc_html($activity['emoji']); ?></span>
                                    <?php echo esc_html(mysql2date(get_option('date_format'), $author->last_post_date)); ?>
                                    <span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($author->last_post_date))))); ?>)</span>
                                <?php else : ?>
                                    <span class="wpm-muted">—</span>
                                <?php endif; ?>
                            </td>
                            <td>
                                <?php $reactivation = $author->reactivation; ?>
                                <?php if ($reactivation['key'] === 'hot' || $reactivation['key'] === 'interesting') : ?>
                                    <span class="wpm-badge <?php echo esc_attr($reactivation['class']); ?>">
                                        <?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?>
                                    </span>
                                <?php elseif ($reactivation['key'] === 'watch') : ?>
                                    <span class="wpm-muted"><?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?></span>
                                <?php else : ?>
                                    <span class="wpm-muted" title="<?php echo esc_attr($reactivation['label']); ?>"><?php echo esc_html($reactivation['emoji']); ?></span>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php else : ?>
                    <tr><td colspan="<?php echo (int) $colspan; ?>"><?php _e('Keine Gast-Autoren gefunden.', 'wp-multi'); ?></td></tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
    <?php if ($show_views) : ?>
        <p class="wpm-muted"><?php _e('Aufrufe stammen aus dem Analytics-Modul: alle Besucher, Bots ausgenommen, je Besucher höchstens einmal pro halbe Stunde und Beitrag.', 'wp-multi'); ?></p>
    <?php endif; ?>
    <?php
    wpmt_admin_card_close();
}

/**
 * Baut die URL zur Detailansicht eines Gast-Autors.
 *
 * @param string $base_url Basis-URL der Übersichtsseite.
 * @param string $name     Name des Gast-Autors.
 * @param string $tab      Reiter, aus dem der Aufruf kommt (für den Zurück-Link).
 * @return string
 */
function wp_multi_get_guest_author_detail_url($base_url, $name, $tab = 'authors') {
    return add_query_arg(['tab' => $tab, 'guest_author' => $name], $base_url);
}

/**
 * Gibt einen oder mehrere Autorennamen als Links zur Detailansicht aus.
 *
 * @param string $names    Ein Name oder mehrere, mit ", " verbunden.
 * @param string $base_url Basis-URL der Übersichtsseite.
 * @param string $tab      Aktueller Reiter.
 * @param bool   $split    Bei true wird an ", " getrennt (GROUP_CONCAT-Listen).
 */
function wp_multi_render_guest_author_link($names, $base_url, $tab = 'authors', $split = false) {
    if ($names === null || $names === '') {
        echo '<span class="wpm-muted">—</span>';
        return;
    }

    $list = $split ? array_filter(array_map('trim', explode(',', $names))) : [$names];
    $links = [];

    foreach ($list as $name) {
        $links[] = '<a href="' . esc_url(wp_multi_get_guest_author_detail_url($base_url, $name, $tab)) . '">' . esc_html($name) . '</a>';
    }

    echo implode(', ', $links);
}

/**
 * Zeigt alles zu einem Gast-Autor auf einer Seite: Kennzahlen, Serien und
 * Einzelgeschichten. Grundlage für die Entscheidung, wen man kontaktiert.
 *
 * @param string $base_url     Basis-URL der Übersichtsseite.
 * @param string $guest_author Name des Gast-Autors.
 * @param string $tab          Reiter, aus dem der Aufruf kam.
 */
function wp_multi_render_guest_author_detail($base_url, $guest_author, $tab = 'authors') {
    global $wpdb;

    $back_url = add_query_arg('tab', $tab, $base_url);

    // Kennzahlen des Autors
    $summary = $wpdb->get_row($wpdb->prepare("
        SELECT COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
        WHERE pm.meta_key = '_guest_author'
        AND pm.meta_value = %s
        AND p.post_status = 'publish'
    ", $guest_author));

    // Der Name stammt aus einem Meta-Feld und wird deshalb selbst escaped
    // ausgegeben, statt ihn als Kartentitel durchzureichen.
    wpmt_admin_card_open(__('Autoren-Details', 'wp-multi'));
    ?>
    <p><a href="<?php echo esc_url($back_url); ?>">&larr; <?php _e('Zurück zur Übersicht', 'wp-multi'); ?></a></p>
    <h2 class="wpm-author-name"><?php echo esc_html($guest_author); ?></h2>
    <?php

    if (!$summary || !$summary->post_count) {
        echo '<p class="wpm-muted">' . esc_html__('Zu diesem Namen wurden keine veröffentlichten Geschichten gefunden.', 'wp-multi') . '</p>';
        wpmt_admin_card_close();
        return;
    }

    $post_count = (int) $summary->post_count;
    $activity = wp_multi_get_guest_author_activity($summary->last_post_date, $post_count);
    $milestone = wp_multi_get_guest_author_milestone($post_count);

    // Gesamtaufrufe aller Geschichten dieses Autors
    $analytics_table = wp_multi_get_analytics_table();
    $total_views = null;

    if ($analytics_table) {
        $total_views = (int) $wpdb->get_var($wpdb->prepare("
            SELECT COUNT(*)
            FROM {$analytics_table} a
            INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author'
            WHERE a.action = 'view'
            AND pm.meta_value = %s
        ", $guest_author));
    }
    ?>
    <ul class="wpm-author-facts">
        <li>
            <strong><?php echo esc_html(number_format_i18n($post_count)); ?></strong>
            <?php echo esc_html(_n('Geschichte', 'Geschichten', $post_count, 'wp-multi')); ?>
            <?php if ($milestone) : ?>
                <span title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
            <?php endif; ?>
        </li>
        <li>
            <?php _e('Letzte Veröffentlichung:', 'wp-multi'); ?>
            <strong><?php echo esc_html(mysql2date(get_option('date_format'), $summary->last_post_date)); ?></strong>
            <span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($summary->last_post_date))))); ?>)</span>
        </li>
        <?php if ($activity) : ?>
            <li>
                <?php _e('Status:', 'wp-multi'); ?>
                <span class="wpm-badge <?php echo esc_attr($activity['class']); ?>"><?php echo esc_html($activity['emoji'] . ' ' . $activity['label']); ?></span>
            </li>
        <?php endif; ?>
        <?php if ($total_views !== null) : ?>
            <li><?php _e('Gesamtaufrufe:', 'wp-multi'); ?> <strong><?php echo esc_html(number_format_i18n($total_views)); ?></strong></li>
        <?php endif; ?>
        <?php
        // Reaktivierungsbewertung mit denselben Kennzahlen wie in der Übersicht
        $series_stats = wp_multi_get_guest_author_series_stats();
        $author_series = (is_array($series_stats) && isset($series_stats[$guest_author])) ? $series_stats[$guest_author] : ['series_count' => 0, 'max_parts' => 0];

        $reactivation = wp_multi_get_guest_author_reactivation([
            'post_count'   => $post_count,
            'days'         => $activity ? $activity['days'] : 0,
            'views'        => (int) $total_views,
            'series_count' => $author_series['series_count'],
            'max_parts'    => $author_series['max_parts'],
        ], wp_multi_get_average_guest_author_views());
        ?>
        <li>
            <?php _e('Reaktivierung:', 'wp-multi'); ?>
            <?php if ($reactivation['key'] === 'current') : ?>
                <span class="wpm-muted"><?php echo esc_html($reactivation['label']); ?></span>
            <?php else : ?>
                <span class="wpm-badge <?php echo esc_attr($reactivation['class']); ?>"><?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?></span>
            <?php endif; ?>
        </li>
    </ul>
    <?php

    // Serien dieses Autors
    $source = wp_multi_detect_series_source();
    $series_post_ids = [];

    if ($source) {
        $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';

        $series = $wpdb->get_results($wpdb->prepare("
            SELECT t.term_id, t.name AS series_name,
                   COUNT(DISTINCT p.ID) AS author_parts,
                   MAX(p.post_date) AS last_part_date
            FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s
            INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
            INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql}
            INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
            WHERE p.post_status = 'publish'
            GROUP BY t.term_id, t.name
            ORDER BY last_part_date DESC
        ", $guest_author, $source['taxonomy']));

        if ($series) {
            // Gesamtteile der Serie (auch von anderen Autoren) und Aufrufe nachladen
            $term_ids = [];
            foreach ($series as $row) {
                $term_ids[] = (int) $row->term_id;
            }
            $term_id_list = implode(',', $term_ids);

            $totals = $wpdb->get_results($wpdb->prepare("
                SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
                INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
                WHERE p.post_status = 'publish'
                AND tt.term_id IN ({$term_id_list})
                GROUP BY tt.term_id
            ", $source['taxonomy']));

            $total_parts = [];
            foreach ((array) $totals as $total) {
                $total_parts[(int) $total->term_id] = (int) $total->total_parts;
            }

            wp_multi_add_series_details($series, $source['taxonomy']);

            // Beiträge in Serien merken, damit sie unten nicht doppelt auftauchen
            $ids = $wpdb->get_col($wpdb->prepare("
                SELECT DISTINCT p.ID
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
                INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
                WHERE tt.term_id IN ({$term_id_list})
            ", $source['taxonomy']));

            foreach ((array) $ids as $id) {
                $series_post_ids[] = (int) $id;
            }
            ?>
            <h3><?php _e('Serien', 'wp-multi'); ?></h3>
            <div class="wpm-table-wrap">
                <table class="wpm-table">
                    <thead>
                        <tr>
                            <th><?php _e('Serie', 'wp-multi'); ?></th>
                            <th><?php _e('Teile', 'wp-multi'); ?></th>
                            <th><?php _e('Letzter Teil', 'wp-multi'); ?></th>
                            <?php if ($analytics_table) : ?>
                                <th><?php _e('Aufrufe gesamt', 'wp-multi'); ?></th>
                            <?php endif; ?>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($series as $row) : ?>
                            <?php
                            $term_link = get_term_link((int) $row->term_id, $source['taxonomy']);
                            $parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts;
                            ?>
                            <tr>
                                <td>
                                    <?php if (!is_wp_error($term_link)) : ?>
                                        <a href="<?php echo esc_url($term_link); ?>" target="_blank" rel="noopener"><?php echo esc_html($row->series_name); ?></a>
                                    <?php else : ?>
                                        <?php echo esc_html($row->series_name); ?>
                                    <?php endif; ?>
                                </td>
                                <td>
                                    <span class="wpm-badge"><?php echo (int) $parts; ?></span>
                                    <?php if ((int) $row->author_parts !== (int) $parts) : ?>
                                        <span class="wpm-muted">
                                            <?php
                                            /* translators: %d: Teile dieses Autors */
                                            printf(esc_html__('(davon %d von diesem Autor)', 'wp-multi'), (int) $row->author_parts);
                                            ?>
                                        </span>
                                    <?php endif; ?>
                                </td>
                                <td>
                                    <?php if ($row->last_part_id) : ?>
                                        <a href="<?php echo esc_url(get_permalink($row->last_part_id)); ?>" target="_blank" rel="noopener">
                                            <?php echo esc_html($row->last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
                                        </a><br />
                                    <?php endif; ?>
                                    <span class="wpm-muted">
                                        <?php echo esc_html(mysql2date(get_option('date_format'), $row->last_part_date)); ?>
                                        (<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($row->last_part_date))))); ?>)
                                    </span>
                                </td>
                                <?php if ($analytics_table) : ?>
                                    <td><?php echo esc_html(number_format_i18n($row->series_views)); ?></td>
                                <?php endif; ?>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php
        }
    }

    // Einzelgeschichten außerhalb von Serien
    $exclude_sql = $series_post_ids ? 'AND p.ID NOT IN (' . implode(',', array_map('intval', $series_post_ids)) . ')' : '';
    $limit = (int) apply_filters('wp_multi_author_detail_limit', 100);

    $posts = $wpdb->get_results($wpdb->prepare("
        SELECT p.ID, p.post_title, p.post_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
        WHERE pm.meta_value = %s
        AND p.post_status = 'publish'
        {$exclude_sql}
        ORDER BY p.post_date DESC
        LIMIT %d
    ", $guest_author, $limit));

    // Aufrufe der angezeigten Geschichten
    $post_views = [];
    if ($analytics_table && $posts) {
        $post_ids = [];
        foreach ($posts as $row) {
            $post_ids[] = (int) $row->ID;
        }
        $post_id_list = implode(',', $post_ids);

        $view_rows = $wpdb->get_results("
            SELECT post_id, COUNT(*) AS views
            FROM {$analytics_table}
            WHERE action = 'view'
            AND post_id IN ({$post_id_list})
            GROUP BY post_id
        ");

        foreach ((array) $view_rows as $view_row) {
            $post_views[(int) $view_row->post_id] = (int) $view_row->views;
        }
    }
    ?>
    <h3><?php _e('Weitere Geschichten', 'wp-multi'); ?></h3>
    <div class="wpm-table-wrap">
        <table class="wpm-table">
            <thead>
                <tr>
                    <th><?php _e('Geschichte', 'wp-multi'); ?></th>
                    <th><?php _e('Veröffentlicht', 'wp-multi'); ?></th>
                    <?php if ($analytics_table) : ?>
                        <th><?php _e('Aufrufe', 'wp-multi'); ?></th>
                    <?php endif; ?>
                </tr>
            </thead>
            <tbody>
                <?php if ($posts) : ?>
                    <?php foreach ($posts as $row) : ?>
                        <tr>
                            <td>
                                <a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>" target="_blank" rel="noopener">
                                    <?php echo esc_html($row->post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?>
                                </a>
                            </td>
                            <td><?php echo esc_html(mysql2date(get_option('date_format'), $row->post_date)); ?></td>
                            <?php if ($analytics_table) : ?>
                                <td><?php echo esc_html(number_format_i18n(isset($post_views[(int) $row->ID]) ? $post_views[(int) $row->ID] : 0)); ?></td>
                            <?php endif; ?>
                        </tr>
                    <?php endforeach; ?>
                <?php else : ?>
                    <tr><td colspan="<?php echo $analytics_table ? 3 : 2; ?>"><?php _e('Alle Geschichten dieses Autors gehören zu einer Serie.', 'wp-multi'); ?></td></tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
    <?php
    wpmt_admin_card_close();
}

/**
 * Ermittelt, wo die Serien dieser Installation stecken.
 *
 * Unterstützt zwei übliche Modelle:
 *  1. eigene Taxonomie (z. B. "series") – jeder Term ist eine Serie
 *  2. Oberkategorie "Serien" – jede Unterkategorie ist eine Serie
 *
 * @return array|null ['taxonomy' => string, 'parent' => int, 'label' => string] oder null.
 */
function wp_multi_detect_series_source() {
    // Feste Vorgabe per Filter, z. B. add_filter('wp_multi_series_taxonomy', fn() => 'meine-serien');
    $forced = apply_filters('wp_multi_series_taxonomy', '');
    if ($forced && taxonomy_exists($forced)) {
        return ['taxonomy' => $forced, 'parent' => 0, 'label' => $forced];
    }

    // Mögliche Namen einer eigenen Serien-Taxonomie
    $candidates = apply_filters('wp_multi_series_taxonomy_candidates', ['series', 'serie', 'serien', 'story-series']);

    foreach ($candidates as $candidate) {
        if (!taxonomy_exists($candidate)) {
            continue;
        }

        $count = wp_count_terms(['taxonomy' => $candidate, 'hide_empty' => false]);
        if (!is_wp_error($count) && (int) $count > 0) {
            return ['taxonomy' => $candidate, 'parent' => 0, 'label' => $candidate];
        }
    }

    // Zweites Modell: Oberkategorie mit einer Unterkategorie je Serie
    foreach ($candidates as $candidate) {
        $term = get_term_by('slug', $candidate, 'category');
        if (!$term || is_wp_error($term)) {
            continue;
        }

        $children = get_term_children($term->term_id, 'category');
        if (!is_wp_error($children) && !empty($children)) {
            return [
                'taxonomy' => 'category',
                'parent'   => (int) $term->term_id,
                /* translators: %s: Name der Oberkategorie */
                'label'    => sprintf(__('Unterkategorien von "%s"', 'wp-multi'), $term->name),
            ];
        }
    }

    return null;
}

/**
 * Gibt einen Spaltenkopf mit Sortier-Link aus.
 *
 * @param string $label         Beschriftung der Spalte.
 * @param string $key           Sortierschlüssel dieser Spalte.
 * @param string $orderby       Aktuell aktive Sortierung.
 * @param string $order         Aktuelle Richtung (ASC/DESC).
 * @param string $sort_url      Basis-URL für den Link.
 * @param string $default_order Richtung beim ersten Klick auf diese Spalte.
 */
function wp_multi_render_sort_link($label, $key, $orderby, $order, $sort_url, $default_order = 'ASC') {
    $is_active = ($orderby === $key);
    // Aktive Spalte kehrt die Richtung um, inaktive startet mit ihrem Standard
    $next_order = $is_active ? (($order === 'ASC') ? 'DESC' : 'ASC') : $default_order;
    $url = add_query_arg(['orderby' => $key, 'order' => $next_order], $sort_url);

    echo '<a href="' . esc_url($url) . '">' . esc_html($label);
    if ($is_active) {
        echo ' <span>' . (($order === 'ASC') ? '↑' : '↓') . '</span>';
    }
    echo '</a>';
}

/**
 * Liefert den Namen der Analytics-Tabelle, sofern sie existiert.
 *
 * @return string Tabellenname oder '' ohne Analytics-Modul.
 */
function wp_multi_get_analytics_table() {
    static $table = null;

    if ($table !== null) {
        return $table;
    }

    if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
        $table = '';
        return $table;
    }

    global $wpdb;
    $name = WP_MULTI_ANALYTICS_TABLE;
    $table = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $name)) ? $name : '';

    return $table;
}

/**
 * Ergänzt Serien um letzten Teil und Aufrufzahlen.
 *
 * Läuft in eigenen Abfragen statt als weiterer JOIN in der Hauptabfrage: Ein
 * zusätzlicher JOIN würde die Zeilen vervielfachen und die Teile-Zählung
 * verfälschen.
 *
 * @param array  $rows     Serienzeilen mit term_id.
 * @param string $taxonomy Taxonomie der Serien.
 */
function wp_multi_add_series_details(&$rows, $taxonomy) {
    if (empty($rows)) {
        return;
    }

    global $wpdb;

    $term_ids = [];
    foreach ($rows as $row) {
        $term_ids[] = (int) $row->term_id;
    }
    $term_id_list = implode(',', $term_ids);

    // Letzter Teil je Serie: absteigend sortiert, der erste Treffer je Serie gewinnt
    $parts = $wpdb->get_results($wpdb->prepare("
        SELECT tt.term_id, p.ID AS post_id, p.post_title
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
        INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
        WHERE p.post_status = 'publish'
        AND tt.term_id IN ({$term_id_list})
        ORDER BY p.post_date DESC
    ", $taxonomy));

    $last_parts = [];
    foreach ((array) $parts as $part) {
        if (!isset($last_parts[$part->term_id])) {
            $last_parts[$part->term_id] = $part;
        }
    }

    // Aufrufe der ganzen Serie und des letzten Teils
    $series_views = [];
    $post_views = [];
    $analytics_table = wp_multi_get_analytics_table();

    if ($analytics_table) {
        $totals = $wpdb->get_results($wpdb->prepare("
            SELECT tt.term_id, COUNT(*) AS views
            FROM {$analytics_table} a
            INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = a.post_id
            INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
            WHERE a.action = 'view'
            AND tt.term_id IN ({$term_id_list})
            GROUP BY tt.term_id
        ", $taxonomy));

        foreach ((array) $totals as $total) {
            $series_views[(int) $total->term_id] = (int) $total->views;
        }

        $post_ids = [];
        foreach ($last_parts as $part) {
            $post_ids[] = (int) $part->post_id;
        }

        if ($post_ids) {
            $post_id_list = implode(',', $post_ids);
            $single = $wpdb->get_results("
                SELECT post_id, COUNT(*) AS views
                FROM {$analytics_table}
                WHERE action = 'view'
                AND post_id IN ({$post_id_list})
                GROUP BY post_id
            ");

            foreach ((array) $single as $entry) {
                $post_views[(int) $entry->post_id] = (int) $entry->views;
            }
        }
    }

    foreach ($rows as $row) {
        $term_id = (int) $row->term_id;
        $last_part = isset($last_parts[$term_id]) ? $last_parts[$term_id] : null;

        $row->last_part_id = $last_part ? (int) $last_part->post_id : 0;
        $row->last_part_title = $last_part ? $last_part->post_title : '';
        $row->series_views = isset($series_views[$term_id]) ? $series_views[$term_id] : 0;
        $row->last_part_views = ($row->last_part_id && isset($post_views[$row->last_part_id])) ? $post_views[$row->last_part_id] : 0;
    }
}

/**
 * Zeigt Serien, die seit längerem keinen neuen Teil bekommen haben.
 *
 * Beispiel: "Serie mit 7 Teilen – letzter Teil vor 11 Monaten". Damit sieht man
 * im Backend sofort, wo sich Nachfragen beim Autor lohnt.
 */
function wp_multi_render_stalled_series($base_url = '') {
    // Mindestens so viele Teile und so viele Tage ohne Fortsetzung
    $min_parts = (int) apply_filters('wp_multi_stalled_series_min_parts', 2);
    $min_days = (int) apply_filters('wp_multi_stalled_series_min_days', 180);

    if ($base_url === '') {
        $base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php'));
    }
    $sort_url = add_query_arg('tab', 'series', $base_url);

    // Sortierparameter aus URL lesen. 'series_views' und 'last_part_views' stammen
    // aus den Nachfragen und werden deshalb in PHP sortiert.
    $valid_orderby = ['name' => 't.name', 'parts' => 'part_count', 'date' => 'last_part_date'];
    $php_orderby = ['series_views', 'last_part_views'];
    $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'date';
    if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) {
        $orderby = 'date';
    }
    // Standard: neueste Fortsetzung oben
    $order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC';
    $sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'last_part_date';

    wpmt_admin_card_open(__('Liegengebliebene Serien', 'wp-multi'));

    $source = wp_multi_detect_series_source();

    if (!$source) {
        wp_multi_render_series_source_hint();
        wpmt_admin_card_close();
        return;
    }

    global $wpdb;

    // Bei Unterkategorien nur die Kinder der Oberkategorie berücksichtigen
    $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';

    $series = $wpdb->get_results($wpdb->prepare("
        SELECT t.term_id, t.name AS series_name,
               COUNT(DISTINCT p.ID) AS part_count,
               MAX(p.post_date) AS last_part_date,
               GROUP_CONCAT(DISTINCT pm.meta_value ORDER BY pm.meta_value SEPARATOR ', ') AS authors
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
        INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql}
        INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
        LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
        WHERE p.post_status = 'publish'
        GROUP BY t.term_id, t.name
        HAVING part_count >= %d
        ORDER BY {$sql_orderby} {$order}, last_part_date DESC
    ", $source['taxonomy'], $min_parts));

    // Nur Serien behalten, deren letzter Teil lange genug zurückliegt
    $stalled = [];
    foreach ((array) $series as $row) {
        $last_ts = strtotime(get_gmt_from_date($row->last_part_date));
        if (!$last_ts) {
            continue;
        }

        $row->days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS);
        if ($row->days > $min_days) {
            $stalled[] = $row;
        }
    }

    // Letzten Teil und Aufrufe nur für die tatsächlich angezeigten Serien nachladen
    wp_multi_add_series_details($stalled, $source['taxonomy']);

    // Sortierung nach Aufrufen: bei Gleichstand die DB-Reihenfolge behalten
    if (in_array($orderby, $php_orderby, true)) {
        $position = 0;
        foreach ($stalled as $row) {
            $row->position = $position++;
        }

        usort($stalled, function ($a, $b) use ($orderby, $order) {
            $value_a = (int) $a->$orderby;
            $value_b = (int) $b->$orderby;

            if ($value_a === $value_b) {
                return $a->position - $b->position;
            }

            $diff = $value_a - $value_b;

            return ($order === 'DESC') ? -$diff : $diff;
        });
    }

    $show_views = (bool) wp_multi_get_analytics_table();
    $colspan = $show_views ? 6 : 4;
    ?>
    <p class="wpm-muted">
        <?php
        printf(
            /* translators: 1: Mindestanzahl Teile, 2: Anzahl Tage, 3: Quelle der Serien */
            esc_html__('Serien mit mindestens %1$d Teilen, deren letzter Teil länger als %2$d Tage zurückliegt. Quelle: %3$s.', 'wp-multi'),
            $min_parts,
            $min_days,
            esc_html($source['label'])
        );
        ?>
    </p>
    <div class="wpm-table-wrap">
        <table class="wpm-table">
            <thead>
                <tr>
                    <th><?php wp_multi_render_sort_link(__('Serie', 'wp-multi'), 'name', $orderby, $order, $sort_url, 'ASC'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Teile', 'wp-multi'), 'parts', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <th><?php wp_multi_render_sort_link(__('Letzter Teil', 'wp-multi'), 'date', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <?php if ($show_views) : ?>
                        <th><?php wp_multi_render_sort_link(__('Aufrufe gesamt', 'wp-multi'), 'series_views', $orderby, $order, $sort_url, 'DESC'); ?></th>
                        <th><?php wp_multi_render_sort_link(__('Aufrufe letzter Teil', 'wp-multi'), 'last_part_views', $orderby, $order, $sort_url, 'DESC'); ?></th>
                    <?php endif; ?>
                    <th><?php _e('Autor(en)', 'wp-multi'); ?></th>
                </tr>
            </thead>
            <tbody>
                <?php if ($stalled) : ?>
                    <?php foreach ($stalled as $row) : ?>
                        <?php $term_link = get_term_link((int) $row->term_id, $source['taxonomy']); ?>
                        <tr>
                            <td>
                                <?php if (!is_wp_error($term_link)) : ?>
                                    <a href="<?php echo esc_url($term_link); ?>" target="_blank" rel="noopener"><?php echo esc_html($row->series_name); ?></a>
                                <?php else : ?>
                                    <?php echo esc_html($row->series_name); ?>
                                <?php endif; ?>
                            </td>
                            <td><span class="wpm-badge"><?php echo (int) $row->part_count; ?></span></td>
                            <td>
                                <?php if ($row->last_part_id) : ?>
                                    <a href="<?php echo esc_url(get_permalink($row->last_part_id)); ?>" target="_blank" rel="noopener">
                                        <?php echo esc_html($row->last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
                                    </a><br />
                                <?php endif; ?>
                                <span class="wpm-muted">
                                    <?php echo esc_html(mysql2date(get_option('date_format'), $row->last_part_date)); ?>
                                    (<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($row->last_part_date))))); ?>)
                                </span>
                            </td>
                            <?php if ($show_views) : ?>
                                <td><span class="wpm-badge"><?php echo esc_html(number_format_i18n($row->series_views)); ?></span></td>
                                <td><?php echo esc_html(number_format_i18n($row->last_part_views)); ?></td>
                            <?php endif; ?>
                            <td><?php wp_multi_render_guest_author_link($row->authors, $base_url, 'series', true); ?></td>
                        </tr>
                    <?php endforeach; ?>
                <?php else : ?>
                    <tr><td colspan="<?php echo (int) $colspan; ?>"><?php _e('Keine liegengebliebenen Serien – alle Serien sind aktuell.', 'wp-multi'); ?></td></tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
    <?php if ($show_views) : ?>
        <p class="wpm-muted"><?php _e('Aufrufe stammen aus dem Analytics-Modul und werden erst seit dessen Aktivierung erfasst – alte Serien haben deshalb oft weniger Aufrufe, als sie tatsächlich hatten.', 'wp-multi'); ?></p>
    <?php endif; ?>
    <?php
    wpmt_admin_card_close();
}

/**
 * Hinweis, wenn keine Serien-Quelle gefunden wurde – inklusive Liste der
 * vorhandenen Taxonomien, damit die richtige schnell eingetragen werden kann.
 */
function wp_multi_render_series_source_hint() {
    $taxonomies = get_taxonomies(['public' => true], 'objects');
    ?>
    <p><?php _e('Es wurde keine Quelle für Serien gefunden. Erwartet wird entweder eine eigene Taxonomie ("series", "serie", "serien") oder eine Oberkategorie gleichen Namens mit einer Unterkategorie je Serie.', 'wp-multi'); ?></p>
    <p class="wpm-muted"><?php _e('Vorhandene Taxonomien dieser Seite:', 'wp-multi'); ?></p>
    <div class="wpm-table-wrap">
        <table class="wpm-table">
            <thead>
                <tr>
                    <th><?php _e('Taxonomie', 'wp-multi'); ?></th>
                    <th><?php _e('Name', 'wp-multi'); ?></th>
                    <th><?php _e('Einträge', 'wp-multi'); ?></th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($taxonomies as $taxonomy) : ?>
                    <?php $count = wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false]); ?>
                    <tr>
                        <td><code><?php echo esc_html($taxonomy->name); ?></code></td>
                        <td><?php echo esc_html($taxonomy->labels->name); ?></td>
                        <td><span class="wpm-badge"><?php echo is_wp_error($count) ? 0 : (int) $count; ?></span></td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
    <p class="wpm-muted">
        <?php _e('Die passende Taxonomie lässt sich per Filter festlegen:', 'wp-multi'); ?>
        <code>add_filter('wp_multi_series_taxonomy', function () { return 'taxonomie-name'; });</code>
    </p>
    <?php
}


// Funktion zum Erstellen der Gastautoren-Beitragsseite
function wp_multi_create_guest_author_page() {
    $page_title = __('Gastautor Beiträge', 'wp-multi');
    $page_slug = 'gastautor-beitraege';
    $page = get_page_by_path($page_slug);

    if (!$page) {
        $page_id = wp_insert_post(array(
            'post_title' => $page_title,
            'post_name' => $page_slug,
            'post_content' => '[guest_author_posts]',
            'post_status' => 'publish',
            'post_type' => 'page',
        ));

        if ($page_id && !is_wp_error($page_id)) {
            update_option('wp_multi_guest_author_page_id', $page_id);
        }
    } else {
        update_option('wp_multi_guest_author_page_id', $page->ID);
    }
}
add_action('init', 'wp_multi_create_guest_author_page');

/**
 * Liefert die Serien eines Gast-Autors.
 *
 * @param string $guest_author Name des Gast-Autors.
 * @return array ['series' => Zeilen, 'post_ids' => IDs aller Teile dieser Serien]
 */
function wp_multi_get_guest_author_series($guest_author) {
    $empty = ['series' => [], 'post_ids' => []];
    $source = wp_multi_detect_series_source();

    if (!$source || $guest_author === '') {
        return $empty;
    }

    global $wpdb;
    $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';

    $series = $wpdb->get_results($wpdb->prepare("
        SELECT t.term_id, t.name AS series_name,
               COUNT(DISTINCT p.ID) AS author_parts,
               MAX(p.post_date) AS last_part_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s
        INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
        INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql}
        INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
        WHERE p.post_status = 'publish'
        GROUP BY t.term_id, t.name
        ORDER BY last_part_date DESC
    ", $guest_author, $source['taxonomy']));

    if (!$series) {
        return $empty;
    }

    $term_ids = [];
    foreach ($series as $row) {
        $term_ids[] = (int) $row->term_id;
    }
    $term_id_list = implode(',', $term_ids);

    // Gesamtlänge der Serie – auch Teile anderer Autoren zählen dazu
    $totals = $wpdb->get_results($wpdb->prepare("
        SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
        INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
        WHERE p.post_status = 'publish'
        AND tt.term_id IN ({$term_id_list})
        GROUP BY tt.term_id
    ", $source['taxonomy']));

    $total_parts = [];
    foreach ((array) $totals as $total) {
        $total_parts[(int) $total->term_id] = (int) $total->total_parts;
    }

    foreach ($series as $row) {
        $row->total_parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts;
        $row->taxonomy = $source['taxonomy'];
    }

    // Alle Beiträge dieser Serien – damit sie in der Einzelliste nicht doppelt stehen
    $post_ids = $wpdb->get_col($wpdb->prepare("
        SELECT DISTINCT p.ID
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
        INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
        WHERE tt.term_id IN ({$term_id_list})
    ", $source['taxonomy']));

    return [
        'series'   => $series,
        'post_ids' => array_map('intval', (array) $post_ids),
    ];
}

/**
 * Findet die Seite, auf der die Autorenübersicht steht.
 *
 * Gesucht wird die Seite mit dem Shortcode [guest_authors]; das Ergebnis wird
 * zwischengespeichert, damit die Suche nicht bei jedem Profilaufruf läuft.
 *
 * @return string URL oder '' wenn keine Übersichtsseite existiert.
 */
function wp_multi_get_guest_author_overview_url() {
    $url = apply_filters('wp_multi_author_overview_url', '');

    if ($url) {
        return $url;
    }

    $cached = get_transient('wp_multi_author_overview_page');

    if ($cached === false) {
        global $wpdb;

        $cached = (int) $wpdb->get_var("
            SELECT ID
            FROM {$wpdb->posts}
            WHERE post_type = 'page'
            AND post_status = 'publish'
            AND post_content LIKE '%[guest_authors%'
            ORDER BY ID ASC
            LIMIT 1
        ");

        set_transient('wp_multi_author_overview_page', $cached, 12 * HOUR_IN_SECONDS);
    }

    return $cached ? get_permalink($cached) : '';
}

// Shortcode für Gastautoren-Liste
function wp_multi_guest_author_shortcode($atts = []) {
    ob_start();
    global $wpdb;

    // Erlaubte Sortierfelder (Whitelist – verhindert SQL-Injection)
    $valid_orderby = [
        'name'  => 'pm.meta_value',
        'posts' => 'post_count',
        'date'  => 'last_post_date',
        'new'   => 'first_post_date',
    ];
    // Sinnvolle Standardrichtung je Spalte
    $default_order = ['name' => 'ASC', 'posts' => 'DESC', 'date' => 'DESC', 'new' => 'DESC'];

    // Voreinstellung über Shortcode-Attribute, z. B. [guest_authors orderby="date" order="desc"]
    $atts = shortcode_atts(['orderby' => 'name', 'order' => ''], $atts, 'guest_authors');

    // URL-Parameter haben Vorrang vor den Shortcode-Attributen
    $orderby = isset($_GET['ga_orderby']) ? sanitize_key(wp_unslash($_GET['ga_orderby'])) : sanitize_key($atts['orderby']);
    if (!isset($valid_orderby[$orderby])) {
        $orderby = 'name';
    }

    $order_raw = isset($_GET['ga_order']) ? sanitize_key(wp_unslash($_GET['ga_order'])) : sanitize_key($atts['order']);
    $order = in_array(strtoupper($order_raw), ['ASC', 'DESC'], true) ? strtoupper($order_raw) : $default_order[$orderby];

    $sql_orderby = $valid_orderby[$orderby];

    // Alle Gastautoren mit Beitragsanzahl, erster und letzter Veröffentlichung
    $query = "
        SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count,
               MAX(p.post_date) AS last_post_date, MIN(p.post_date) AS first_post_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
        WHERE pm.meta_key = '_guest_author'
        AND pm.meta_value != ''
        AND p.post_status = 'publish'
        GROUP BY pm.meta_value
        ORDER BY {$sql_orderby} {$order}, pm.meta_value ASC
    ";
    $guest_authors = $wpdb->get_results($query);

    // Serien je Autor (leer, wenn es keine Serien-Taxonomie gibt)
    $series_stats = wp_multi_get_guest_author_series_stats();

    // URL der Gastautoren-Beitragsseite
    $guest_author_page_id = get_option('wp_multi_guest_author_page_id');
    $guest_author_page_url = $guest_author_page_id ? get_permalink($guest_author_page_id) : '#';

    // Beschriftungen der Sortier-Schaltflächen
    $sort_labels = [
        'name'  => __('Name', 'wp-multi'),
        'posts' => __('Meiste Geschichten', 'wp-multi'),
        'date'  => __('Zuletzt veröffentlicht', 'wp-multi'),
        'new'   => __('Neueste Autoren', 'wp-multi'),
    ];

    ?>
    <div id="guest-author-section" class="container mx-auto px-4 py-8">
        <!-- Sortierung -->
        <div class="guest-author-sort flex flex-wrap items-center gap-2 mb-6">
            <span class="guest-author-sort-label text-sm text-gray-600"><?php _e('Sortieren nach:', 'wp-multi'); ?></span>
            <?php foreach ($sort_labels as $key => $label) : ?>
                <?php
                $is_active  = ($orderby === $key);
                // Aktive Spalte kehrt die Richtung um, inaktive startet mit ihrem Standard
                $next_order = $is_active ? (($order === 'ASC') ? 'DESC' : 'ASC') : $default_order[$key];
                $sort_url   = add_query_arg(['ga_orderby' => $key, 'ga_order' => strtolower($next_order)]) . '#guest-author-section';
                ?>
                <a href="<?php echo esc_url($sort_url); ?>"
                   class="guest-author-sort-link<?php echo $is_active ? ' is-active' : ''; ?> px-3 py-1 rounded border text-sm <?php echo $is_active ? 'border-gray-800 font-semibold' : 'border-gray-300'; ?>">
                    <?php echo esc_html($label); ?>
                    <?php if ($is_active) : ?>
                        <span aria-hidden="true"><?php echo ($order === 'ASC') ? '↑' : '↓'; ?></span>
                    <?php endif; ?>
                </a>
            <?php endforeach; ?>
        </div>

        <!-- Gastautoren-Liste -->
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
            <?php if ($guest_authors) : ?>
                <?php foreach ($guest_authors as $author) : ?>
                    <?php
                    $milestone = wp_multi_get_guest_author_milestone($author->post_count);
                    $series_count = (is_array($series_stats) && isset($series_stats[$author->guest_author])) ? $series_stats[$author->guest_author]['series_count'] : 0;
                    ?>
                    <a href="<?php echo esc_url(add_query_arg('guest_author', $author->guest_author, $guest_author_page_url)); ?>" class="guest-author-card bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
                        <h3 class="text-xl font-semibold text-gray-800">
                            <?php echo esc_html($author->guest_author); ?>
                            <?php if ($milestone) : ?>
                                <span class="guest-author-milestone" title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
                            <?php endif; ?>
                        </h3>
                        <p class="text-gray-600">
                            <?php
                            /* translators: %s: Anzahl der Geschichten */
                            printf(esc_html(_n('%s Geschichte', '%s Geschichten', $author->post_count, 'wp-multi')), esc_html(number_format_i18n($author->post_count)));
                            ?>
                            <?php if ($series_count) : ?>
                                &middot;
                                <?php
                                /* translators: %s: Anzahl der Serien */
                                printf(esc_html(_n('%s Serie', '%s Serien', $series_count, 'wp-multi')), esc_html(number_format_i18n($series_count)));
                                ?>
                            <?php endif; ?>
                        </p>
                        <?php if (!empty($author->last_post_date)) : ?>
                            <p class="guest-author-last-post text-sm text-gray-500"><?php printf(__('Zuletzt veröffentlicht: %s', 'wp-multi'), esc_html(mysql2date(get_option('date_format'), $author->last_post_date))); ?></p>
                        <?php endif; ?>
                    </a>
                <?php endforeach; ?>
            <?php else : ?>
                <p class="col-span-2 text-center text-gray-600"><?php _e('Keine Gastautoren gefunden.', 'wp-multi'); ?></p>
            <?php endif; ?>
        </div>
    </div>

    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <?php

    return ob_get_clean();
}
add_shortcode('guest_authors', 'wp_multi_guest_author_shortcode');

// Shortcode für das Autorenprofil
function wp_multi_guest_author_posts_shortcode() {
    ob_start();
    global $wpdb;

    $guest_author = isset($_GET['guest_author']) ? sanitize_text_field(wp_unslash($_GET['guest_author'])) : '';
    $post_count = $guest_author ? wp_multi_get_guest_author_post_count($guest_author) : 0;

    $overview_url = wp_multi_get_guest_author_overview_url();

    if (!$guest_author || !$post_count) {
        ?>
        <div id="guest-author-posts" class="container mx-auto px-4 py-8">
            <p class="text-gray-600">
                <?php
                if (!$guest_author) {
                    _e('Wählen Sie einen Gastautor aus, um die Geschichten anzuzeigen.', 'wp-multi');
                } else {
                    _e('Von diesem Autor sind derzeit keine Geschichten veröffentlicht.', 'wp-multi');
                }
                ?>
            </p>
        </div>
        <?php
        return ob_get_clean();
    }

    $milestone = wp_multi_get_guest_author_milestone($post_count);
    $next_milestone = wp_multi_get_guest_author_next_milestone($post_count);

    // Serien des Autors
    $series_data = wp_multi_get_guest_author_series($guest_author);
    $series = $series_data['series'];
    $series_post_ids = $series_data['post_ids'];

    // Neueste Einzelgeschichten (Serienteile stehen im eigenen Abschnitt)
    $exclude_sql = $series_post_ids ? 'AND p.ID NOT IN (' . implode(',', $series_post_ids) . ')' : '';
    $latest_limit = (int) apply_filters('wp_multi_author_profile_latest_limit', 12);

    $latest = $wpdb->get_results($wpdb->prepare("
        SELECT p.ID, p.post_title, p.post_date
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
        WHERE pm.meta_value = %s
        AND p.post_status = 'publish'
        {$exclude_sql}
        ORDER BY p.post_date DESC
        LIMIT %d
    ", $guest_author, $latest_limit));

    // Beliebteste Geschichten – nur wenn Aufrufe vorliegen
    $popular = [];
    $analytics_table = wp_multi_get_analytics_table();

    if ($analytics_table) {
        $popular_limit = (int) apply_filters('wp_multi_author_profile_popular_limit', 5);

        $popular = $wpdb->get_results($wpdb->prepare("
            SELECT p.ID, p.post_title, COUNT(*) AS views
            FROM {$analytics_table} a
            INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id
            INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
            WHERE a.action = 'view'
            AND pm.meta_value = %s
            AND p.post_status = 'publish'
            GROUP BY p.ID, p.post_title
            ORDER BY views DESC
            LIMIT %d
        ", $guest_author, $popular_limit));
    }
    ?>
    <div id="guest-author-posts" class="container mx-auto px-4 py-8">
        <?php if ($overview_url) : ?>
            <p class="mb-4"><a href="<?php echo esc_url($overview_url); ?>" class="text-sm">&larr; <?php _e('Alle Autoren', 'wp-multi'); ?></a></p>
        <?php endif; ?>

        <h2 class="text-2xl font-semibold text-gray-800">
            <?php echo esc_html($guest_author); ?>
            <?php if ($milestone) : ?>
                <span title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
            <?php endif; ?>
        </h2>

        <p class="text-gray-600 mb-2">
            <?php
            /* translators: %s: Anzahl der Geschichten */
            printf(esc_html(_n('%s Geschichte', '%s Geschichten', $post_count, 'wp-multi')), esc_html(number_format_i18n($post_count)));
            ?>
            <?php if ($series) : ?>
                &middot;
                <?php
                /* translators: %s: Anzahl der Serien */
                printf(esc_html(_n('%s Serie', '%s Serien', count($series), 'wp-multi')), esc_html(number_format_i18n(count($series))));
            endif;
            ?>
        </p>

        <?php if ($milestone) : ?>
            <p class="guest-author-milestone-badge text-lg font-semibold">
                <?php echo esc_html($milestone['emoji'] . ' ' . $milestone['label']); ?>
                <span class="guest-author-milestone-threshold text-sm font-normal text-gray-500">
                    <?php
                    /* translators: %d: Schwelle des Rangs */
                    printf(esc_html__('(ab %d Geschichten)', 'wp-multi'), (int) $milestone['threshold']);
                    ?>
                </span>
            </p>
        <?php endif; ?>
        <?php if ($next_milestone) : ?>
            <p class="guest-author-next-milestone text-sm text-gray-500 mb-6">
                <?php
                printf(
                    /* translators: 1: fehlende Geschichten, 2: Emoji des nächsten Meilensteins, 3: Anzahl des nächsten Meilensteins */
                    esc_html(_n('Noch %1$d Geschichte bis %2$s %3$s!', 'Noch %1$d Geschichten bis %2$s %3$s!', $next_milestone['missing'], 'wp-multi')),
                    $next_milestone['missing'],
                    esc_html($next_milestone['emoji']),
                    esc_html($next_milestone['label'])
                );
                ?>
            </p>
        <?php endif; ?>

        <?php if ($series) : ?>
            <h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Serien', 'wp-multi'); ?></h3>
            <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                <?php foreach ($series as $row) : ?>
                    <?php $term_link = get_term_link((int) $row->term_id, $row->taxonomy); ?>
                    <div class="post-card bg-white p-6 rounded-lg shadow-md">
                        <h4 class="text-lg font-medium text-gray-800">
                            <?php if (!is_wp_error($term_link)) : ?>
                                <a href="<?php echo esc_url($term_link); ?>"><?php echo esc_html($row->series_name); ?></a>
                            <?php else : ?>
                                <?php echo esc_html($row->series_name); ?>
                            <?php endif; ?>
                        </h4>
                        <p class="text-gray-600">
                            <?php
                            /* translators: %s: Anzahl der Teile */
                            printf(esc_html(_n('%s Teil', '%s Teile', $row->total_parts, 'wp-multi')), esc_html(number_format_i18n($row->total_parts)));
                            ?>
                            &middot;
                            <?php printf(esc_html__('zuletzt %s', 'wp-multi'), esc_html(mysql2date(get_option('date_format'), $row->last_part_date))); ?>
                        </p>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>

        <?php if ($popular) : ?>
            <h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Beliebteste Geschichten', 'wp-multi'); ?></h3>
            <ul class="guest-author-popular list-disc pl-5 text-gray-700">
                <?php foreach ($popular as $row) : ?>
                    <li>
                        <a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>"><?php echo esc_html(get_the_title((int) $row->ID)); ?></a>
                    </li>
                <?php endforeach; ?>
            </ul>
        <?php endif; ?>

        <?php if ($latest) : ?>
            <h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Neueste Geschichten', 'wp-multi'); ?></h3>
            <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                <?php foreach ($latest as $row) : ?>
                    <div class="post-card bg-white p-6 rounded-lg shadow-md">
                        <h4 class="text-lg font-medium text-gray-800">
                            <a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>" class="hover:text-blue-600"><?php echo esc_html(get_the_title((int) $row->ID)); ?></a>
                        </h4>
                        <p class="text-gray-500 text-sm"><?php echo esc_html(mysql2date(get_option('date_format'), $row->post_date)); ?></p>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div>

    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <?php

    return ob_get_clean();
}
add_shortcode('guest_author_posts', 'wp_multi_guest_author_posts_shortcode');

