<?php
/**
 * Plugin Name: Newsroom OS – Google Discover, Google News, Schema & Workflow
 * Plugin URI: https://wordpress.org/plugins/newsroom-ai-assistant/
 * Description: Google Discover & Google News optimization for publishers: Discover Score, NewsArticle schema, Search Console analytics (Pro), editorial workflow. Gutenberg + Classic. Pro AI (BYOK) at newsos.io.
 * Version: 2.8.0
 * Author: Kostas Karapapas
 * Text Domain: newsroom-ai-assistant
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

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

// Only one copy may load (e.g. old folder + new upload both active → fatal on constants).
if ( defined( 'NEWSAI_BOOTSTRAPPED' ) ) {
	return;
}
define( 'NEWSAI_BOOTSTRAPPED', true );

if ( ! defined( 'NEWSAI_VERSION' ) ) {
	define( 'NEWSAI_VERSION', '2.8.0' );
}
if ( ! defined( 'NEWSAI_PATH' ) ) {
	define( 'NEWSAI_PATH', plugin_dir_path( __FILE__ ) );
}
if ( ! defined( 'NEWSAI_URL' ) ) {
	define( 'NEWSAI_URL', plugin_dir_url( __FILE__ ) );
}

if ( ! function_exists( 'newsai_asset_ver' ) ) {
	/**
	 * Cache-bust static assets when file contents change (helps after plugin updates).
	 *
	 * @param string $rel_path Path relative to plugin root, e.g. assets/foo.css.
	 */
	function newsai_asset_ver( $rel_path ) {
		$path = NEWSAI_PATH . ltrim( (string) $rel_path, '/' );
		if ( is_readable( $path ) ) {
			return NEWSAI_VERSION . '.' . (string) filemtime( $path );
		}
		return NEWSAI_VERSION;
	}
}

/**
 * Pro gate (single plugin package for everyone):
 * - Default: Pro is OFF. Customers unlock Pro with a NewsOS.io license (Editorial Control → PRO). Updates do not require re-uploading a different zip.
 * - Optional dev override: in wp-config.php (before wp-settings.php loads), add: define( 'NEWSAI_PRO_BUILD', true );
 */
if ( ! defined( 'NEWSAI_PRO_BUILD' ) ) {
	define( 'NEWSAI_PRO_BUILD', false );
}

if ( ! function_exists( 'newsai_supported_languages' ) ) {
    /**
     * The set of UI languages the plugin officially ships translations for.
     * Each entry maps a short code (used in newsai_lang option) to its full WP
     * locale + native label. Adding a new language = drop a .mo file + extend
     * this array (and the language selector in Settings).
     *
     * @return array<string, array{locale:string, label:string, native:string}>
     */
    function newsai_supported_languages() {
        return [
            'en' => [ 'locale' => 'en_US', 'label' => 'English',  'native' => 'English'   ],
            'el' => [ 'locale' => 'el',    'label' => 'Greek',    'native' => 'Ελληνικά'  ],
            'de' => [ 'locale' => 'de_DE', 'label' => 'German',   'native' => 'Deutsch'   ],
            'es' => [ 'locale' => 'es_ES', 'label' => 'Spanish',  'native' => 'Español'   ],
        ];
    }
}

if (!function_exists('newsai_get_ui_lang')) {
    /**
     * Short language code (en|el|de|es) used internally by the plugin UI.
     * Reads the explicit `newsai_lang` option first, then falls back to the
     * WP locale. The actual gettext translation is loaded via plugin_locale.
     */
    function newsai_get_ui_lang() {
        $supported  = array_keys( newsai_supported_languages() );
        $saved_lang = (string) get_option( 'newsai_lang', '' );
        if ( in_array( $saved_lang, $supported, true ) ) {
            $lang = $saved_lang;
        } else {
            $locale = function_exists( 'determine_locale' ) ? (string) determine_locale() : (string) get_bloginfo( 'language' );
            $lang   = 'en';
            foreach ( $supported as $code ) {
                if ( $code === 'en' ) { continue; }
                if ( stripos( $locale, $code ) === 0 ) { $lang = $code; break; }
            }
        }
        $filtered = apply_filters( 'newsai_ui_lang', $lang, $saved_lang );
        return ( is_string( $filtered ) && in_array( $filtered, $supported, true ) ) ? $filtered : $lang;
    }
}

if ( ! function_exists( 'newsai_infer_geo_from_locale' ) ) {
	/**
	 * Guess Target Geo (ISO 3166-1 alpha-2) from the WordPress site locale.
	 *
	 * @param string $locale Optional BCP47 / WP locale.
	 * @return string Two-letter country code.
	 */
	function newsai_infer_geo_from_locale( $locale = '' ) {
		if ( $locale === '' ) {
			$locale = function_exists( 'determine_locale' ) ? (string) determine_locale() : (string) get_locale();
		}
		$norm = strtolower( str_replace( '_', '-', (string) $locale ) );
		$map  = [
			'el'      => 'GR',
			'el-gr'   => 'GR',
			'en-us'   => 'US',
			'en-gb'   => 'GB',
			'en-au'   => 'AU',
			'en-ca'   => 'CA',
			'en-in'   => 'IN',
			'en-za'   => 'ZA',
			'de'      => 'DE',
			'de-de'   => 'DE',
			'de-at'   => 'AT',
			'de-ch'   => 'CH',
			'fr'      => 'FR',
			'fr-fr'   => 'FR',
			'fr-be'   => 'BE',
			'fr-ch'   => 'CH',
			'es'      => 'ES',
			'es-es'   => 'ES',
			'es-mx'   => 'MX',
			'es-419'  => 'MX',
			'it'      => 'IT',
			'it-it'   => 'IT',
			'pt-br'   => 'BR',
			'pt-pt'   => 'PT',
			'nl'      => 'NL',
			'nl-nl'   => 'NL',
			'nl-be'   => 'BE',
			'ja'      => 'JP',
			'id'      => 'ID',
			'id-id'   => 'ID',
			'ru'      => 'RU',
			'sv'      => 'SE',
			'pl'      => 'PL',
			'tr'      => 'TR',
			'ko'      => 'KR',
			'zh-cn'   => 'CN',
			'zh-tw'   => 'TW',
			'he'      => 'IL',
			'ar'      => 'SA',
		];
		if ( isset( $map[ $norm ] ) ) {
			return (string) apply_filters( 'newsai_infer_geo_from_locale', $map[ $norm ], $locale );
		}
		$short = substr( $norm, 0, 2 );
		if ( isset( $map[ $short ] ) ) {
			return (string) apply_filters( 'newsai_infer_geo_from_locale', $map[ $short ], $locale );
		}

		return (string) apply_filters( 'newsai_infer_geo_from_locale', 'US', $locale );
	}
}

if ( ! function_exists( 'newsai_get_target_geo' ) ) {
	/**
	 * Target Geo for Trends, News RSS, publisher areaServed, and regional prompts.
	 * Uses saved Settings value, else infers from site locale (not hardcoded Greece).
	 *
	 * @return string ISO 3166-1 alpha-2.
	 */
	function newsai_get_target_geo() {
		$saved = get_option( 'newsai_geo', false );
		if ( is_string( $saved ) && $saved !== '' && preg_match( '/^[A-Za-z]{2}$/', $saved ) ) {
			return strtoupper( $saved );
		}

		return strtoupper( (string) apply_filters( 'newsai_default_geo', newsai_infer_geo_from_locale() ) );
	}
}

if ( ! function_exists( 'newsai_is_http_rest_request' ) ) {
    /**
     * True when this HTTP request is targeting the WordPress REST API.
     *
     * `REST_REQUEST` is only defined inside `rest_api_loaded()` (after `parse_request`),
     * while this plugin hooks `newsai_load_textdomain` on `plugins_loaded` priority 0.
     * Relying on `REST_REQUEST` alone therefore misses real REST traffic and still runs
     * `unload_textdomain()` / `load_textdomain()` during REST bootstrap — that has been
     * linked to Site Health failing the `?context=edit` check (JSON without `capabilities`).
     *
     * @return bool
     */
    function newsai_is_http_rest_request() {
        if ( ! empty( $GLOBALS['newsai_parse_request_is_rest'] ) ) {
            return true;
        }
        if ( function_exists( 'wp_is_serving_rest_request' ) && wp_is_serving_rest_request() ) {
            return true;
        }
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return true;
        }
        $prefix = function_exists( 'rest_get_url_prefix' ) ? (string) rest_get_url_prefix() : 'wp-json';
        $prefix = trim( $prefix, '/' );
        if ( $prefix === '' ) {
            $prefix = 'wp-json';
        }
        $needles = array( '/' . $prefix . '/', '/' . $prefix . '?' );
        $paths_to_check = array();
        foreach ( array( 'REQUEST_URI', 'PATH_INFO', 'REDIRECT_URL', 'HTTP_X_ORIGINAL_URL', 'HTTP_X_REWRITE_URL' ) as $srv_key ) {
            if ( empty( $_SERVER[ $srv_key ] ) || ! is_string( $_SERVER[ $srv_key ] ) ) {
                continue;
            }
            $raw = $_SERVER[ $srv_key ];
            $q   = strpos( $raw, '?' );
            $path = false !== $q ? substr( $raw, 0, $q ) : $raw;
            if ( $path !== '' ) {
                $paths_to_check[] = $path;
            }
        }
        foreach ( $paths_to_check as $path ) {
            foreach ( $needles as $needle ) {
                if ( false !== strpos( $path, $needle ) ) {
                    return true;
                }
            }
            if ( preg_match( '#/'
                . preg_quote( $prefix, '#' )
                . '/?$#', $path ) ) {
                return true;
            }
        }
        if ( isset( $_GET['rest_route'] ) && is_string( $_GET['rest_route'] ) && $_GET['rest_route'] !== '' ) {
            return true;
        }
        if ( ! empty( $_SERVER['QUERY_STRING'] ) && is_string( $_SERVER['QUERY_STRING'] )
            && preg_match( '/(^|&)rest_route=/', $_SERVER['QUERY_STRING'] ) ) {
            return true;
        }

        return false;
    }
}

if ( ! function_exists( 'newsai_filter_plugin_locale' ) ) {
    /**
     * Bridge the plugin's per-site language picker (`newsai_lang` option) to the
     * WordPress gettext stack so `__()`, `_e()`, etc. return translations for the
     * UI language the chief actually picked — not the global WP locale.
     *
     * Scoped to our own text domain so we never affect other plugins or core.
     *
     * @param string $locale WP locale candidate (e.g. en_US).
     * @param string $domain Text domain being loaded.
     * @return string
     */
    function newsai_filter_plugin_locale( $locale, $domain ) {
        if ( $domain !== 'newsroom-ai-assistant' ) {
            return $locale;
        }
        if ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) {
            return $locale;
        }
        $map  = newsai_supported_languages();
        $code = function_exists( 'newsai_get_ui_lang' ) ? newsai_get_ui_lang() : 'en';
        return isset( $map[ $code ]['locale'] ) ? $map[ $code ]['locale'] : $locale;
    }
}
add_filter( 'plugin_locale', 'newsai_filter_plugin_locale', 10, 2 );

if ( ! function_exists( 'newsai_load_textdomain' ) ) {
    /**
     * Load the gettext catalog from /languages directly with `load_textdomain()`
     * so the locale resolution is in our hands (the WP 6.7+ just-in-time loader
     * bypasses the `plugin_locale` filter, which would otherwise leave us stuck
     * on the global site locale even when the chief picked a different UI lang
     * from Settings → Newsroom OS).
     *
     * Hooked on `plugins_loaded` so the catalog is ready before the first
     * `__()` call in any of our includes, and re-applied on `init` as a safety
     * net for callers that include the plugin file later in the lifecycle.
     */
    function newsai_load_textdomain() {
        static $loaded = false;
        if ( $loaded ) {
            return;
        }
        if ( ! function_exists( 'newsai_supported_languages' ) ) {
            return;
        }
        if ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) {
            return;
        }
        $map  = newsai_supported_languages();
        $code = function_exists( 'newsai_get_ui_lang' ) ? newsai_get_ui_lang() : 'en';
        // Locale baked into the .mo filename (el, de_DE, …).
        $catalog_locale = isset( $map[ $code ]['locale'] ) ? $map[ $code ]['locale'] : 'en_US';

        $candidates = array( $catalog_locale );
        if ( strpos( $catalog_locale, '_' ) !== false ) {
            $candidates[] = substr( $catalog_locale, 0, strpos( $catalog_locale, '_' ) );
        }

        $mofile = '';
        foreach ( $candidates as $candidate ) {
            $try = NEWSAI_PATH . 'languages/newsroom-ai-assistant-' . $candidate . '.mo';
            if ( is_readable( $try ) ) {
                $mofile = $try;
                break;
            }
        }

        if ( $mofile === '' ) {
            load_plugin_textdomain(
                'newsroom-ai-assistant',
                false,
                dirname( plugin_basename( __FILE__ ) ) . '/languages'
            );
            return;
        }

        // WP 6.5+ stores catalogs under WP_Translation_Controller keyed by the
        // `$locale` passed here, but `__()` resolves using `determine_locale()`.
        // If we passed `$catalog_locale` (e.g. el) while the admin runs as en_US,
        // lookups hit an empty bucket and every string falls back to English.
        $lookup_locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
        if ( ! is_string( $lookup_locale ) || $lookup_locale === '' ) {
            $lookup_locale = $catalog_locale;
        }

        if ( function_exists( 'unload_textdomain' ) ) {
            unload_textdomain( 'newsroom-ai-assistant' );
        }
        load_textdomain( 'newsroom-ai-assistant', $mofile, $lookup_locale );
        $loaded = true;
    }
}
add_action( 'plugins_loaded', 'newsai_load_textdomain', 0 );

// Prime gettext before any `require_once` below can trigger `__()` on our domain.
// WordPress 6.7+ JIT may otherwise attach the catalog under the site locale first.
if ( function_exists( 'newsai_load_textdomain' ) && ! ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) ) {
    newsai_load_textdomain();
}

if ( ! function_exists( 'newsai_flag_rest_route_request' ) ) {
    /**
     * After WP parses the main request, record REST intent (front-end / loopback hits).
     * Admin screens often never parse rest_route; early detection still uses REQUEST_URI.
     *
     * @param WP $wp Main WP instance.
     */
    function newsai_flag_rest_route_request( $wp ) {
        if ( $wp instanceof WP && ! empty( $wp->query_vars['rest_route'] ) ) {
            $GLOBALS['newsai_parse_request_is_rest'] = true;
        }
    }
}
add_action( 'parse_request', 'newsai_flag_rest_route_request', 999, 1 );

if ( ! function_exists( 'newsai_resolve_script_translation_file' ) ) {
    /**
     * Route `wp_set_script_translations()` to predictable filenames under
     * `/languages` instead of the default md5-of-relative-path filename
     * computed by WordPress. Works regardless of the plugin folder name
     * (e.g. `newsroom-ai-assistant` vs `newsroom-ai-assistant-pro`).
     */
    function newsai_resolve_script_translation_file( $file, $handle, $domain ) {
        if ( 'newsroom-ai-assistant' !== $domain || 'newsai-script' !== $handle ) {
            return $file;
        }
        if ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) {
            return $file;
        }
        $candidates = array();
        if ( function_exists( 'newsai_supported_languages' ) && function_exists( 'newsai_get_ui_lang' ) ) {
            $map  = newsai_supported_languages();
            $code = newsai_get_ui_lang();
            if ( isset( $map[ $code ]['locale'] ) ) {
                $cat = $map[ $code ]['locale'];
                $candidates[] = NEWSAI_PATH . 'languages/' . $domain . '-' . $cat . '-' . $handle . '.json';
                if ( strpos( $cat, '_' ) !== false ) {
                    $candidates[] = NEWSAI_PATH . 'languages/' . $domain . '-' . substr( $cat, 0, strpos( $cat, '_' ) ) . '-' . $handle . '.json';
                }
            }
        }
        $wp_locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
        $candidates[] = NEWSAI_PATH . 'languages/' . $domain . '-' . $wp_locale . '-' . $handle . '.json';
        if ( is_string( $wp_locale ) && strpos( $wp_locale, '_' ) !== false ) {
            $candidates[] = NEWSAI_PATH . 'languages/' . $domain . '-' . substr( $wp_locale, 0, strpos( $wp_locale, '_' ) ) . '-' . $handle . '.json';
        }
        foreach ( $candidates as $candidate ) {
            if ( is_readable( $candidate ) ) {
                return $candidate;
            }
        }
        return $file;
    }
}
add_filter( 'load_script_translation_file', 'newsai_resolve_script_translation_file', 10, 3 );

// ==========================================
// 1. ΕΝΕΡΓΟΠΟΙΗΣΗ, DB & AUTO-UPDATER
// ==========================================
if ( ! defined( 'NEWSAI_DB_VERSION' ) ) {
	define( 'NEWSAI_DB_VERSION', '1.4' ); // Αυξάνουμε αυτό το νούμερο κάθε φορά που αλλάζουμε τη βάση
}

// Η συνάρτηση που χτίζει ή αναβαθμίζει τη βάση
if (!function_exists('newsroom_os_install_or_update_db')) {
    function newsroom_os_install_or_update_db() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'newsai_alerts';
        $messages_table = $wpdb->prefix . 'newsai_task_messages';
        $charset_collate = $wpdb->get_charset_collate();
        
        // Το νέο σχήμα της βάσης με τα Composite Indexes
        $sql = "CREATE TABLE $table_name (
            id mediumint(9) NOT NULL AUTO_INCREMENT,
            title varchar(255) NOT NULL,
            day tinyint(2) NOT NULL,
            month tinyint(2) NOT NULL,
            year smallint(4) NOT NULL,
            lead_days tinyint(2) DEFAULT 7 NOT NULL,
            author_id bigint(20) DEFAULT 0 NOT NULL,
            notes text DEFAULT '' NOT NULL,
            target_keyword varchar(255) DEFAULT '' NOT NULL,
            ref_link varchar(255) DEFAULT '' NOT NULL,
            status varchar(20) DEFAULT 'pending' NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_status_date (status, year, month, day),
            KEY idx_author_status (author_id, status)
        ) $charset_collate;";

        // Task ↔ chief / author messaging thread (Pro feature, but the schema ships for everyone
        // so upgrading a license afterwards doesn't trigger a schema migration).
        $sql_msg = "CREATE TABLE $messages_table (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            alert_id mediumint(9) NOT NULL,
            user_id bigint(20) NOT NULL,
            body text NOT NULL,
            created_utc datetime NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_alert (alert_id, created_utc),
            KEY idx_user (user_id)
        ) $charset_collate;";

        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
        dbDelta($sql_msg);

        if ( function_exists( 'newsai_load_plugin_module' ) ) {
            newsai_load_plugin_module( 'pro-gsc-sync.php' );
        }
        if ( function_exists( 'newsai_gsc_create_table' ) ) {
            newsai_gsc_create_table();
        }
        
        // Αποθηκεύουμε τη νέα έκδοση της βάσης
        update_option('newsai_db_version', NEWSAI_DB_VERSION);
    }
}

register_deactivation_hook( __FILE__, 'newsai_pro_on_plugin_deactivate' );
if ( ! function_exists( 'newsai_pro_on_plugin_deactivate' ) ) {
	function newsai_pro_on_plugin_deactivate() {
		wp_clear_scheduled_hook( 'newsai_pro_daily_license_check' );
		wp_clear_scheduled_hook( 'newsai_task_messages_purge' );
		wp_clear_scheduled_hook( 'nros_refresh_trend_keyword_cache' );
		wp_clear_scheduled_hook( 'newsai_breaking_expire_scan' );
		wp_clear_scheduled_hook( 'newsai_gsc_daily_sync' );
		wp_clear_scheduled_hook( 'newsai_usage_registry_weekly' );
		wp_clear_scheduled_hook( 'newsai_content_decay_scan' );
		wp_clear_scheduled_hook( 'newsai_seasonal_radar_refresh' );
		wp_clear_scheduled_hook( 'newsai_ai_alt_generate_event' );
		wp_clear_scheduled_hook( 'newsai_weekly_cleanup' );
		wp_clear_scheduled_hook( 'nros_schema_legacy_warm_batch' );
		wp_clear_scheduled_hook( 'nros_schema_nightly_warm_start' );
		wp_clear_scheduled_hook( 'nros_schema_nightly_warm_end' );
		wp_clear_scheduled_hook( 'nros_schema_nightly_warm_pulse' );
		if ( function_exists( 'newsai_usage_registry_revoke_consent' ) ) {
			newsai_usage_registry_revoke_consent();
		}
	}
}

// Τρέχει κατά τη ΝΕΑ ΕΓΚΑΤΑΣΤΑΣΗ
register_activation_hook(__FILE__, 'newsroom_os_activate_plugin');
if (!function_exists('newsroom_os_activate_plugin')) {
    function newsroom_os_activate_plugin() {
        newsroom_os_install_or_update_db();
        add_option('nros_schema_output_mode', 'hybrid');
        add_option('nros_do_activation_redirect', true);
        if ( function_exists( 'newsai_schedule_activation_crons' ) ) {
            newsai_schedule_activation_crons();
        } else {
            if ( function_exists( 'newsai_breaking_schedule_cron' ) ) {
                newsai_breaking_schedule_cron();
            }
            if ( function_exists( 'newsai_pro_schedule_license_cron' ) ) {
                newsai_pro_schedule_license_cron();
            }
            if ( function_exists( 'newsai_task_schedule_purge_cron' ) ) {
                newsai_task_schedule_purge_cron();
            }
        }
        if ( function_exists( 'newsai_retire_content_decay_radar' ) ) {
            newsai_retire_content_decay_radar();
        }
        if ( false === get_option( 'newsai_publisher_profile', false ) ) {
            add_option( 'newsai_publisher_profile', 'standard', '', false );
        }
        if ( false === get_option( 'newsai_geo', false ) && function_exists( 'newsai_infer_geo_from_locale' ) ) {
            add_option( 'newsai_geo', newsai_infer_geo_from_locale(), false );
        }
        if ( false === get_option( 'newsai_usage_registry_optin', false ) ) {
            add_option( 'newsai_usage_registry_optin', 'no', false );
        }
        if ( function_exists( 'newsai_detect_duplicate_plugin_folders' ) ) {
            newsai_detect_duplicate_plugin_folders();
        }
    }
}

if ( ! function_exists( 'newsai_find_plugin_install_folders' ) ) {
	/**
	 * All wp-content/plugins paths that contain newsroom-ai-assistant.php (top-level or one nested).
	 *
	 * @return string[] Folder paths relative to WP_PLUGIN_DIR, e.g. newsroom-ai-assistant-pro
	 */
	function newsai_find_plugin_install_folders() {
		if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
			return [];
		}
		$root  = wp_normalize_path( WP_PLUGIN_DIR );
		$found = [];
		foreach ( glob( $root . '/*/newsroom-ai-assistant.php' ) ?: [] as $main_file ) {
			$found[] = basename( dirname( $main_file ) );
		}
		foreach ( glob( $root . '/*/*/newsroom-ai-assistant.php' ) ?: [] as $nested_file ) {
			$rel     = wp_normalize_path( dirname( $nested_file ) );
			$found[] = ltrim( str_replace( $root, '', $rel ), '/' );
		}
		return array_values( array_unique( array_filter( $found ) ) );
	}
}

if ( ! function_exists( 'newsai_detect_duplicate_plugin_folders' ) ) {
	/**
	 * Flag duplicate installs (leftover folder + new upload) — common cause of white-screen fatals.
	 */
	function newsai_detect_duplicate_plugin_folders() {
		$folders = newsai_find_plugin_install_folders();
		if ( count( $folders ) > 1 ) {
			update_option( 'newsai_plugin_duplicate_folders', $folders, false );
		} else {
			delete_option( 'newsai_plugin_duplicate_folders' );
		}
	}
}

add_action( 'admin_init', 'newsai_maybe_warn_duplicate_plugin_folders', 5 );
if ( ! function_exists( 'newsai_maybe_warn_duplicate_plugin_folders' ) ) {
	function newsai_maybe_warn_duplicate_plugin_folders() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		newsai_detect_duplicate_plugin_folders();
		$folders = get_option( 'newsai_plugin_duplicate_folders', [] );
		if ( ! is_array( $folders ) || count( $folders ) < 2 ) {
			return;
		}
		add_action(
			'admin_notices',
			static function () use ( $folders ) {
				echo '<div class="notice notice-error"><p><strong>Newsroom OS:</strong> ';
				echo esc_html(
					sprintf(
						/* translators: %s: comma-separated plugin folder names under wp-content/plugins */
						__( 'Multiple plugin folders detected (%s). Deactivate, delete ALL copies via Plugins → Delete or FTP, then upload a single zip. A leftover folder after uninstall often causes a white screen on re-install.', 'newsroom-ai-assistant' ),
						implode( ', ', $folders )
					)
				);
				echo '</p></div>';
			}
		);
	}
}

// 🚀 FIX: Ο ΑΥΤΟΜΑΤΟΣ ΕΛΕΓΚΤΗΣ ΑΝΑΒΑΘΜΙΣΕΩΝ (Seamless Update Routine)
// Ελέγχει στο παρασκήνιο αν ο χρήστης έκανε Update το plugin. Αν ναι, αναβαθμίζει τη βάση!
add_action('plugins_loaded', 'newsroom_os_update_db_check');
if (!function_exists('newsroom_os_update_db_check')) {
    function newsroom_os_update_db_check() {
        if ( (string) get_option( 'newsai_db_version', '' ) !== (string) NEWSAI_DB_VERSION ) {
            newsroom_os_install_or_update_db();
        }
    }
}
// 🚀 FIX: Ανακατεύθυνση στο Wizard αμέσως μετά την ενεργοποίηση!
add_action('admin_init', 'nros_activation_redirect');
if (!function_exists('nros_activation_redirect')) {
    function nros_activation_redirect() {
        if ( ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_cron() || ! is_admin() ) {
            return;
        }
        if (get_option('nros_do_activation_redirect', false)) {
            delete_option('nros_do_activation_redirect');
            if (!isset($_GET['activate-multi'])) {
                wp_safe_redirect(admin_url('admin.php?page=newsroom-os-wizard'));
                exit;
            }
        }
    }
}

if ( ! function_exists( 'newsai_pro_build_admin_notice' ) ) {
	/**
	 * Visible reminder when Pro is force-enabled via wp-config constant.
	 */
	function newsai_pro_build_admin_notice() {
		if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		if ( ! defined( 'NEWSAI_PRO_BUILD' ) || ! NEWSAI_PRO_BUILD ) {
			return;
		}
		echo '<div class="notice notice-warning"><p>' . esc_html__( 'Newsroom OS: Pro features are active via NEWSAI_PRO_BUILD constant (development mode).', 'newsroom-ai-assistant' ) . '</p></div>';
	}
}
add_action( 'admin_notices', 'newsai_pro_build_admin_notice' );

// ==========================================
// 2. ΦΟΡΤΩΣΗ ΑΡΧΕΙΩΝ & ASSETS
// ==========================================
// Never add sidebar.php here: it echoes HTML/JS for the editor metabox and must only be
// loaded via newsai_render_sidebar() when the metabox callback runs.
// Core vs admin bundles: includes/bootstrap.php (frontend skips ~23 admin-only modules).
if ( file_exists( NEWSAI_PATH . 'includes/bootstrap.php' ) ) {
	require_once NEWSAI_PATH . 'includes/bootstrap.php';
}

if ( ! function_exists( 'newsai_get_script_vars' ) ) {
	/**
	 * Shared config for wp_localize_script (editor + settings).
	 *
	 * @return array<string, mixed>
	 */
	function newsai_get_script_vars() {
		global $post;
		$post_id = ( $post instanceof WP_Post ) ? (int) $post->ID : 0;
		$geo     = function_exists( 'newsai_get_post_target_geo' ) ? newsai_get_post_target_geo( $post_id ) : ( function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US' );
		return [
			'ajax_url'  => admin_url( 'admin-ajax.php' ),
			'home_url'  => home_url( '/' ),
			'nonce'     => wp_create_nonce( 'newsai_ajax_nonce' ),
			'post_id'   => $post_id,
			'lang'      => newsai_get_ui_lang(),
			'geo'       => $geo,
			'site_geo'  => function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US',
			'is_pro'    => function_exists( 'newsai_is_pro' ) && newsai_is_pro(),
			'ai_active' => function_exists( 'newsai_ai_is_active' ) && newsai_ai_is_active(),
			'can_plan_brief' => (
				current_user_can( 'edit_others_posts' )
				&& function_exists( 'newsai_is_pro' ) && newsai_is_pro()
				&& function_exists( 'newsai_ai_is_active' ) && newsai_ai_is_active()
			),
			'desk_radar_url' => admin_url( 'admin.php?page=newsai-settings&tab=alerts' ),
			'features'  => [
				'circular_gauge'      => ( get_option( 'newsai_ui_circular_gauge', 'yes' ) === 'yes' ),
				'onboarding_modal'    => ( get_option( 'newsai_ui_onboarding_modal', 'yes' ) === 'yes' ),
				'storytelling_auto'   => (
					function_exists( 'newsai_is_pro' ) && newsai_is_pro()
					&& get_option( 'newsai_timeline_mode', 'auto' ) === 'auto'
				),
				'mobile_bottom_sheet' => ( get_option( 'newsai_mobile_bottom_sheet', 'yes' ) === 'yes' ),
				'faq_block'           => ( get_option( 'newsai_editor_faq_enabled', 'no' ) === 'yes' ),
				'floating_ai_menu'    => ( get_option( 'newsai_editor_floating_ai', 'yes' ) === 'yes' ),
				'copy_prompts'        => true,
				'lazy_radar'          => true,
				'lite_mode'           => function_exists( 'newsai_lite_mode_active' ) && newsai_lite_mode_active(),
				'large_site_mode'     => function_exists( 'newsai_large_site_mode_enabled' ) && newsai_large_site_mode_enabled(),
				'skip_radar'          => (
					( function_exists( 'newsai_lite_mode_active' ) && newsai_lite_mode_active() )
					|| ( function_exists( 'newsai_large_site_mode_skips_radar' ) && newsai_large_site_mode_skips_radar() )
				),
			],
			'ui_text'   => [
				'focused_meta'        => __( 'Focused Excerpt/Meta field.', 'newsroom-ai-assistant' ),
				'start_links'         => __( 'Start with related links search.', 'newsroom-ai-assistant' ),
				'start_title'         => __( 'Start by refining the title.', 'newsroom-ai-assistant' ),
				'open_detail_first'   => __( 'Open detailed panel for the first fix.', 'newsroom-ai-assistant' ),
				'timeline_not_enough' => __( 'Not enough related published posts found for a timeline.', 'newsroom-ai-assistant' ),
				'timeline_no_tags'    => __( 'Add at least one tag to this post and save before building a timeline.', 'newsroom-ai-assistant' ),
				'radar_headline_copied' => __( 'Headline copied.', 'newsroom-ai-assistant' ),
				'radar_headline_copied_desk' => __( 'Headline copied. Plan AI briefs in Dashboard → Trend Radar.', 'newsroom-ai-assistant' ),
			],
			'discover_ui' => [
				'great_state_title'    => __( 'Great state: ready to publish.', 'newsroom-ai-assistant' ),
				'all_perfect'          => __( 'Everything looks perfect! Ready to publish.', 'newsroom-ai-assistant' ),
				'open_discover_hint'   => __( 'Open the Discover tab for details.', 'newsroom-ai-assistant' ),
				'badge_ready'          => __( 'READY', 'newsroom-ai-assistant' ),
				'badge_high'           => __( 'HIGH PRIORITY', 'newsroom-ai-assistant' ),
				'badge_medium'         => __( 'MEDIUM', 'newsroom-ai-assistant' ),
				'badge_low'            => __( 'LOW', 'newsroom-ai-assistant' ),
				'image_label'          => __( 'Image:', 'newsroom-ai-assistant' ),
				'featured_missing'     => __( 'Featured image missing', 'newsroom-ai-assistant' ),
				'up_to'                => __( 'Up to', 'newsroom-ai-assistant' ),
				'not_16_9'             => __( 'not 16:9', 'newsroom-ai-assistant' ),
				'timeliness_label'     => __( 'Timeliness:', 'newsroom-ai-assistant' ),
				'auto_timeline_active' => __( 'Auto Timeline active', 'newsroom-ai-assistant' ),
				'matches_open'         => __( 'Matches (', 'newsroom-ai-assistant' ),
				'trend_list_empty'     => __( 'Trend list warming up', 'newsroom-ai-assistant' ),
				'no_trend_match'       => __( 'No trend match yet', 'newsroom-ai-assistant' ),
				'trend_cache_age'      => __( 'Trend cache age: %s h', 'newsroom-ai-assistant' ),
				'clickbait_cues'       => __( 'clickbait cues', 'newsroom-ai-assistant' ),
				'headline_label'       => __( 'Headline:', 'newsroom-ai-assistant' ),
				'topic_label'          => __( 'Topic:', 'newsroom-ai-assistant' ),
				'og_label'             => __( 'OG image:', 'newsroom-ai-assistant' ),
				'og_save_hint'         => __( 'Set featured image or custom OG in Yoast/Rank Math', 'newsroom-ai-assistant' ),
				'og_via_featured'      => __( 'via featured image (SEO plugin fallback)', 'newsroom-ai-assistant' ),
				'og_explicit'          => __( 'custom image in Yoast/Rank Math', 'newsroom-ai-assistant' ),
				'author_label'         => __( 'Author:', 'newsroom-ai-assistant' ),
				'internal_links_label' => __( 'Internal links:', 'newsroom-ai-assistant' ),
				'links_covered_timeline' => __( 'Covered by Storytelling Timeline', 'newsroom-ai-assistant' ),
			],
		];
	}
}

if ( ! function_exists( 'newsai_post_content_has_block_class' ) ) {
	/**
	 * Whether saved post HTML uses Newsroom editor blocks (FAQ, key points).
	 */
	function newsai_post_content_has_block_class( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id < 1 ) {
			return false;
		}
		$content = (string) get_post_field( 'post_content', $post_id );
		if ( $content === '' ) {
			return false;
		}
		return (bool) preg_match( '/(newsai-faq|newsai-key-points|nros-geo-summary)/i', $content );
	}
}

if ( ! function_exists( 'newsai_post_content_has_story_cluster' ) ) {
	function newsai_post_content_has_story_cluster( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id < 1 ) {
			return false;
		}
		$content = (string) get_post_field( 'post_content', $post_id );
		if ( $content === '' ) {
			return false;
		}
		return (bool) preg_match( '/(newsai-story-cluster|newsroom-os-story-cluster|nros-timeline|newsroom-timeline)/i', $content );
	}
}

if ( ! function_exists( 'newsai_should_enqueue_auto_timeline_styles' ) ) {
	function newsai_should_enqueue_auto_timeline_styles() {
		if ( function_exists( 'newsai_lite_mode_skips_frontend_injections' ) && newsai_lite_mode_skips_frontend_injections() ) {
			return false;
		}
		if ( get_option( 'newsai_timeline_mode', 'auto' ) !== 'auto' ) {
			return false;
		}
		if ( ! function_exists( 'newsai_is_pro' ) || ! newsai_is_pro() ) {
			return false;
		}
		return is_singular( 'post' );
	}
}

if ( ! function_exists( 'newsai_enqueue_content_block_styles' ) ) {
	/**
	 * Styles for editor-inserted blocks (key points, FAQ) — no inline CSS in post HTML.
	 */
	function newsai_enqueue_content_block_styles() {
		$rel = file_exists( NEWSAI_PATH . 'assets/newsai-content-blocks.css' )
			? 'assets/newsai-content-blocks.css'
			: 'newsai-content-blocks.css';
		if ( ! file_exists( NEWSAI_PATH . $rel ) ) {
			return;
		}
		$ver = function_exists( 'newsai_asset_ver' ) ? newsai_asset_ver( $rel ) : NEWSAI_VERSION;
		wp_enqueue_style( 'newsai-content-blocks', NEWSAI_URL . $rel, [], $ver );
	}
}

if ( ! function_exists( 'newsai_enqueue_story_cluster_styles' ) ) {
	/**
	 * Styles for story cluster / related coverage blocks (no inline CSS in post HTML).
	 */
	function newsai_enqueue_story_cluster_styles() {
		$rel = file_exists( NEWSAI_PATH . 'assets/story-cluster.css' ) ? 'assets/story-cluster.css' : 'story-cluster.css';
		if ( file_exists( NEWSAI_PATH . $rel ) ) {
			$ver = function_exists( 'newsai_asset_ver' ) ? newsai_asset_ver( $rel ) : NEWSAI_VERSION;
			wp_enqueue_style( 'newsai-story-cluster', NEWSAI_URL . $rel, [], $ver );
		}
	}
}

if ( ! function_exists( 'newsai_enqueue_story_cluster_frontend' ) ) {
	function newsai_enqueue_story_cluster_frontend() {
		if ( ! is_singular( 'post' ) ) {
			return;
		}
		global $post;
		$post_id = ( $post instanceof WP_Post ) ? (int) $post->ID : (int) get_queried_object_id();
		if ( $post_id < 1 ) {
			return;
		}
		if ( newsai_post_content_has_block_class( $post_id ) ) {
			newsai_enqueue_content_block_styles();
		}
		if ( newsai_post_content_has_story_cluster( $post_id ) || newsai_should_enqueue_auto_timeline_styles() ) {
			newsai_enqueue_story_cluster_styles();
		}
	}
}
add_action( 'wp_enqueue_scripts', 'newsai_enqueue_story_cluster_frontend', 20 );

if ( ! function_exists( 'newsai_story_cluster_aria_label' ) ) {
	/**
	 * Accessible label for complementary story cluster blocks.
	 *
	 * @return string
	 */
	function newsai_story_cluster_aria_label() {
		return __( 'Related coverage', 'newsroom-ai-assistant' );
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_theme_styles' ) ) {
	/**
	 * Shared light/dark CSS variables for admin + editor surfaces.
	 */
	function newsai_enqueue_nros_theme_styles() {
		$rel = 'assets/nros-theme.css';
		if ( ! file_exists( NEWSAI_PATH . $rel ) ) {
			return;
		}
		wp_enqueue_style( 'newsai-nros-theme', NEWSAI_URL . $rel, [], NEWSAI_VERSION );
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_admin_dashboard_styles' ) ) {
	/**
	 * Settings / inbox dashboard layout (depends on theme tokens).
	 */
	function newsai_enqueue_nros_admin_dashboard_styles() {
		newsai_enqueue_nros_theme_styles();
		$rel = 'assets/nros-admin-dashboard.css';
		if ( ! file_exists( NEWSAI_PATH . $rel ) ) {
			return;
		}
		wp_enqueue_style( 'newsai-nros-admin-dashboard', NEWSAI_URL . $rel, [ 'newsai-nros-theme' ], NEWSAI_VERSION );
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_classic_polish' ) ) {
	/**
	 * Cross-cutting classic admin polish (Phase 5).
	 */
	function newsai_enqueue_nros_classic_polish() {
		$css = 'assets/nros-classic-polish.css';
		if ( file_exists( NEWSAI_PATH . $css ) ) {
			wp_enqueue_style(
				'newsai-nros-classic-polish',
				NEWSAI_URL . $css,
				[ 'newsai-nros-admin-dashboard' ],
				newsai_asset_ver( $css )
			);
		}
		$js = 'assets/nros-classic-polish.js';
		if ( file_exists( NEWSAI_PATH . $js ) ) {
			wp_enqueue_script(
				'newsai-nros-classic-polish',
				NEWSAI_URL . $js,
				[],
				newsai_asset_ver( $js ),
				true
			);
		}
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_chief_insights_styles' ) ) {
	/**
	 * Chief Insights command-center layout (settings → insights tab).
	 */
	function newsai_enqueue_nros_chief_insights_styles() {
		newsai_enqueue_nros_theme_styles();
		$rel = 'assets/nros-chief-insights.css';
		if ( ! file_exists( NEWSAI_PATH . $rel ) ) {
			return;
		}
		wp_enqueue_style( 'newsai-nros-chief-insights', NEWSAI_URL . $rel, [ 'newsai-nros-theme', 'newsai-nros-admin-dashboard' ], newsai_asset_ver( $rel ) );
	}
}

if ( ! function_exists( 'newsai_ensure_admin_vars_script' ) ) {
	/**
	 * Shared newsai_vars for admin tabs (palette, Chief Insights, War room) without script.js.
	 */
	function newsai_ensure_admin_vars_script() {
		static $done = false;
		if ( $done ) {
			return;
		}
		if ( ! wp_script_is( 'newsai-admin-vars', 'registered' ) ) {
			wp_register_script( 'newsai-admin-vars', false, [], NEWSAI_VERSION, true );
		}
		if ( ! wp_script_is( 'newsai-admin-vars', 'enqueued' ) ) {
			wp_enqueue_script( 'newsai-admin-vars' );
			$vars = function_exists( 'newsai_get_script_vars' )
				? newsai_get_script_vars()
				: [
					'ajax_url' => admin_url( 'admin-ajax.php' ),
					'nonce'    => wp_create_nonce( 'newsai_ajax_nonce' ),
					'geo'      => function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US',
					'lang'     => function_exists( 'newsai_get_ui_lang' ) ? newsai_get_ui_lang() : 'en',
					'is_pro'   => function_exists( 'newsai_is_pro' ) && newsai_is_pro(),
					'ai_active' => function_exists( 'newsai_ai_is_active' ) && newsai_ai_is_active(),
				];
			wp_localize_script( 'newsai-admin-vars', 'newsai_vars', $vars );
			wp_add_inline_script( 'newsai-admin-vars', 'window.newsai_vars = window.newsai_vars || newsai_vars;', 'after' );
		}
		$done = true;
	}
}

if ( ! function_exists( 'newsai_enqueue_admin_palette_bundle' ) ) {
	/**
	 * Lightweight ⌘K palette on Newsroom admin tabs without loading script.js.
	 */
	function newsai_enqueue_admin_palette_bundle() {
		newsai_ensure_admin_vars_script();
		if ( function_exists( 'newsai_enqueue_nros_command_palette' ) ) {
			newsai_enqueue_nros_command_palette();
		}
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_command_palette' ) ) {
	/**
	 * ⌘K / Ctrl+K command palette (editor + Newsroom settings).
	 */
	function newsai_enqueue_nros_command_palette() {
		newsai_enqueue_nros_theme_styles();
		if ( function_exists( 'newsai_ensure_admin_vars_script' ) ) {
			newsai_ensure_admin_vars_script();
		}
		$css_rel = 'assets/nros-command-palette.css';
		$js_rel  = 'assets/nros-command-palette.js';
		if ( file_exists( NEWSAI_PATH . $css_rel ) ) {
			wp_enqueue_style( 'newsai-nros-command-palette', NEWSAI_URL . $css_rel, [ 'newsai-nros-theme' ], NEWSAI_VERSION );
		}
		if ( file_exists( NEWSAI_PATH . $js_rel ) ) {
			$deps = [ 'wp-i18n', 'newsai-admin-vars' ];
			if ( wp_script_is( 'newsai-script', 'enqueued' ) ) {
				$deps[] = 'newsai-script';
			}
			wp_enqueue_script( 'newsai-nros-command-palette', NEWSAI_URL . $js_rel, $deps, newsai_asset_ver( $js_rel ), true );
			if ( function_exists( 'wp_set_script_translations' ) ) {
				wp_set_script_translations(
					'newsai-nros-command-palette',
					'newsroom-ai-assistant',
					NEWSAI_PATH . 'languages'
				);
			}
		}
	}
}

if ( ! function_exists( 'newsai_enqueue_editor_assets' ) ) {
	/**
	 * Enqueue sidebar JS/CSS (idempotent). Themes that reorder admin scripts still get newsai_vars on window.
	 */
	function newsai_enqueue_editor_assets() {
		newsai_enqueue_nros_theme_styles();
		newsai_enqueue_story_cluster_styles();
		newsai_enqueue_content_block_styles();
		$css_path = file_exists( NEWSAI_PATH . 'assets/style.css' ) ? 'assets/style.css' : 'style.css';
		$js_path  = file_exists( NEWSAI_PATH . 'assets/script.js' ) ? 'assets/script.js' : 'script.js';
		wp_enqueue_style( 'newsai-style', NEWSAI_URL . $css_path, [ 'newsai-nros-theme' ], NEWSAI_VERSION );
		wp_enqueue_script( 'newsai-script', NEWSAI_URL . $js_path, [ 'jquery', 'wp-i18n' ], NEWSAI_VERSION, true );
		if ( function_exists( 'wp_set_script_translations' ) ) {
			wp_set_script_translations(
				'newsai-script',
				'newsroom-ai-assistant',
				NEWSAI_PATH . 'languages'
			);
		}
		wp_localize_script( 'newsai-script', 'newsai_vars', newsai_get_script_vars() );
		wp_add_inline_script( 'newsai-script', 'window.newsai_vars = window.newsai_vars || newsai_vars;', 'after' );
		if ( function_exists( 'newsai_enqueue_nros_ai_float' ) ) {
			newsai_enqueue_nros_ai_float();
		}
		if ( function_exists( 'newsai_enqueue_nros_command_palette' ) ) {
			newsai_enqueue_nros_command_palette();
		}
	}
}

if ( ! function_exists( 'newsai_enqueue_nros_ai_float' ) ) {
	/**
	 * Floating AI toolbar on text selection (Pro BYOK, editor only).
	 */
	function newsai_enqueue_nros_ai_float() {
		if ( function_exists( 'newsai_lite_mode_skips_post_editor_ui' ) && newsai_lite_mode_skips_post_editor_ui() ) {
			return;
		}
		if ( ! function_exists( 'newsai_ai_is_active' ) || ! newsai_ai_is_active() ) {
			return;
		}
		if ( get_option( 'newsai_editor_floating_ai', 'yes' ) !== 'yes' ) {
			return;
		}
		$css_rel = 'assets/nros-ai-float.css';
		$js_rel  = 'assets/nros-ai-float.js';
		if ( file_exists( NEWSAI_PATH . $css_rel ) ) {
			wp_enqueue_style( 'newsai-nros-ai-float', NEWSAI_URL . $css_rel, [], function_exists( 'newsai_asset_ver' ) ? newsai_asset_ver( $css_rel ) : NEWSAI_VERSION );
		}
		if ( ! file_exists( NEWSAI_PATH . $js_rel ) ) {
			return;
		}
		$float_deps = [ 'jquery', 'wp-i18n', 'newsai-script' ];
		$screen     = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
			$float_deps[] = 'wp-data';
			$float_deps[] = 'wp-rich-text';
		}
		wp_enqueue_script(
			'newsai-nros-ai-float',
			NEWSAI_URL . $js_rel,
			$float_deps,
			function_exists( 'newsai_asset_ver' ) ? newsai_asset_ver( $js_rel ) : NEWSAI_VERSION,
			true
		);
		if ( function_exists( 'wp_set_script_translations' ) ) {
			wp_set_script_translations(
				'newsai-nros-ai-float',
				'newsroom-ai-assistant',
				NEWSAI_PATH . 'languages'
			);
		}
		$gb_rel = 'assets/nros-ai-float-gutenberg.js';
		if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && file_exists( NEWSAI_PATH . $gb_rel ) ) {
			wp_enqueue_script(
				'newsai-nros-ai-float-gutenberg',
				NEWSAI_URL . $gb_rel,
				[ 'wp-hooks', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'newsai-nros-ai-float' ],
				function_exists( 'newsai_asset_ver' ) ? newsai_asset_ver( $gb_rel ) : NEWSAI_VERSION,
				true
			);
			if ( function_exists( 'wp_set_script_translations' ) ) {
				wp_set_script_translations(
					'newsai-nros-ai-float-gutenberg',
					'newsroom-ai-assistant',
					NEWSAI_PATH . 'languages'
				);
			}
		}
	}
}

add_action('admin_enqueue_scripts', 'newsai_enqueue_assets');
if (!function_exists('newsai_enqueue_assets')) {
    function newsai_enqueue_assets($hook) {
        $hook = is_string( $hook ) ? $hook : '';
        $is_settings = ( strpos( $hook, 'newsai-settings' ) !== false );
        $is_inbox    = ( strpos( $hook, 'newsai-inbox' ) !== false );
        $tab         = function_exists( 'newsai_admin_current_tab' ) ? newsai_admin_current_tab() : 'alerts';

        if ( $hook === 'post.php' || $hook === 'post-new.php' ) {
            if ( function_exists( 'newsai_admin_screen_needs_editor_bundle' ) && newsai_admin_screen_needs_editor_bundle( $hook ) ) {
                newsai_enqueue_editor_assets();
            }
            return;
        }

        if ( ! $is_settings && ! $is_inbox ) {
            return;
        }

        if ( function_exists( 'newsai_enqueue_nros_admin_dashboard_styles' ) ) {
            newsai_enqueue_nros_admin_dashboard_styles();
        }
        if ( function_exists( 'newsai_enqueue_nros_classic_polish' ) ) {
            newsai_enqueue_nros_classic_polish();
        }

        if ( function_exists( 'newsai_admin_screen_needs_editor_bundle' ) && newsai_admin_screen_needs_editor_bundle( $hook ) ) {
            newsai_enqueue_editor_assets();
        } elseif ( function_exists( 'newsai_enqueue_admin_palette_bundle' ) ) {
            newsai_enqueue_admin_palette_bundle();
        }

        if ( $is_settings ) {
            if ( $tab === 'insights' && function_exists( 'newsai_enqueue_nros_chief_insights_styles' ) ) {
                newsai_enqueue_nros_chief_insights_styles();
            }
            if ( $tab === 'alerts' && file_exists( NEWSAI_PATH . 'assets/nros-kanban.js' ) ) {
                wp_enqueue_script( 'newsai-nros-kanban', NEWSAI_URL . 'assets/nros-kanban.js', [ 'jquery' ], NEWSAI_VERSION, true );
                if ( function_exists( 'newsai_get_script_vars' ) ) {
                    wp_localize_script( 'newsai-nros-kanban', 'newsai_vars', newsai_get_script_vars() );
                }
            }
            if ( $tab === 'alerts' && file_exists( NEWSAI_PATH . 'assets/nros-dashboard-radar.js' ) ) {
                if ( function_exists( 'newsai_ensure_admin_vars_script' ) ) {
                    newsai_ensure_admin_vars_script();
                }
                wp_enqueue_script(
                    'newsai-nros-dashboard-radar',
                    NEWSAI_URL . 'assets/nros-dashboard-radar.js',
                    [ 'jquery', 'newsai-admin-vars' ],
                    newsai_asset_ver( 'assets/nros-dashboard-radar.js' ),
                    true
                );
            }
            if ( $tab === 'alerts' && file_exists( NEWSAI_PATH . 'assets/nros-content-brief.js' ) ) {
                if ( function_exists( 'newsai_ensure_admin_vars_script' ) ) {
                    newsai_ensure_admin_vars_script();
                }
                wp_enqueue_script(
                    'newsai-nros-content-brief',
                    NEWSAI_URL . 'assets/nros-content-brief.js',
                    [ 'jquery', 'wp-i18n', 'newsai-admin-vars' ],
                    newsai_asset_ver( 'assets/nros-content-brief.js' ),
                    true
                );
                if ( function_exists( 'wp_set_script_translations' ) ) {
                    wp_set_script_translations(
                        'newsai-nros-content-brief',
                        'newsroom-ai-assistant',
                        NEWSAI_PATH . 'languages'
                    );
                }
            }
            if ( $tab === 'alerts' && file_exists( NEWSAI_PATH . 'assets/nros-war-room.js' ) ) {
                wp_enqueue_script( 'newsai-nros-war-room', NEWSAI_URL . 'assets/nros-war-room.js', [ 'jquery' ], NEWSAI_VERSION, true );
                if ( function_exists( 'newsai_get_script_vars' ) ) {
                    wp_localize_script( 'newsai-nros-war-room', 'newsai_vars', newsai_get_script_vars() );
                }
            }
            if ( $tab === 'insights' && file_exists( NEWSAI_PATH . 'assets/nros-chief-insights.js' ) ) {
                $ci_deps = [ 'jquery' ];
                if ( wp_script_is( 'newsai-admin-vars', 'registered' ) || wp_script_is( 'newsai-admin-vars', 'enqueued' ) ) {
                    $ci_deps[] = 'newsai-admin-vars';
                }
                wp_enqueue_script( 'newsai-nros-chief-insights', NEWSAI_URL . 'assets/nros-chief-insights.js', $ci_deps, newsai_asset_ver( 'assets/nros-chief-insights.js' ), true );
                if ( function_exists( 'newsai_get_script_vars' ) && ! wp_script_is( 'newsai-admin-vars', 'enqueued' ) ) {
                    wp_localize_script( 'newsai-nros-chief-insights', 'newsai_vars', newsai_get_script_vars() );
                }
            }
            if ( $tab === 'insights' && file_exists( NEWSAI_PATH . 'assets/nros-war-room.js' ) ) {
                $wr_deps = [ 'jquery' ];
                if ( wp_script_is( 'newsai-admin-vars', 'registered' ) || wp_script_is( 'newsai-admin-vars', 'enqueued' ) ) {
                    $wr_deps[] = 'newsai-admin-vars';
                }
                wp_enqueue_script( 'newsai-nros-war-room', NEWSAI_URL . 'assets/nros-war-room.js', $wr_deps, newsai_asset_ver( 'assets/nros-war-room.js' ), true );
                if ( function_exists( 'newsai_get_script_vars' ) && ! wp_script_is( 'newsai-admin-vars', 'enqueued' ) ) {
                    wp_localize_script( 'newsai-nros-war-room', 'newsai_vars', newsai_get_script_vars() );
                }
            }
        }
    }
}

if ( ! function_exists( 'newsai_enqueue_assets_block_editor' ) ) {
	/**
	 * Block editor + some themes (Ruby, etc.) load assets on this hook instead of admin_enqueue_scripts order.
	 */
	function newsai_enqueue_assets_block_editor() {
		if ( function_exists( 'newsai_lite_mode_skips_post_editor_ui' ) && newsai_lite_mode_skips_post_editor_ui() ) {
			return;
		}
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen || $screen->base !== 'post' ) {
			return;
		}
		if ( ! is_object_in_taxonomy( (string) $screen->post_type, 'post_tag' ) ) {
			return;
		}
		newsai_enqueue_editor_assets();
	}
}
add_action( 'enqueue_block_editor_assets', 'newsai_enqueue_assets_block_editor', 20 );

if ( ! function_exists( 'newsai_ensure_editor_assets_footer' ) ) {
	/**
	 * Last-chance enqueue before admin footer (Classic editor themes that dequeue in head).
	 */
	function newsai_ensure_editor_assets_footer() {
		global $pagenow;
		if ( $pagenow !== 'post.php' && $pagenow !== 'post-new.php' ) {
			return;
		}
		if ( function_exists( 'newsai_lite_mode_skips_post_editor_ui' ) && newsai_lite_mode_skips_post_editor_ui() ) {
			return;
		}
		if ( function_exists( 'newsai_enqueue_editor_assets' ) && ! wp_script_is( 'newsai-script', 'done' ) && ! wp_script_is( 'newsai-script', 'enqueued' ) ) {
			newsai_enqueue_editor_assets();
		}
	}
}
add_action( 'admin_print_footer_scripts', 'newsai_ensure_editor_assets_footer', 5 );

// ==========================================
// 3. EDITOR SIDEBAR 
// ==========================================
add_action('add_meta_boxes', 'newsai_add_sidebar');
if (!function_exists('newsai_add_sidebar')) {
    function newsai_add_sidebar() {
        if ( function_exists( 'newsai_lite_mode_skips_post_editor_ui' ) && newsai_lite_mode_skips_post_editor_ui() ) {
            return;
        }
        add_meta_box('newsai_sidebar', '🚀 Newsroom OS: Editorial Control', 'newsai_render_sidebar', ['post'], 'side', 'high');
    }
}

// ==========================================
// TAG-GUARD: Gutenberg snackbar + Classic AJAX inline notice + admin_notices fallback
// ==========================================
if (!function_exists('newsai_tag_guard_enqueue_block_editor')) {
    function newsai_tag_guard_enqueue_block_editor() {
        if (!function_exists('newsai_tag_guard_is_active') || !newsai_tag_guard_is_active()) {
            return;
        }
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        $post_type = $screen ? (string) $screen->post_type : '';
        if ($post_type === '' || !is_object_in_taxonomy($post_type, 'post_tag')) {
            return;
        }
        $rel = file_exists(NEWSAI_PATH . 'assets/tag-guard-editor.js') ? 'assets/tag-guard-editor.js' : 'tag-guard-editor.js';
        if (!file_exists(NEWSAI_PATH . $rel)) {
            return;
        }
        wp_enqueue_script(
            'newsai-tag-guard-editor',
            NEWSAI_URL . $rel,
            ['wp-api-fetch', 'wp-data', 'wp-i18n'],
            NEWSAI_VERSION,
            true
        );
    }
}
add_action('enqueue_block_editor_assets', 'newsai_tag_guard_enqueue_block_editor');

if (!function_exists('newsai_tag_guard_enqueue_classic_editor')) {
    function newsai_tag_guard_enqueue_classic_editor($hook) {
        if (!function_exists('newsai_tag_guard_is_active') || !newsai_tag_guard_is_active()) {
            return;
        }
        $hook = is_string( $hook ) ? $hook : '';
        if ($hook !== 'post.php' && $hook !== 'post-new.php' && $hook !== 'edit-tags.php' && $hook !== 'term.php') {
            return;
        }
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if ($screen && $hook !== 'edit-tags.php' && $hook !== 'term.php') {
            $pt = (string) $screen->post_type;
            if ($pt === '' || !is_object_in_taxonomy($pt, 'post_tag')) {
                return;
            }
        }
        $rel = file_exists(NEWSAI_PATH . 'assets/tag-guard-classic.js') ? 'assets/tag-guard-classic.js' : 'tag-guard-classic.js';
        if ( ! file_exists( NEWSAI_PATH . $rel ) ) {
            return;
        }
        $skip_editor_bundle = ( $hook === 'post.php' || $hook === 'post-new.php' )
            && function_exists( 'newsai_lite_mode_skips_post_editor_ui' )
            && newsai_lite_mode_skips_post_editor_ui();
        if ( ! $skip_editor_bundle && function_exists( 'newsai_enqueue_editor_assets' ) ) {
            newsai_enqueue_editor_assets();
        }
        wp_enqueue_script(
            'newsai-tag-guard-classic',
            NEWSAI_URL . $rel,
            ['jquery', 'tags-box'],
            NEWSAI_VERSION,
            true
        );
        wp_localize_script('newsai-tag-guard-classic', 'newsaiTagGuard', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'nonce'   => wp_create_nonce('newsai_tag_guard'),
            'lang'    => function_exists('newsai_get_ui_lang') ? newsai_get_ui_lang() : 'en',
        ]);
    }
}
add_action('admin_enqueue_scripts', 'newsai_tag_guard_enqueue_classic_editor');

if (!function_exists('newsai_tag_guard_ajax_check')) {
    function newsai_tag_guard_ajax_check() {
        check_ajax_referer('newsai_tag_guard', 'nonce');
        if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) {
            wp_send_json_error( [ 'message' => 'unauthorized' ], 403 );
        }
        $term = isset($_POST['term']) ? sanitize_text_field(wp_unslash($_POST['term'])) : '';
        $taxonomy = isset($_POST['taxonomy']) ? sanitize_key(wp_unslash($_POST['taxonomy'])) : 'post_tag';
        $tax_obj = get_taxonomy($taxonomy);
        if (!$tax_obj || !current_user_can($tax_obj->cap->assign_terms)) {
            wp_send_json_error(['message' => 'forbidden'], 403);
        }
        if ($term === '' || $taxonomy !== 'post_tag') {
            wp_send_json_success(['block' => false]);
        }
        if (!function_exists('newsai_tag_guard_is_active') || !newsai_tag_guard_is_active()) {
            wp_send_json_success(['block' => false]);
        }
        $result = newsai_tag_guard_prevent_near_duplicates($term, $taxonomy);
        if (is_wp_error($result) && $result->get_error_code() === 'newsai_tag_duplicate_suspect') {
            $data = $result->get_error_data();
            delete_transient('newsai_tag_guard_notice_' . get_current_user_id());
            wp_send_json_success([
                'block'          => true,
                'message'        => $result->get_error_message(),
                'suggested_id'   => isset($data['suggested_id']) ? (int) $data['suggested_id'] : 0,
                'suggested_name' => isset($data['suggested_name']) ? (string) $data['suggested_name'] : '',
                'original'       => isset($data['original']) ? (string) $data['original'] : $term,
            ]);
        }
        wp_send_json_success(['block' => false]);
    }
}
add_action('wp_ajax_newsai_tag_guard_check', 'newsai_tag_guard_ajax_check');

if (!function_exists('newsai_tag_guard_admin_notice')) {
    function newsai_tag_guard_admin_notice() {
        if (!is_user_logged_in()) {
            return;
        }
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if ($screen && !in_array($screen->base, ['post', 'edit-tags', 'term'], true)) {
            return;
        }
        $key = 'newsai_tag_guard_notice_' . get_current_user_id();
        $msg = get_transient($key);
        if (empty($msg) || !is_string($msg)) {
            return;
        }
        delete_transient($key);
        echo '<div class="notice notice-warning is-dismissible"><p><strong>Newsroom OS:</strong> ' . esc_html($msg) . '</p></div>';
    }
}
add_action('admin_notices', 'newsai_tag_guard_admin_notice');

if (!function_exists('newsai_render_sidebar')) {
    /**
     * Metabox callback: $post is the current editor post from WordPress. Keep global $post
     * in sync so sidebar.php can resolve the post ID safely.
     *
     * @param WP_Post|null $the_post Post being edited.
     * @param mixed        $box      Metabox args (unused).
     */
    function newsai_render_sidebar( $the_post = null, $box = null ) {
        global $post;
        if ( $the_post instanceof WP_Post ) {
            $post = $the_post;
        }
        // Themes (Classic editor / Ruby, etc.) sometimes dequeue newsai-script; re-enqueue when the metabox renders.
        if ( function_exists( 'newsai_enqueue_editor_assets' ) && ! wp_script_is( 'newsai-script', 'enqueued' ) ) {
            newsai_enqueue_editor_assets();
        }
        // Only sidebar.php may define this flag; prevents accidental output if the file is ever required elsewhere.
        $GLOBALS['newsai_sidebar_metabox_render'] = true;
        try {
            if (file_exists(NEWSAI_PATH . 'sidebar.php')) {
                require NEWSAI_PATH . 'sidebar.php';
            } elseif (file_exists(NEWSAI_PATH . 'includes/sidebar.php')) {
                require NEWSAI_PATH . 'includes/sidebar.php';
            }
        } finally {
            unset( $GLOBALS['newsai_sidebar_metabox_render'] );
        }
    }
}

// ------------------------------------------------------------------
// AJAX ENDPOINTS & SERVER PROTECTION (Caching)
// ------------------------------------------------------------------
if ( ! function_exists( 'newsai_editor_ajax_gate' ) ) {
	/**
	 * Baseline gate for editor sidebar AJAX: CSRF nonce + edit_posts (not a substitute for object-level checks).
	 */
	function newsai_editor_ajax_gate() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( [ 'message' => 'unauthorized' ], 403 );
		}
	}
}

if ( ! function_exists( 'newsai_editor_ajax_require_post_edit' ) ) {
	/**
	 * When a handler receives a saved post ID, ensure the user may edit that post.
	 *
	 * @param int $post_id Post ID (0 = skip, e.g. unsaved draft screen).
	 */
	function newsai_editor_ajax_require_post_edit( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id < 1 ) {
			return;
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
	}
}

add_action('wp_ajax_newsai_mark_task_done', 'newsai_ajax_mark_task_done');
if (!function_exists('newsai_ajax_mark_task_done')) {
    function newsai_ajax_mark_task_done() {
        check_ajax_referer('newsai_ajax_nonce', 'nonce');
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( null, 401 );
        }
        $task_id = isset($_POST['task_id']) ? (int) $_POST['task_id'] : 0;
        if ( $task_id < 1 ) {
            wp_send_json_error( null, 400 );
        }
        if ( ! function_exists( 'newsai_task_can_access' ) || ! newsai_task_can_access( $task_id ) ) {
            wp_send_json_error( null, 403 );
        }
        global $wpdb;
        $wpdb->update( $wpdb->prefix . 'newsai_alerts', [ 'status' => 'done' ], [ 'id' => $task_id ] );
        delete_transient( 'nros_tasks_user_v3_' . get_current_user_id() );
        delete_transient( 'nros_tasks_user_' . get_current_user_id() );
        delete_transient( 'nros_tasks_user_0' );
        if ( function_exists( 'newsai_invalidate_task_due_metrics_cache' ) ) {
            newsai_invalidate_task_due_metrics_cache();
        }
        if ( function_exists( 'nros_clear_task_transients' ) ) {
            nros_clear_task_transients();
        }
        do_action( 'newsai_task_completed', $task_id, (int) get_current_user_id() );
        wp_send_json_success();
    }
}

add_action('wp_ajax_newsai_get_related_posts', 'newsai_ajax_get_related_posts');
if (!function_exists('newsai_ajax_get_related_posts')) {
    function newsai_ajax_get_related_posts() {
        newsai_editor_ajax_gate();
        $keyword = isset($_POST['keyword']) ? sanitize_text_field($_POST['keyword']) : '';
        $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
        newsai_editor_ajax_require_post_edit( $post_id );
        $cache_key = 'newsai_rel_' . md5( $keyword . '|' . $post_id );
        $cached = get_transient( $cache_key );
        if ( false !== $cached && is_array( $cached ) ) {
            wp_send_json_success( $cached );
        }
        $query = new WP_Query(['s' => $keyword, 'post_type' => 'post', 'posts_per_page' => 5, 'post__not_in' => [$post_id], 'post_status' => 'publish']);
        $results = [];
        if ($query->have_posts()) {
            while ($query->have_posts()) { $query->the_post(); $results[] = ['title' => get_the_title(), 'url' => get_permalink()]; }
        }
        wp_reset_postdata();
        set_transient( $cache_key, $results, 15 * MINUTE_IN_SECONDS );
        wp_send_json_success($results);
    }
}

if ( ! function_exists( 'newsai_sem_links_profiles' ) ) {
	/**
	 * Presets for editor “Smart archive links” (internal link suggestions).
	 *
	 * @return array<string, array{label:string, min_chars:int, min_age_days:int, min_score:int, max_results:int, query_limit:int}>
	 */
	function newsai_sem_links_profiles() {
		return [
			'strict'   => [
				'label'       => __( 'Strict evergreen (45+ days, 3 links)', 'newsroom-ai-assistant' ),
				'min_chars'   => 80,
				'min_age_days' => 45,
				'min_score'   => 6,
				'max_results' => 3,
				'query_limit' => 30,
			],
			'balanced' => [
				'label'       => __( 'Balanced — recommended (30+ days, 5 links)', 'newsroom-ai-assistant' ),
				'min_chars'   => 60,
				'min_age_days' => 30,
				'min_score'   => 5,
				'max_results' => 5,
				'query_limit' => 50,
			],
			'relaxed'  => [
				'label'       => __( 'Relaxed archive (21+ days, 8 links)', 'newsroom-ai-assistant' ),
				'min_chars'   => 50,
				'min_age_days' => 21,
				'min_score'   => 5,
				'max_results' => 8,
				'query_limit' => 60,
			],
			'wide'     => [
				'label'       => __( 'Wide coverage (14+ days, 8 links)', 'newsroom-ai-assistant' ),
				'min_chars'   => 50,
				'min_age_days' => 14,
				'min_score'   => 4,
				'max_results' => 8,
				'query_limit' => 80,
			],
		];
	}
}

if ( ! function_exists( 'newsai_sem_links_config' ) ) {
	/**
	 * Active preset for semantic / archive link suggestions.
	 *
	 * @return array{label:string, min_chars:int, min_age_days:int, min_score:int, max_results:int, query_limit:int}
	 */
	function newsai_sem_links_config() {
		$profiles = newsai_sem_links_profiles();
		$key      = (string) get_option( 'newsai_sem_links_profile', 'balanced' );
		if ( ! isset( $profiles[ $key ] ) ) {
			$key = 'balanced';
		}
		return $profiles[ $key ];
	}
}

if ( ! function_exists( 'newsai_sem_links_archive_cutoff_gmt' ) ) {
	/**
	 * Published posts on or before this GMT datetime count as archive candidates.
	 *
	 * @param int $min_age_days Minimum age in days.
	 */
	function newsai_sem_links_archive_cutoff_gmt( $min_age_days ) {
		$min_age_days = max( 1, (int) $min_age_days );
		return gmdate( 'Y-m-d H:i:s', strtotime( '-' . $min_age_days . ' days', current_time( 'timestamp', true ) ) );
	}
}

if ( ! function_exists( 'newsai_sem_links_normalize_match_text' ) ) {
	function newsai_sem_links_normalize_match_text( $text ) {
		$text = mb_strtolower( wp_strip_all_tags( (string) $text ), 'UTF-8' );
		if ( function_exists( 'nros_global_unaccent' ) ) {
			$text = nros_global_unaccent( $text );
		}
		return $text;
	}
}

if ( ! function_exists( 'newsai_sem_links_text_has_token' ) ) {
	function newsai_sem_links_text_has_token( $haystack_norm, $token ) {
		$token = newsai_sem_links_normalize_match_text( $token );
		if ( $token === '' ) {
			return false;
		}
		return mb_stripos( (string) $haystack_norm, $token, 0, 'UTF-8' ) !== false;
	}
}

if ( ! function_exists( 'newsai_sem_links_fetch_ids_by_terms' ) ) {
	/**
	 * Archive posts sharing taxonomy terms (tag or category).
	 *
	 * @param int[]  $term_ids Term IDs.
	 * @param string $taxonomy post_tag|category.
	 * @param int    $post_id  Exclude.
	 * @param string $before_gmt Cutoff datetime (GMT).
	 * @param int    $limit    Max rows.
	 * @return int[]
	 */
	function newsai_sem_links_fetch_ids_by_terms( $term_ids, $taxonomy, $post_id, $before_gmt, $limit ) {
		global $wpdb;
		$term_ids = array_values( array_filter( array_map( 'intval', (array) $term_ids ) ) );
		$limit    = max( 1, (int) $limit );
		if ( empty( $term_ids ) ) {
			return [];
		}
		$placeholders = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
		$args         = array_merge( $term_ids, [ $taxonomy, (int) $post_id, $before_gmt, $limit ] );
		return array_map(
			'intval',
			(array) $wpdb->get_col(
				$wpdb->prepare(
					"
			SELECT DISTINCT p.ID
			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
			WHERE tt.term_id IN ($placeholders)
			  AND tt.taxonomy = %s
			  AND p.ID != %d
			  AND p.post_status = 'publish'
			  AND p.post_type = 'post'
			  AND p.post_date_gmt <= %s
			ORDER BY p.post_date_gmt DESC
			LIMIT %d
		",
					...$args
				)
			)
		);
	}
}

if ( ! function_exists( 'newsai_sem_links_fetch_ids_by_title_words' ) ) {
	/**
	 * OR-match significant words in post_title (works when core `s` search returns nothing).
	 *
	 * @param string[] $words Significant tokens.
	 * @return int[]
	 */
	function newsai_sem_links_fetch_ids_by_title_words( $words, $post_id, $before_gmt, $limit ) {
		global $wpdb;
		$words = array_values(
			array_filter(
				(array) $words,
				function ( $w ) {
					return mb_strlen( (string) $w, 'UTF-8' ) >= 3;
				}
			)
		);
		$words = array_slice( array_unique( $words ), 0, 5 );
		$limit = max( 1, (int) $limit );
		if ( empty( $words ) ) {
			return [];
		}
		$likes = [];
		$args  = [ (int) $post_id ];
		foreach ( $words as $w ) {
			$likes[] = 'p.post_title LIKE %s';
			$args[]  = '%' . $wpdb->esc_like( (string) $w ) . '%';
		}
		$args[] = $before_gmt;
		$args[] = $limit;
		return array_map(
			'intval',
			(array) $wpdb->get_col(
				$wpdb->prepare(
					"
			SELECT DISTINCT p.ID
			FROM {$wpdb->posts} p
			WHERE p.post_type = 'post'
			  AND p.post_status = 'publish'
			  AND p.ID != %d
			  AND (" . implode( ' OR ', $likes ) . ")
			  AND p.post_date_gmt <= %s
			ORDER BY p.post_date_gmt DESC
			LIMIT %d
		",
					...$args
				)
			)
		);
	}
}

if ( ! function_exists( 'newsai_sem_links_collect_candidate_ids' ) ) {
	/**
	 * Build a deduped pool of archive post IDs (tags, categories, title tokens).
	 *
	 * @param int      $post_id Current post (0 = unsaved draft).
	 * @param string[] $words   Significant words from draft.
	 * @param array    $cfg     Preset from newsai_sem_links_config().
	 * @return int[]
	 */
	function newsai_sem_links_collect_candidate_ids( $post_id, $words, $cfg ) {
		$post_id    = (int) $post_id;
		$limit      = max( 20, (int) ( $cfg['query_limit'] ?? 50 ) );
		$min_age    = max( 7, (int) ( $cfg['min_age_days'] ?? 30 ) );
		$before_gmt = newsai_sem_links_archive_cutoff_gmt( $min_age );
		$per_source = (int) min( $limit, max( 25, (int) floor( $limit * 0.75 ) ) );
		$ids        = [];
		$cat_ids    = [];

		if ( $post_id > 0 ) {
			$tag_ids = wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] );
			$ids     = array_merge(
				$ids,
				newsai_sem_links_fetch_ids_by_terms( $tag_ids, 'post_tag', $post_id, $before_gmt, $per_source )
			);

			$cat_ids = wp_get_post_categories( $post_id, [ 'fields' => 'ids' ] );
			$ids     = array_merge(
				$ids,
				newsai_sem_links_fetch_ids_by_terms( $cat_ids, 'category', $post_id, $before_gmt, $per_source )
			);
		}

		$ids = array_merge(
			$ids,
			newsai_sem_links_fetch_ids_by_title_words( $words, $post_id, $before_gmt, $per_source )
		);

		if ( count( $ids ) < 15 && $post_id > 0 && ! empty( $cat_ids ) ) {
			$primary = (int) reset( $cat_ids );
			$ids     = array_merge(
				$ids,
				newsai_sem_links_fetch_ids_by_terms( [ $primary ], 'category', $post_id, $before_gmt, $per_source )
			);
		}

		$ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
		if ( $post_id > 0 ) {
			$ids = array_values( array_diff( $ids, [ $post_id ] ) );
		}

		/**
		 * Filter archive link candidate post IDs before relevance scoring.
		 *
		 * @param int[]    $ids     Candidate post IDs.
		 * @param int      $post_id Current editor post ID.
		 * @param string[] $words   Significant words from draft.
		 * @param array    $cfg     Active preset.
		 */
		$ids = apply_filters( 'newsai_sem_links_candidate_ids', $ids, $post_id, $words, $cfg );

		return array_slice( $ids, 0, $limit );
	}
}

add_action('wp_ajax_newsai_get_semantic_links', 'newsai_ajax_get_semantic_links');
if (!function_exists('newsai_ajax_get_semantic_links')) {
    function newsai_ajax_get_semantic_links() {
        newsai_editor_ajax_gate();
        $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
        newsai_editor_ajax_require_post_edit( $post_id );
        $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
        $content = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
        $cfg     = function_exists( 'newsai_sem_links_config' ) ? newsai_sem_links_config() : [
            'min_chars' => 60, 'min_age_days' => 30, 'min_score' => 5, 'max_results' => 5, 'query_limit' => 50,
        ];
        $profile_key = (string) get_option( 'newsai_sem_links_profile', 'balanced' );
        $raw_text = trim($title . ' ' . wp_strip_all_tags(strip_shortcodes($content)));
        $min_chars = max( 30, (int) $cfg['min_chars'] );
        if (mb_strlen($raw_text, 'UTF-8') < $min_chars) {
            wp_send_json_success([
                '_meta' => [
                    'reason'     => 'content_too_short',
                    'min_chars'  => $min_chars,
                    'profile'    => $profile_key,
                ],
            ]);
        }

        $cache_key = 'nros_sem_links_v3_' . $profile_key . '_' . md5((string) $post_id . '|' . mb_substr($raw_text, 0, 700, 'UTF-8'));
        $cached = get_transient($cache_key);
        if (is_array($cached) && ! isset($cached['_meta'])) {
            wp_send_json_success($cached);
        }

        $words = function_exists('nros_get_significant_words') ? nros_get_significant_words($raw_text) : [];
        $bigrams = function_exists('nros_get_significant_bigrams') ? nros_get_significant_bigrams($raw_text) : [];
        $words = array_slice(array_values(array_unique(array_filter($words))), 0, 15);
        $bigrams = array_slice(array_values(array_unique(array_filter($bigrams))), 0, 8);
        if (empty($words) && empty($bigrams)) {
            wp_send_json_success([
                '_meta' => [ 'reason' => 'no_keywords', 'profile' => $profile_key ],
            ]);
        }

        $candidate_ids = newsai_sem_links_collect_candidate_ids( $post_id, $words, $cfg );
        $source_tags   = $post_id > 0 ? wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] ) : [];
        $source_cats   = $post_id > 0 ? wp_get_post_categories( $post_id, [ 'fields' => 'ids' ] ) : [];

        $min_score   = max( 3, (int) $cfg['min_score'] );
        $max_results = max( 1, min( 12, (int) $cfg['max_results'] ) );

        // Bulk-load GSC clicks for the whole candidate pool in one query (cached).
        // For internal links, click performance is a PRIMARY signal — we want to link
        // out to pages that already earn traffic (link equity + engagement).
        $gsc_days = 90;
        $gsc_on   = function_exists( 'newsai_gsc_data_available' ) && newsai_gsc_data_available();
        if ( $gsc_on && function_exists( 'newsai_gsc_prefetch_post_traffic' ) && ! empty( $candidate_ids ) ) {
            newsai_gsc_prefetch_post_traffic( array_map( 'intval', $candidate_ids ), $gsc_days );
        }

        $results = [];
        foreach ( $candidate_ids as $candidate_id ) {
            $candidate_title = (string) get_the_title( $candidate_id );
            $candidate_excerpt = (string) get_post_field( 'post_excerpt', $candidate_id );
            if ( $candidate_excerpt === '' ) {
                $candidate_excerpt = wp_trim_words( (string) get_post_field( 'post_content', $candidate_id ), 45, '' );
            }
            $candidate_norm = newsai_sem_links_normalize_match_text( $candidate_title . ' ' . $candidate_excerpt );

            $score = 0;
            if ( ! empty( $source_tags ) ) {
                $cand_tags = wp_get_post_tags( $candidate_id, [ 'fields' => 'ids' ] );
                $score    += count( array_intersect( $source_tags, $cand_tags ) ) * 3;
            }
            if ( ! empty( $source_cats ) ) {
                $cand_cats = wp_get_post_categories( $candidate_id, [ 'fields' => 'ids' ] );
                $score    += count( array_intersect( $source_cats, $cand_cats ) ) * 3; // same-section soft boost
            }
            foreach ( $words as $w ) {
                if ( newsai_sem_links_text_has_token( $candidate_norm, $w ) ) {
                    $score += 2;
                }
            }
            foreach ( $bigrams as $bg ) {
                if ( newsai_sem_links_text_has_token( $candidate_norm, $bg ) ) {
                    $score += 4;
                }
            }

            $age_days = (int) floor( ( time() - strtotime( (string) get_post_field( 'post_date_gmt', $candidate_id ) ) ) / DAY_IN_SECONDS );
            if ( $age_days >= 120 ) {
                $score += 3;
            } elseif ( $age_days >= 60 ) {
                $score += 2;
            }

            // Primary click boost: reward archive pages that already perform in Search.
            $clicks = 0;
            if ( $gsc_on && function_exists( 'newsai_gsc_get_post_traffic_cached' ) ) {
                $tr = newsai_gsc_get_post_traffic_cached( $candidate_id, $gsc_days );
                if ( $tr && (int) ( $tr['total'] ?? 0 ) > 0 ) {
                    $clicks = (int) $tr['total'];
                    $score += nros_gsc_click_bonus( $clicks, 6.0, 2.5 );
                }
            }

            if ( $score < $min_score ) {
                continue;
            }

            $reason = ( $age_days >= 60 ) ? 'Evergreen' : 'Relevant';
            if ( $clicks >= 50 ) {
                $reason = 'Popular';
            }
            $results[] = [
                'title'  => $candidate_title,
                'url'    => get_permalink( $candidate_id ),
                'score'  => $score,
                'clicks' => $clicks,
                'reason' => $reason,
            ];
        }

        usort($results, function($a, $b) {
            if ( $b['score'] !== $a['score'] ) { return $b['score'] <=> $a['score']; }
            return ( ( $b['clicks'] ?? 0 ) <=> ( $a['clicks'] ?? 0 ) );
        });
        $results = array_slice($results, 0, $max_results );
        if ( empty( $results ) ) {
            wp_send_json_success([
                '_meta' => [
                    'reason'       => 'no_matches',
                    'profile'      => $profile_key,
                    'pool_size'    => count( $candidate_ids ),
                ],
            ]);
        }
        set_transient($cache_key, $results, 10 * MINUTE_IN_SECONDS);
        wp_send_json_success($results);
    }
}

// ==========================================
// 3.1 AUTO-TAXONOMY GUARD (TAG HELL CLEANER)
// ==========================================
if ( ! function_exists( 'newsai_tag_pool_size_choices' ) ) {
	/**
	 * Allowed tag-pool sizes (most-used tags by post count).
	 *
	 * @return array<int, string> size => admin label
	 */
	function newsai_tag_pool_size_choices() {
		return [
			800  => __( 'Small–medium sites (800 tags)', 'newsroom-ai-assistant' ),
			1500 => __( 'Newsroom / tag hell (1,500 tags) — recommended', 'newsroom-ai-assistant' ),
			2500 => __( 'Large site, normal VPS (2,500 tags)', 'newsroom-ai-assistant' ),
			4000 => __( 'Very large inventory (4,000 tags)', 'newsroom-ai-assistant' ),
		];
	}
}

if ( ! function_exists( 'newsai_tag_pool_size_get' ) ) {
	/**
	 * @return int One of newsai_tag_pool_size_choices() keys.
	 */
	function newsai_tag_pool_size_get() {
		$allowed = array_keys( newsai_tag_pool_size_choices() );
		$val     = (int) get_option( 'newsai_tag_pool_size', 800 );
		return in_array( $val, $allowed, true ) ? $val : 800;
	}
}

if ( ! function_exists( 'newsai_tag_pool_editor_size_choices' ) ) {
	/**
	 * Tag pool embedded in the editor sidebar (JSON). Kept smaller than the guard pool.
	 *
	 * @return array<int, string>
	 */
	function newsai_tag_pool_editor_size_choices() {
		return [
			300 => __( 'Light (300 tags) — recommended for editor', 'newsroom-ai-assistant' ),
			500 => __( 'Medium (500 tags)', 'newsroom-ai-assistant' ),
			800 => __( 'Large (800 tags)', 'newsroom-ai-assistant' ),
		];
	}
}

if ( ! function_exists( 'newsai_tag_pool_editor_size_get' ) ) {
	/**
	 * @return int Capped by the active tag-guard pool size.
	 */
	function newsai_tag_pool_editor_size_get() {
		$allowed = array_keys( newsai_tag_pool_editor_size_choices() );
		$val     = (int) get_option( 'newsai_tag_pool_editor_size', 300 );
		if ( ! in_array( $val, $allowed, true ) ) {
			$val = 300;
		}
		$guard = function_exists( 'newsai_tag_pool_size_get' ) ? newsai_tag_pool_size_get() : 800;
		return min( $val, $guard );
	}
}

if ( ! function_exists( 'newsai_tag_pool_size_clear_caches' ) ) {
	function newsai_tag_pool_size_clear_caches() {
		foreach ( array_keys( newsai_tag_pool_size_choices() ) as $size ) {
			delete_transient( 'nros_tag_guard_candidates_v2_' . $size );
			delete_transient( 'nros_top_tags_cache_v3_' . $size );
		}
		foreach ( array_keys( newsai_tag_pool_editor_size_choices() ) as $size ) {
			delete_transient( 'nros_top_tags_cache_editor_v1_' . $size );
		}
		delete_transient( 'nros_tag_guard_candidates_v1' );
		delete_transient( 'nros_top_tags_cache_v2' );
	}
}

if ( ! function_exists( 'newsai_sql_datetime_days_ago' ) ) {
	/**
	 * Static SQL datetime threshold (avoids DATE_SUB in WHERE for large tables).
	 *
	 * @param int  $days Days back.
	 * @param bool $gmt  True for post_date_gmt comparisons.
	 * @return string MySQL datetime string.
	 */
	function newsai_sql_datetime_days_ago( $days, $gmt = true ) {
		$days = max( 0, (int) $days );
		$ts   = time() - ( $days * DAY_IN_SECONDS );
		if ( $gmt ) {
			return gmdate( 'Y-m-d H:i:s', $ts );
		}
		return wp_date( 'Y-m-d H:i:s', $ts );
	}
}

if ( ! function_exists( 'newsai_sql_date_days_ago' ) ) {
	/**
	 * @param int $days Days back.
	 * @return string Y-m-d date.
	 */
	function newsai_sql_date_days_ago( $days ) {
		$days = max( 0, (int) $days );
		return gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
	}
}

if (!function_exists('newsai_normalize_tag_label')) {
    function newsai_normalize_tag_label($label) {
        $txt = (string) $label;
        if (function_exists('nros_global_unaccent')) {
            $txt = nros_global_unaccent($txt);
        } else {
            $txt = mb_strtolower($txt, 'UTF-8');
        }
        $txt = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $txt);
        $txt = preg_replace('/\s+/u', ' ', (string) $txt);
        return trim((string) $txt);
    }
}

if (!function_exists('newsai_get_tag_guard_candidates')) {
    function newsai_get_tag_guard_candidates() {
        $limit     = function_exists( 'newsai_tag_pool_size_get' ) ? newsai_tag_pool_size_get() : 800;
        $cache_key = 'nros_tag_guard_candidates_v2_' . $limit;
        $cached = get_transient($cache_key);
        if (is_array($cached)) {
            return $cached;
        }

        $terms = get_terms([
            'taxonomy'   => 'post_tag',
            'hide_empty' => false,
            'number'     => $limit,
            'orderby'    => 'count',
            'order'      => 'DESC',
            'fields'     => 'all',
        ]);
        if (is_wp_error($terms) || empty($terms)) {
            return [];
        }

        $out = [];
        foreach ($terms as $t) {
            if (empty($t->name) || !isset($t->term_id)) {
                continue;
            }
            $out[] = [
                'id' => (int) $t->term_id,
                'name' => (string) $t->name,
                'norm' => newsai_normalize_tag_label((string) $t->name),
            ];
        }
        set_transient($cache_key, $out, 6 * HOUR_IN_SECONDS);
        return $out;
    }
}

if (!function_exists('newsai_clear_tag_guard_cache')) {
    function newsai_clear_tag_guard_cache() {
        if ( function_exists( 'newsai_tag_pool_size_clear_caches' ) ) {
            newsai_tag_pool_size_clear_caches();
        } else {
            delete_transient( 'nros_tag_guard_candidates_v1' );
        }
    }
}
add_action('created_post_tag', 'newsai_clear_tag_guard_cache');
add_action('edited_post_tag', 'newsai_clear_tag_guard_cache');
add_action('delete_post_tag', 'newsai_clear_tag_guard_cache');

if (!function_exists('newsai_tag_guard_is_force_bypass')) {
    function newsai_tag_guard_is_force_bypass() {
        $can_force = current_user_can( 'manage_options' ) || current_user_can( 'edit_others_posts' );
        if ( $can_force && isset($_SERVER['HTTP_X_NEWSAI_TAG_FORCE']) && sanitize_text_field( (string) wp_unslash( $_SERVER['HTTP_X_NEWSAI_TAG_FORCE'] ) ) === '1') {
            return true;
        }
        if (isset($_POST['newsai_tag_force']) && (string) wp_unslash($_POST['newsai_tag_force']) === '1') {
            return true;
        }
        return false;
    }
}

if (!function_exists('newsai_tag_guard_is_active')) {
    /**
     * Tag duplicate guard runs only when explicitly enabled in settings AND the site is on Pro.
     * Default for the option is 'yes' so existing Pro users keep current behavior; non-Pro sites
     * always skip the guard regardless of the option.
     *
     * @return bool
     */
    function newsai_tag_guard_is_active() {
        if (!function_exists('newsai_is_pro') || !newsai_is_pro()) {
            return false;
        }
        if ( function_exists( 'newsai_large_site_mode_skips_tag_guard' ) && newsai_large_site_mode_skips_tag_guard() ) {
            return false;
        }
        return get_option('newsai_tag_guard_enabled', 'yes') === 'yes';
    }
}

if (!function_exists('newsai_tag_guard_typo_score')) {
    /**
     * Score single-token near-duplicate typos (e.g. NEWSOS vs NEWSSS).
     *
     * @param string $a Normalized tag A.
     * @param string $b Normalized tag B.
     * @return int Score bump (0 or 7).
     */
    function newsai_tag_guard_typo_score($a, $b) {
        $a = (string) $a;
        $b = (string) $b;
        if ($a === '' || $b === '' || $a === $b) {
            return 0;
        }
        $len_a = mb_strlen($a, 'UTF-8');
        $len_b = mb_strlen($b, 'UTF-8');
        if ($len_a < 3 || $len_b < 3) {
            return 0;
        }
        if (abs($len_a - $len_b) > 2) {
            return 0;
        }
        if (function_exists('levenshtein') && strlen($a) === $len_a && strlen($b) === $len_b) {
            $dist = levenshtein($a, $b);
            if ($dist >= 1 && $dist <= 2) {
                return 7;
            }
        }
        $pct = 0.0;
        similar_text($a, $b, $pct);
        if ($pct >= 72.0) {
            return 7;
        }
        return 0;
    }
}

if (!function_exists('newsai_tag_guard_prevent_near_duplicates')) {
    function newsai_tag_guard_prevent_near_duplicates($term, $taxonomy) {
        if ($taxonomy !== 'post_tag') {
            return $term;
        }
        if (!is_string($term)) {
            return $term;
        }
        if (!newsai_tag_guard_is_active()) {
            return $term;
        }
        if (newsai_tag_guard_is_force_bypass()) {
            return $term;
        }

        $new_raw = trim($term);
        if ($new_raw === '' || mb_strlen($new_raw, 'UTF-8') < 3) {
            return $term;
        }
        $new_norm = newsai_normalize_tag_label($new_raw);
        if ($new_norm === '') {
            return $term;
        }

        $candidates = newsai_get_tag_guard_candidates();
        if (empty($candidates)) {
            return $term;
        }

        $new_tokens = array_values(array_filter(preg_split('/\s+/u', $new_norm)));
        $new_token_count = count($new_tokens);
        $best = null;
        $best_score = 0;

        foreach ($candidates as $c) {
            if (empty($c['norm']) || empty($c['name'])) {
                continue;
            }
            $cand_norm = (string) $c['norm'];
            $cand_name = (string) $c['name'];
            $score = 0;

            if ($cand_norm === $new_norm) {
                $score = 100;
            } else {
                if (mb_strlen($new_norm, 'UTF-8') >= 6 && (mb_stripos($cand_norm, $new_norm, 0, 'UTF-8') !== false || mb_stripos($new_norm, $cand_norm, 0, 'UTF-8') !== false)) {
                    $score += 8;
                }
                $cand_tokens = array_values(array_filter(preg_split('/\s+/u', $cand_norm)));
                if (!empty($cand_tokens) && $new_token_count > 0) {
                    $overlap = count(array_intersect($new_tokens, $cand_tokens));
                    if ($overlap > 0) {
                        $ratio = $overlap / max($new_token_count, count($cand_tokens));
                        if ($ratio >= 0.66) {
                            $score += 7;
                        } elseif ($ratio >= 0.5) {
                            $score += 5;
                        } else {
                            $score += 2;
                        }
                    }
                }
                if ($new_token_count === 1 && count($cand_tokens) === 1) {
                    $score = max($score, newsai_tag_guard_typo_score($new_norm, $cand_norm));
                }
            }

            if ($score > $best_score) {
                $best_score = $score;
                $best = $c;
            }
        }

        if ($best && $best_score >= 7) {
            $is_gr = (newsai_get_ui_lang() === 'el');
            $wikidata = get_term_meta((int) $best['id'], 'nros_wikidata_url', true);
            $meta_hint = (!empty($wikidata))
                ? ($is_gr ? ' (έχει ήδη Wikidata mapping)' : ' (already has Wikidata mapping)')
                : '';
            $best_name_safe = sanitize_text_field( (string) $best['name'] );
            $msg = $is_gr
                ? 'Πιθανό διπλό Tag. Μήπως εννοείτε: "' . $best_name_safe . '"' . $meta_hint . ';'
                : 'Possible duplicate tag. Did you mean: "' . $best_name_safe . '"' . $meta_hint . '?';
            if (is_user_logged_in() && ! ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) ) {
                set_transient('newsai_tag_guard_notice_' . get_current_user_id(), $msg, 5 * MINUTE_IN_SECONDS);
            }
            return new WP_Error(
                'newsai_tag_duplicate_suspect',
                $msg,
                [
                    'status'         => 409,
                    'suggested_id'   => (int) $best['id'],
                    'suggested_name' => $best_name_safe,
                    'original'       => $new_raw,
                ]
            );
        }

        return $term;
    }
}
add_filter('pre_insert_term', 'newsai_tag_guard_prevent_near_duplicates', 10, 2);

// 🚀 NROS GOOGLE TRENDS (With 5-Min Cache Protection)
add_action('wp_ajax_newsai_get_trends', 'newsai_ajax_get_trends');
if (!function_exists('newsai_ajax_get_trends')) {
    function newsai_ajax_get_trends() {
        newsai_editor_ajax_gate();
        $geo = isset($_POST['geo']) ? sanitize_text_field($_POST['geo']) : '';
        if ( $geo === '' && function_exists( 'newsai_get_post_target_geo' ) ) {
            $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
            $geo = newsai_get_post_target_geo( $post_id );
        }
        if ( $geo === '' ) {
            $geo = function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US';
        }
        if ( function_exists( 'newsai_google_trends_normalize_geo' ) ) {
            $geo = newsai_google_trends_normalize_geo( $geo );
        }
        $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
        $context = $post_id > 0 ? 'editor' : 'desk';
        $user_id = get_current_user_id();
        $user_beat = $user_id ? get_user_meta($user_id, 'nros_trends_beat', true) : '';
        $allowed_beats = ['s', 'b', 'e', 'm', 't'];
        $beat = in_array($user_beat, $allowed_beats, true) ? $user_beat : '';
        
        $cache_key = 'nros_trends_html_v4_' . $context . '_' . $geo . '_' . ($beat !== '' ? $beat : 'general');
        $cached_html = get_transient($cache_key);
        if (false !== $cached_html) {
            wp_send_json_success($cached_html);
        }

        if ( function_exists( 'newsai_radar_try_fetch_lock' ) && ! newsai_radar_try_fetch_lock( 'trends', $geo ) ) {
            wp_send_json_error( [ 'message' => __( 'Trends are refreshing — try again in a moment.', 'newsroom-ai-assistant' ) ], 429 );
        }

        if ( ! function_exists( 'newsai_google_trends_get_sidebar_html' ) ) {
            if ( function_exists( 'newsai_radar_release_fetch_lock' ) ) {
                newsai_radar_release_fetch_lock( 'trends', $geo );
            }
            wp_send_json_error( [ 'message' => __( 'Trends module unavailable.', 'newsroom-ai-assistant' ) ] );
        }

        $result = newsai_google_trends_get_sidebar_html( $geo, $context );
        if ( function_exists( 'newsai_radar_release_fetch_lock' ) ) {
            newsai_radar_release_fetch_lock( 'trends', $geo );
        }
        if ( is_wp_error( $result ) ) {
            wp_send_json_error(
                [
                    'message' => $result->get_error_message(),
                    'code'    => $result->get_error_code(),
                ]
            );
        }

        $html = (string) $result['html'];
        if ( $html === '' ) {
            wp_send_json_error( [ 'message' => __( 'No trending topics were returned.', 'newsroom-ai-assistant' ) ] );
        }

        $cache_ttl = ! empty( $result['stale'] ) ? 2 * MINUTE_IN_SECONDS : 5 * MINUTE_IN_SECONDS;
        set_transient( $cache_key, $html, $cache_ttl );
        wp_send_json_success( $html );
    }
}

// 🚀 NROS GOOGLE NEWS (With 5-Min Cache Protection)
/**
 * Google News RSS needs a consistent (hl, gl, ceid) triple per edition. The second
 * half of ceid is often NOT the first segment of hl (e.g. BR:pt-BR, MX:es-419, US:en).
 *
 * @param string $geo ISO-3166-1 alpha-2 country code from plugin settings (e.g. GR, US).
 * @return array{gl:string,hl:string,ceid_tail:string}
 */
if ( ! function_exists( 'newsai_news_edition_for_geo' ) ) {
	function newsai_news_edition_for_geo( $geo ) {
		$geo = strtoupper( sanitize_text_field( (string) $geo ) );
		if ( $geo === '' ) {
			$geo = function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US';
		}
		// Keys must match every <option value="…"> in admin Trends geo + post editor default.
		$map = [
			'GR' => [ 'hl' => 'el', 'ceid_tail' => 'el' ],
			'CY' => [ 'hl' => 'el', 'ceid_tail' => 'el' ],
			'US' => [ 'hl' => 'en-US', 'ceid_tail' => 'en' ],
			'GB' => [ 'hl' => 'en-GB', 'ceid_tail' => 'en' ],
			'CA' => [ 'hl' => 'en-CA', 'ceid_tail' => 'en' ],
			'AU' => [ 'hl' => 'en-AU', 'ceid_tail' => 'en' ],
			'DE' => [ 'hl' => 'de', 'ceid_tail' => 'de' ],
			'FR' => [ 'hl' => 'fr', 'ceid_tail' => 'fr' ],
			'IT' => [ 'hl' => 'it', 'ceid_tail' => 'it' ],
			'ES' => [ 'hl' => 'es', 'ceid_tail' => 'es' ],
			'NL' => [ 'hl' => 'nl', 'ceid_tail' => 'nl' ],
			'BE' => [ 'hl' => 'nl', 'ceid_tail' => 'nl' ],
			'CH' => [ 'hl' => 'de-CH', 'ceid_tail' => 'de' ],
			'SE' => [ 'hl' => 'sv', 'ceid_tail' => 'sv' ],
			'PT' => [ 'hl' => 'pt-PT', 'ceid_tail' => 'pt-PT' ],
			'BR' => [ 'hl' => 'pt-BR', 'ceid_tail' => 'pt-BR' ],
			'MX' => [ 'hl' => 'es-419', 'ceid_tail' => 'es-419' ],
			'RU' => [ 'hl' => 'ru', 'ceid_tail' => 'ru' ],
			'JP' => [ 'hl' => 'ja', 'ceid_tail' => 'ja' ],
			'IN' => [ 'hl' => 'en-IN', 'ceid_tail' => 'en' ],
			'ID' => [ 'hl' => 'id', 'ceid_tail' => 'id' ],
			'ZA' => [ 'hl' => 'en-ZA', 'ceid_tail' => 'en' ],
		];
		if ( ! isset( $map[ $geo ] ) ) {
			$out = [
				'gl'         => $geo,
				'hl'         => 'en-US',
				'ceid_tail'  => 'en',
			];
			return apply_filters( 'newsai_news_edition_for_geo', $out, $geo );
		}
		$out = array_merge( $map[ $geo ], [ 'gl' => $geo ] );
		return apply_filters( 'newsai_news_edition_for_geo', $out, $geo );
	}
}

/**
 * Localized OR-queries for rss/search fallback (topic feed is preferred).
 *
 * @param string $beat One of s,b,e,m,t.
 * @param array  $edition From newsai_news_edition_for_geo().
 * @return string
 */
if ( ! function_exists( 'newsai_news_beat_search_query' ) ) {
	function newsai_news_beat_search_query( $beat, $edition ) {
		$tail = isset( $edition['ceid_tail'] ) ? (string) $edition['ceid_tail'] : 'en';
		// Map ceid tail / hl family to a query bundle.
		$bundle = 'en';
		if ( $tail === 'el' ) {
			$bundle = 'el';
		} elseif ( $tail === 'de' ) {
			$bundle = 'de';
		} elseif ( $tail === 'fr' ) {
			$bundle = 'fr';
		} elseif ( $tail === 'es' || $tail === 'es-419' ) {
			$bundle = 'es';
		} elseif ( $tail === 'it' ) {
			$bundle = 'it';
		} elseif ( $tail === 'nl' ) {
			$bundle = 'nl';
		} elseif ( $tail === 'pt-PT' ) {
			$bundle = 'pt_pt';
		} elseif ( $tail === 'pt-BR' ) {
			$bundle = 'pt_br';
		} elseif ( $tail === 'ru' ) {
			$bundle = 'ru';
		} elseif ( $tail === 'sv' ) {
			$bundle = 'sv';
		} elseif ( $tail === 'ja' ) {
			$bundle = 'ja';
		} elseif ( $tail === 'id' ) {
			$bundle = 'id';
		}

		$sets = [
			'en' => [
				's' => 'sports OR football OR basketball',
				'b' => 'business OR economy OR markets',
				'e' => 'entertainment OR celebrity OR cinema',
				'm' => 'health OR medicine OR wellness',
				't' => 'technology OR science OR artificial intelligence',
			],
			'el' => [
				's' => 'αθλητικά OR ποδόσφαιρο OR μπάσκετ',
				'b' => 'οικονομία OR επιχειρήσεις OR αγορές',
				'e' => 'ψυχαγωγία OR διασημότητες OR κινηματογράφος',
				'm' => 'υγεία OR ιατρικά OR ευεξία',
				't' => 'τεχνολογία OR επιστήμη OR τεχνητή νοημοσύνη',
			],
			'de' => [
				's' => 'sport OR fußball OR basketball',
				'b' => 'wirtschaft OR unternehmen OR finanzen',
				'e' => 'unterhaltung OR promis OR kino',
				'm' => 'gesundheit OR medizin OR wellness',
				't' => 'technologie OR wissenschaft OR künstliche intelligenz',
			],
			'fr' => [
				's' => 'sport OR football OR basket',
				'b' => 'économie OR entreprises OR finance',
				'e' => 'divertissement OR célébrités OR cinéma',
				'm' => 'santé OR médecine OR bien-être',
				't' => 'technologie OR science OR intelligence artificielle',
			],
			'es' => [
				's' => 'deportes OR fútbol OR baloncesto',
				'b' => 'economía OR empresas OR mercados',
				'e' => 'entretenimiento OR celebridades OR cine',
				'm' => 'salud OR medicina OR bienestar',
				't' => 'tecnología OR ciencia OR inteligencia artificial',
			],
			'it' => [
				's' => 'sport OR calcio OR basket',
				'b' => 'economia OR imprese OR mercati',
				'e' => 'intrattenimento OR celebrity OR cinema',
				'm' => 'salute OR medicina OR benessere',
				't' => 'tecnologia OR scienza OR intelligenza artificiale',
			],
			'nl' => [
				's' => 'sport OR voetbal OR basketball',
				'b' => 'economie OR bedrijven OR markten',
				'e' => 'entertainment OR celebrity OR film',
				'm' => 'gezondheid OR geneeskunde OR welzijn',
				't' => 'technologie OR wetenschap OR kunstmatige intelligentie',
			],
			'pt_pt' => [
				's' => 'desporto OR futebol OR basquetebol',
				'b' => 'economia OR empresas OR mercados',
				'e' => 'entretenimento OR celebridades OR cinema',
				'm' => 'saúde OR medicina OR bem-estar',
				't' => 'tecnologia OR ciência OR inteligência artificial',
			],
			'pt_br' => [
				's' => 'esportes OR futebol OR basquete',
				'b' => 'economia OR empresas OR mercados',
				'e' => 'entretenimento OR celebridades OR cinema',
				'm' => 'saúde OR medicina OR bem-estar',
				't' => 'tecnologia OR ciência OR inteligência artificial',
			],
			'ru' => [
				's' => 'спорт OR футбол OR баскетбол',
				'b' => 'экономика OR бизнес OR рынки',
				'e' => 'развлечения OR знаменитости OR кино',
				'm' => 'здоровье OR медицина OR самочувствие',
				't' => 'технологии OR наука OR искусственный интеллект',
			],
			'sv' => [
				's' => 'sport OR fotboll OR basket',
				'b' => 'ekonomi OR företag OR marknader',
				'e' => 'underhållning OR kändisar OR film',
				'm' => 'hälsa OR medicin OR välmående',
				't' => 'teknologi OR vetenskap OR artificiell intelligens',
			],
			'ja' => [
				's' => 'スポーツ OR サッカー OR バスケットボール',
				'b' => '経済 OR ビジネス OR 市場',
				'e' => 'エンタメ OR セレブ OR 映画',
				'm' => '健康 OR 医療 OR ウェルネス',
				't' => 'テクノロジー OR 科学 OR 人工知能',
			],
			'id' => [
				's' => 'olahraga OR sepak bola OR basket',
				'b' => 'ekonomi OR bisnis OR pasar',
				'e' => 'hiburan OR selebriti OR film',
				'm' => 'kesehatan OR medis OR wellness',
				't' => 'teknologi OR sains OR kecerdasan buatan',
			],
		];
		if ( ! isset( $sets[ $bundle ][ $beat ] ) ) {
			return isset( $sets['en'][ $beat ] ) ? $sets['en'][ $beat ] : '';
		}
		return $sets[ $bundle ][ $beat ];
	}
}

add_action('wp_ajax_newsai_get_news', 'newsai_ajax_get_news');
if (!function_exists('newsai_ajax_get_news')) {
    function newsai_ajax_get_news() {
        newsai_editor_ajax_gate();
        $geo = isset($_POST['geo']) ? sanitize_text_field($_POST['geo']) : '';
        if ( $geo === '' && function_exists( 'newsai_get_post_target_geo' ) ) {
            $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
            $geo = newsai_get_post_target_geo( $post_id );
        }
        if ( $geo === '' ) {
            $geo = function_exists( 'newsai_get_target_geo' ) ? newsai_get_target_geo() : 'US';
        }
        $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
        $context = $post_id > 0 ? 'editor' : 'desk';
        $user_id = get_current_user_id();
        $user_beat = $user_id ? get_user_meta($user_id, 'nros_trends_beat', true) : '';
        $allowed_beats = ['s', 'b', 'e', 'm', 't'];
        $beat = in_array($user_beat, $allowed_beats, true) ? $user_beat : '';

        if ( ! function_exists( 'newsai_google_news_ajax_cache_key' ) || ! function_exists( 'newsai_google_news_get_sidebar_html' ) ) {
            wp_send_json_error( [ 'message' => __( 'News module unavailable.', 'newsroom-ai-assistant' ) ] );
        }

        $cache_key   = newsai_google_news_ajax_cache_key( $geo, $beat, $context );
        $cached_html = get_transient( $cache_key );
        if ( false !== $cached_html ) {
            wp_send_json_success( $cached_html );
        }

        if ( function_exists( 'newsai_radar_try_fetch_lock' ) && ! newsai_radar_try_fetch_lock( 'news', $geo ) ) {
            wp_send_json_error( [ 'message' => __( 'News feed is refreshing — try again in a moment.', 'newsroom-ai-assistant' ) ], 429 );
        }

        $result = newsai_google_news_get_sidebar_html( $geo, $beat, $context );
        if ( function_exists( 'newsai_radar_release_fetch_lock' ) ) {
            newsai_radar_release_fetch_lock( 'news', $geo );
        }
        if ( is_wp_error( $result ) ) {
            wp_send_json_error(
                [
                    'message' => $result->get_error_message(),
                    'code'    => $result->get_error_code(),
                ]
            );
        }

        $html = (string) $result['html'];
        if ( $html === '' ) {
            wp_send_json_error( [ 'message' => __( 'No news headlines were returned.', 'newsroom-ai-assistant' ) ] );
        }

        $cache_ttl = ! empty( $result['stale'] ) ? 2 * MINUTE_IN_SECONDS : 5 * MINUTE_IN_SECONDS;
        set_transient( $cache_key, $html, $cache_ttl );
        wp_send_json_success( $html );
    }
}

// ==========================================
// 4. STORY CLUSTERING ENGINE (NEW SMART TIMELINE)
// ==========================================
if (!function_exists('nros_get_stop_words_by_lang')) {
    function nros_get_stop_words_by_lang($lang = 'en') {
        $map = [
            'en' => ['the','and','is','in','at','of','on','for','with','that','this','from','into','over','under','as','by','to','a','an'],
            'el' => ['και','με','το','τα','της','του','των','στο','στα','στη','στην','στον','για','απο','από','σε','ειναι','είναι','μια','ενα','ένα','πως','που','μην','οτι','ότι','τις','τους'],
            'es' => ['el','la','los','las','de','del','y','o','en','por','para','con','sin','un','una','unos','unas','que','como'],
            'fr' => ['le','la','les','de','du','des','et','ou','en','pour','avec','sans','un','une','que','dans','sur'],
            'de' => ['der','die','das','den','dem','des','und','oder','mit','ohne','ein','eine','zu','auf','in','von','für']
        ];
        return isset($map[$lang]) ? $map[$lang] : $map['en'];
    }
}

if (!function_exists('nros_get_significant_words')) {
    function nros_get_significant_words($string) {
        $lang = function_exists('newsai_get_ui_lang') ? newsai_get_ui_lang() : 'en';
        $stop_words = nros_get_stop_words_by_lang($lang);
        $smart_whitelist = ['ai','eu','us','uk','tv','pr','5g'];
        $string = mb_strtolower(wp_strip_all_tags($string), 'UTF-8');
        $string = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $string);
        $words = preg_split('/\s+/u', $string, -1, PREG_SPLIT_NO_EMPTY);

        $filtered = [];
        foreach ($words as $w) {
            if (in_array($w, $stop_words, true)) { continue; }
            $len = mb_strlen($w, 'UTF-8');
            if ($len <= 2 && !in_array($w, $smart_whitelist, true)) { continue; }
            $filtered[] = $w;
        }
        return $filtered;
    }
}

if (!function_exists('nros_get_significant_bigrams')) {
    function nros_get_significant_bigrams($string) {
        if (get_option('newsai_advanced_nlp', 'yes') !== 'yes') {
            return [];
        }
        $tokens = nros_get_significant_words($string);
        if (count($tokens) < 2) { return []; }
        $bigrams = [];
        for ($i = 0; $i < count($tokens) - 1; $i++) {
            $bigrams[] = $tokens[$i] . ' ' . $tokens[$i + 1];
        }
        return $bigrams;
    }
}

if (!function_exists('nros_get_timeline_days_back')) {
    function nros_get_timeline_days_back() {
        $strategy = get_option('newsai_timeline_strategy', 'ongoing');
        if ($strategy === 'breaking') return 7;
        if ($strategy === 'evergreen') return 120;
        if ($strategy === 'ongoing') return 30;
        $manual_days = (int)get_option('newsai_timeline_days', 30);
        if ($manual_days > 0) return $manual_days;
        return 30;
    }
}

if ( ! function_exists( 'newsai_timeline_resolve_label' ) ) {
	/**
	 * Timeline label from settings; empty saved option falls back to default (get_option default only applies when missing).
	 *
	 * @param string $option_name Option key.
	 * @param string $fallback    Default when option is empty.
	 * @return string
	 */
	function newsai_timeline_resolve_label( $option_name, $fallback ) {
		$val = get_option( $option_name, '' );
		$val = is_string( $val ) ? trim( wp_strip_all_tags( $val ) ) : '';
		return $val !== '' ? $val : (string) $fallback;
	}
}

if ( ! function_exists( 'newsai_timeline_format_item_date' ) ) {
	/**
	 * Human-readable date label for one timeline row.
	 *
	 * @param int    $post_time   Unix timestamp.
	 * @param int    $diff_days   Days since publish.
	 * @param int    $index       Row index in cluster.
	 * @param int    $total_posts Total rows.
	 * @param string $lang        UI language code (el|en).
	 * @return string Empty string only if timestamp invalid.
	 */
	function newsai_timeline_format_item_date( $post_time, $diff_days, $index, $total_posts, $lang ) {
		$is_el = ( $lang === 'el' );
		if ( $index === $total_posts - 1 && $total_posts > 1 ) {
			$start = newsai_timeline_resolve_label(
				'newsai_timeline_label_start',
				$is_el ? 'Η αρχή' : 'The start'
			);
			if ( $start !== '' ) {
				return $start;
			}
		}
		if ( $diff_days === 0 ) {
			return newsai_timeline_resolve_label( 'newsai_timeline_label_today', $is_el ? 'Σήμερα' : 'Today' );
		}
		if ( $diff_days === 1 ) {
			return newsai_timeline_resolve_label( 'newsai_timeline_label_yesterday', $is_el ? 'Χθες' : 'Yesterday' );
		}
		if ( $diff_days <= 7 ) {
			$pattern = newsai_timeline_resolve_label(
				'newsai_timeline_label_days_ago',
				$is_el ? 'Πριν %d ημέρες' : '%d days ago'
			);
			if ( strpos( $pattern, '%d' ) !== false ) {
				return sprintf( $pattern, $diff_days );
			}
		}
		if ( $post_time > 0 ) {
			return date_i18n( 'j M Y', $post_time );
		}
		return '';
	}
}

if ( ! function_exists( 'nros_timeline_generic_tag_names' ) ) {
	/**
	 * Tags too broad to anchor a story timeline (editors often add them by habit).
	 * Matched accent-insensitive on the normalized tag slug/name.
	 *
	 * @return string[] Lowercase, no accents.
	 */
	function nros_timeline_generic_tag_names() {
		$list = [
			// Greek newsroom noise
			'αλλαγες', 'αλλαγη', 'ανακοινωση', 'ανακοινωσεις',
			'αστυνομια', 'πυροσβεστικη', 'πυροσβεστικης',
			'ελλαδα', 'κυβερνηση', 'υπουργειο', 'υπουργεια',
			'επιχειρησεις', 'οικονομια', 'πολιτικη',
			'συλληψη', 'συλληψεις', 'δικη', 'δικαστηριο',
			'ειδησεις', 'νεα', 'εξελιξη', 'εξελιξης',
			'φωτο', 'βιντεο', 'video', 'photo',
			// English
			'changes', 'change', 'news', 'breaking', 'update', 'updates',
			'police', 'government', 'economy', 'business',
		];
		return apply_filters( 'nros_timeline_generic_tag_names', $list );
	}
}

if ( ! function_exists( 'nros_timeline_normalize_tag_label' ) ) {
	/**
	 * @param string $name Tag name or slug.
	 */
	function nros_timeline_normalize_tag_label( $name ) {
		$name = mb_strtolower( trim( (string) $name ), 'UTF-8' );
		if ( function_exists( 'nros_global_unaccent' ) ) {
			$name = nros_global_unaccent( $name );
		}
		return preg_replace( '/\s+/u', ' ', $name );
	}
}

if ( ! function_exists( 'nros_timeline_is_generic_tag' ) ) {
	/**
	 * @param string $tag_name Tag display name.
	 */
	function nros_timeline_is_generic_tag( $tag_name ) {
		$norm = nros_timeline_normalize_tag_label( $tag_name );
		if ( $norm === '' ) {
			return true;
		}
		$block = nros_timeline_generic_tag_names();
		if ( in_array( $norm, $block, true ) ) {
			return true;
		}
		// Single very short token (<=5 chars) on a huge site is almost always noise.
		if ( mb_strlen( $norm, 'UTF-8' ) <= 5 && strpos( $norm, ' ' ) === false ) {
			foreach ( $block as $g ) {
				if ( $g === '' ) {
					continue;
				}
				$glen = mb_strlen( $g, 'UTF-8' );
				if ( $norm === $g
					|| ( $glen > 0 && mb_substr( $norm, 0, $glen, 'UTF-8' ) === $g )
					|| ( $glen > 0 && mb_substr( $norm, -$glen, null, 'UTF-8' ) === $g ) ) {
					return true;
				}
			}
		}
		return (bool) apply_filters( 'nros_timeline_is_generic_tag', false, $tag_name, $norm );
	}
}

if ( ! function_exists( 'nros_timeline_tag_in_title' ) ) {
	/**
	 * Accent-insensitive check: tag "ΠΑΤΙΝΙ" matches title "Πατίνια: Σαρωτικές αλλαγές…".
	 *
	 * @param string $tag_name   Tag label.
	 * @param string $post_title Post title.
	 */
	function nros_timeline_tag_in_title( $tag_name, $post_title ) {
		$tag_norm   = nros_timeline_normalize_tag_label( $tag_name );
		$title_norm = nros_timeline_normalize_tag_label( $post_title );
		if ( $tag_norm === '' || $title_norm === '' ) {
			return false;
		}
		if ( mb_strlen( $tag_norm, 'UTF-8' ) >= 4 && mb_strpos( $title_norm, $tag_norm, 0, 'UTF-8' ) !== false ) {
			return true;
		}
		// Greek plural stem: πατινι ↔ πατινια, αλλαγη ↔ αλλαγες.
		$stem = $tag_norm;
		foreach ( [ 'ες', 'ων', 'ας', 'ης', 'οι', 'ια', 'εις' ] as $suffix ) {
			$len = mb_strlen( $suffix, 'UTF-8' );
			if ( mb_strlen( $stem, 'UTF-8' ) > $len + 3 && mb_substr( $stem, -$len, null, 'UTF-8' ) === $suffix ) {
				$try = mb_substr( $stem, 0, -$len, 'UTF-8' );
				if ( mb_strlen( $try, 'UTF-8' ) >= 4 && mb_strpos( $title_norm, $try, 0, 'UTF-8' ) !== false ) {
					return true;
				}
			}
		}
		return false;
	}
}

if ( ! function_exists( 'nros_timeline_tag_weight' ) ) {
    /**
     * Specificity weight for a tag based on how many posts use it (IDF-style).
     * Rare tags (e.g. "ΠΑΤΙΝΙ") describe a story; generic tags (e.g. "ΑΛΛΑΓΕΣ",
     * "ΑΣΤΥΝΟΜΙΑ") are near-useless as a timeline topic, so they score ~0.
     *
     * @param int $count Number of posts assigned to the tag.
     * @return float
     */
    function nros_timeline_tag_weight( $count ) {
        $count = (int) $count;
        if ( $count <= 1 )   { return 0.0; }   // only the current post — cannot build a timeline
        if ( $count <= 40 )  { return 3.0; }   // specific story tag
        if ( $count <= 120 ) { return 2.0; }
        if ( $count <= 300 ) { return 1.0; }
        if ( $count <= 800 ) { return 0.4; }   // broad section tag
        return 0.15;                            // extremely generic ("Ελλάδα", "Αλλαγές")
    }
}

if ( ! function_exists( 'nros_timeline_tag_topic_score' ) ) {
	/**
	 * Score a tag as a timeline topic anchor (higher = better).
	 *
	 * @param object $tag        WP term object (->name, ->count).
	 * @param string $post_title Post title.
	 * @return float 0 when tag must not be used as topic.
	 */
	function nros_timeline_tag_topic_score( $tag, $post_title ) {
		if ( ! isset( $tag->name ) ) {
			return 0.0;
		}
		$name = (string) $tag->name;
		if ( nros_timeline_is_generic_tag( $name ) ) {
			return 0.0;
		}
		$count = isset( $tag->count ) ? (int) $tag->count : 0;
		if ( $count < 2 ) {
			return 0.0;
		}
		$score = nros_timeline_tag_weight( $count );
		if ( nros_timeline_tag_in_title( $name, $post_title ) ) {
			$score += min( 5.0, 2.0 + $score );
		}
		if ( strpos( nros_timeline_normalize_tag_label( $name ), ' ' ) !== false ) {
			$score += 0.5;
		}
		return $score;
	}
}

if ( ! function_exists( 'nros_gsc_click_bonus' ) ) {
    /**
     * Log-scaled bonus from GSC clicks so one viral article cannot dominate.
     * e.g. 0→0, 10→~1.6, 100→~3, capped by $cap.
     *
     * @param int   $clicks Total clicks in the window.
     * @param float $cap    Maximum bonus.
     * @param float $mult   Multiplier applied to log10(clicks+1).
     * @return float
     */
    function nros_gsc_click_bonus( $clicks, $cap = 3.0, $mult = 1.5 ) {
        $clicks = (int) $clicks;
        if ( $clicks < 1 ) {
            return 0.0;
        }
        return min( (float) $cap, log10( $clicks + 1 ) * (float) $mult );
    }
}

if ( ! function_exists( 'nros_timeline_pick_topic_tag' ) ) {
    /**
     * Choose the most meaningful tag to anchor a story timeline.
     * Prefers specific (low post-count) tags, boosts tags that appear in the
     * title, and avoids generic tags even when they happen to be in the title.
     *
     * @param array  $tag_objs   Result of wp_get_post_tags() (objects with ->term_id, ->name, ->count).
     * @param string $post_title Post title (any case).
     * @return object|null The chosen tag object, or null when no usable tag exists.
     */
    function nros_timeline_pick_topic_tag( $tag_objs, $post_title ) {
        if ( empty( $tag_objs ) || ! is_array( $tag_objs ) ) {
            return null;
        }

        $best       = null;
        $best_score = 0.0;

        foreach ( $tag_objs as $t ) {
            $score = nros_timeline_tag_topic_score( $t, $post_title );
            if ( $score > $best_score ) {
                $best_score = $score;
                $best       = $t;
            }
        }

        // Require a minimum topic score so we never fall back to "ΑΛΛΑΓΕΣ".
        if ( $best_score < 1.5 ) {
            return null;
        }

        return $best;
    }
}

if (!function_exists('nros_generate_timeline_html')) {
    function nros_generate_timeline_html($post_id, $manual_tag_id = 0, $manual_keyword = '') {
        global $wpdb;
        $limit = (int)get_option('newsai_timeline_limit', 4);
        $lang = function_exists( 'newsai_get_ui_lang' ) ? newsai_get_ui_lang() : 'en';
        $days_back = nros_get_timeline_days_back();
        $min_relevance = (int)get_option('newsai_timeline_relevance_threshold', 5);
        
        $scored_posts = [];
        $topic_name = '';
        $topic_link = '';

        if ($manual_tag_id || !empty($manual_keyword)) {
            $args = [
                'post_type' => 'post', 
                'posts_per_page' => $limit, 
                'post__not_in' => [$post_id], 
                'orderby' => 'date', 
                'order' => 'DESC', 
                'post_status' => 'publish',
                'date_query' => [
                    ['after' => $days_back . ' days ago']
                ]
            ];
            
            if ($manual_tag_id) {
                $args['tag_id'] = $manual_tag_id;
                $tag = get_tag($manual_tag_id);
                $topic_name = $tag->name;
                $topic_link = get_term_link($tag);
            } else {
                $args['s'] = $manual_keyword;
                $topic_name = $manual_keyword;
                $topic_link = home_url('/?s=' . urlencode($manual_keyword));
            }
            $needle_words   = !empty($manual_keyword) ? nros_get_significant_words($manual_keyword) : [];
            $needle_bigrams = !empty($manual_keyword) ? nros_get_significant_bigrams($manual_keyword) : [];
            $query = new WP_Query($args);
            if ($query->have_posts()) {
                while ($query->have_posts()) {
                    $query->the_post();
                    $score = 0;
                    $cand_title   = get_the_title();
                    $cand_words   = nros_get_significant_words($cand_title);
                    $cand_bigrams = nros_get_significant_bigrams($cand_title);

                    // Bigrams "καρφώνουν" συμφραζόμενα → υψηλότερο βάρος και πρωταρχικό σήμα.
                    if (!empty($needle_bigrams) && !empty($cand_bigrams)) {
                        $shared_bigrams = array_intersect($needle_bigrams, $cand_bigrams);
                        if (!empty($shared_bigrams)) {
                            $score += count($shared_bigrams) * 6;
                        }
                    }

                    // Μεμονωμένες λέξεις: χαμηλό βάρος και απαιτείται >=2 overlap για multi-word keywords,
                    // ώστε ένας τίτλος που τυχαία μοιράζεται μία λέξη να μη φουσκώνει score.
                    if (!empty($needle_words)) {
                        $shared_words = array_intersect($needle_words, $cand_words);
                        $word_overlap = count($shared_words);
                        if (count($needle_words) >= 2) {
                            if ($word_overlap >= 2) {
                                $score += $word_overlap * 2;
                            }
                        } elseif ($word_overlap === 1) {
                            $score += 2;
                        }
                    }

                    if ($manual_tag_id) { $score += 6; } // explicit manual topic anchor boost
                    if (!empty($manual_keyword) && mb_stripos($cand_title, $manual_keyword) !== false) $score += 5;
                    $days_old = (current_time('timestamp') - strtotime(get_the_date('Y-m-d H:i:s'))) / DAY_IN_SECONDS;
                    if ($days_old < 2) $score += 3;
                    elseif ($days_old < 7) $score += 2;
                    elseif ($days_old < 30) $score += 1;
                    if ($score >= $min_relevance) {
                        $scored_posts[] = [ 'id' => get_the_ID(), 'title' => $cand_title, 'date' => get_the_date('Y-m-d H:i:s'), 'score' => $score ];
                    }
                }
            }
            wp_reset_postdata();
            if (!empty($scored_posts)) {
                usort($scored_posts, function($a, $b) { return $b['score'] <=> $a['score']; });
                $scored_posts = array_slice($scored_posts, 0, $limit);
                usort($scored_posts, function($a, $b) { return strtotime($b['date']) <=> strtotime($a['date']); });
            }

        } else {
            $post_tags = wp_get_post_tags($post_id, ['fields' => 'ids']);
            if (empty($post_tags)) return ''; 

            $post_title    = get_the_title($post_id);
            $all_tags_objs = wp_get_post_tags($post_id);

            // Smart topic selection: pick the most specific tag (rare + in title),
            // not simply the first/alphabetical one (which is often generic).
            $topic_tag_obj = nros_timeline_pick_topic_tag( $all_tags_objs, $post_title );
            if ( ! $topic_tag_obj ) {
                // No tag shared by any other post — nothing to build a timeline from.
                return '';
            }
            $best_tag_id  = (int) $topic_tag_obj->term_id;
            $topic_name   = $topic_tag_obj->name;
            $topic_link   = get_term_link($topic_tag_obj);

            // Specificity weight per source tag, so generic shared tags barely count.
            $source_tag_weights = [];
            foreach ($all_tags_objs as $t) {
                if (isset($t->term_id)) {
                    $tid = (int) $t->term_id;
                    if ( nros_timeline_is_generic_tag( (string) $t->name ) ) {
                        $source_tag_weights[ $tid ] = 0.0;
                    } else {
                        $source_tag_weights[ $tid ] = nros_timeline_tag_weight( isset( $t->count ) ? $t->count : 0 );
                    }
                }
            }

            $cat_ids    = wp_get_post_categories($post_id, ['fields' => 'ids']);
            $date_after = function_exists( 'newsai_sql_datetime_days_ago' ) ? newsai_sql_datetime_days_ago( $days_back, false ) : gmdate( 'Y-m-d H:i:s', strtotime( '-' . (int) $days_back . ' days' ) );

            // 1) Topic-first pool: always pull siblings that share the SPECIFIC topic tag,
            //    so a burst of recent generic-tag posts can't crowd out the real story.
            $topic_sql = "
                SELECT DISTINCT p.ID, p.post_title, p.post_date
                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
                WHERE tt.term_id = %d AND tt.taxonomy = 'post_tag'
                AND p.ID != %d AND p.post_status = 'publish' AND p.post_type = 'post'
                AND p.post_date >= %s
                ORDER BY p.post_date DESC
                LIMIT 40
            ";
            $candidates = $wpdb->get_results($wpdb->prepare($topic_sql, $best_tag_id, $post_id, $date_after));
            $seen_ids   = [];
            foreach ((array) $candidates as $c) { $seen_ids[(int)$c->ID] = true; }

            // 2) Broaden to other shared tags only if the topic pool is thin, then dedupe.
            if (count($candidates) < max(6, $limit * 2)) {
                $tag_in_array = array_map('intval', $post_tags);
                $placeholders = implode(', ', array_fill(0, count($tag_in_array), '%d'));
                $broad_sql = "
                    SELECT DISTINCT p.ID, p.post_title, p.post_date
                    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
                    WHERE tt.term_id IN ($placeholders) AND tt.taxonomy = 'post_tag'
                    AND p.ID != %d AND p.post_status = 'publish' AND p.post_type = 'post'
                    AND p.post_date >= %s
                    ORDER BY p.post_date DESC
                    LIMIT 30
                ";
                $broad_args = array_merge($tag_in_array, [$post_id, $date_after]);
                $extra = $wpdb->get_results($wpdb->prepare($broad_sql, ...$broad_args));
                foreach ((array) $extra as $c) {
                    if (empty($seen_ids[(int)$c->ID])) {
                        $candidates[]           = $c;
                        $seen_ids[(int)$c->ID]  = true;
                    }
                }
            }

            if ($candidates) {
                $current_title_words = nros_get_significant_words(get_the_title($post_id));
                $current_title_bigrams = nros_get_significant_bigrams(get_the_title($post_id));

                // Bulk-load GSC clicks for all candidates in a single query (cached).
                $gsc_days = 30;
                $gsc_on   = function_exists('newsai_gsc_data_available') && newsai_gsc_data_available();
                if ($gsc_on && function_exists('newsai_gsc_prefetch_post_traffic')) {
                    $cand_ids = [];
                    foreach ($candidates as $c) { $cand_ids[] = (int) $c->ID; }
                    if (!empty($cand_ids)) {
                        newsai_gsc_prefetch_post_traffic($cand_ids, $gsc_days);
                    }
                }

                foreach ($candidates as $cand) {
                    $score = 0;
                    
                    $cand_tags   = wp_get_post_tags($cand->ID, ['fields' => 'ids']);
                    $shared_tags = array_intersect($post_tags, $cand_tags);

                    // IDF-weighted tag overlap: sharing "ΠΑΤΙΝΙ" is worth a lot,
                    // sharing "ΑΛΛΑΓΕΣ"/"ΑΣΤΥΝΟΜΙΑ" almost nothing.
                    $has_topic = false;
                    foreach ($shared_tags as $stid) {
                        $stid = (int) $stid;
                        $score += isset($source_tag_weights[$stid]) ? $source_tag_weights[$stid] : 1.0;
                        if ($stid === $best_tag_id) { $has_topic = true; }
                    }
                    if ($has_topic) {
                        $score += 4; // shares the specific story topic
                    }
                    
                    // Same category: strong soft boost (not a hard filter) so cross-section
                    // articles of the same story still qualify.
                    $cand_cats  = wp_get_post_categories($cand->ID, ['fields' => 'ids']);
                    $same_cat   = !empty(array_intersect($cat_ids, $cand_cats));
                    if ($same_cat) {
                        $score += 4;
                    }
                    
                    $cand_title_words = nros_get_significant_words($cand->post_title);
                    $cand_title_bigrams = nros_get_significant_bigrams($cand->post_title);
                    $shared_words = array_intersect($current_title_words, $cand_title_words);
                    $shared_bigrams = array_intersect($current_title_bigrams, $cand_title_bigrams);
                    $score += count($shared_words) * 2;
                    $score += count($shared_bigrams) * 4;
                    
                    $days_old = (current_time('timestamp') - strtotime($cand->post_date)) / DAY_IN_SECONDS;
                    if ($days_old < 2) $score += 3;
                    elseif ($days_old < 7) $score += 2;
                    elseif ($days_old < 30) $score += 1;

                    // Drop weak matches: sharing only generic tags (ΑΛΛΑΓΕΣ) must not pass.
                    if ( ! $has_topic && count( $shared_bigrams ) < 1 && count( $shared_words ) < 2 ) {
                        continue;
                    }

                    // GSC clicks: small, log-scaled bonus only. Recency + topic stay dominant
                    // so a brand-new (0-click) follow-up is never buried by an older popular post.
                    $clicks = 0;
                    if ($gsc_on && function_exists('newsai_gsc_get_post_traffic_cached')) {
                        $tr = newsai_gsc_get_post_traffic_cached($cand->ID, $gsc_days);
                        if ($tr && (int)($tr['total'] ?? 0) > 0) {
                            $clicks = (int) $tr['total'];
                            $score += nros_gsc_click_bonus($clicks, 3.0, 1.5);
                        }
                    }
                    
                    if ($score >= $min_relevance) { 
                        $scored_posts[] = [
                            'id'       => $cand->ID,
                            'title'    => $cand->post_title,
                            'date'     => $cand->post_date,
                            'score'    => $score,
                            'same_cat' => $same_cat ? 1 : 0,
                            'clicks'   => $clicks,
                        ];
                    }
                }
                
                if (!empty($scored_posts)) {
                    // Selection order: score, then same-category, then clicks as tiebreakers.
                    usort($scored_posts, function($a, $b) {
                        if ($b['score'] !== $a['score'])       { return $b['score'] <=> $a['score']; }
                        if ($b['same_cat'] !== $a['same_cat']) { return $b['same_cat'] <=> $a['same_cat']; }
                        return $b['clicks'] <=> $a['clicks'];
                    });
                    $scored_posts = array_slice($scored_posts, 0, $limit);
                    // Display order: chronological (newest → oldest).
                    usort($scored_posts, function($a, $b) { return strtotime($b['date']) <=> strtotime($a['date']); });
                }
            }
        }

        if (empty($scored_posts)) return '';

        $is_el = ( $lang === 'el' );
        $def_h = $is_el ? 'Η εξέλιξη της είδησης: {tag}' : 'Story Evolution: {tag}';
        $t_heading = newsai_timeline_resolve_label( 'newsai_timeline_heading', $def_h );
        $topic_url = is_wp_error( $topic_link ) ? '' : (string) $topic_link;
        $topic_anchor = '';
        if ( $topic_url !== '' && $topic_name !== '' ) {
            $topic_anchor = '<a class="newsai-story-cluster__topic-link" href="' . esc_url( $topic_url ) . '">' . esc_html( $topic_name ) . '</a>';
        }
        $heading_final = str_replace( '{tag}', $topic_anchor, $t_heading );
        $heading_html  = wp_kses(
            $heading_final,
            [
                'a' => [
                    'class' => [],
                    'href'  => [],
                    'rel'   => [],
                ],
            ]
        );

        $aria = esc_attr( newsai_story_cluster_aria_label() );
        $html = '<aside class="newsai-story-cluster newsai-story-cluster--timeline" role="complementary" aria-label="' . $aria . '">';
        if ( $heading_html !== '' ) {
            $html .= '<h3 class="newsai-story-cluster__heading">' . $heading_html . '</h3>';
        }
        $html .= '<ol class="newsai-story-cluster__timeline">';

        $total_posts = count( $scored_posts );
        $now         = current_time( 'timestamp' );

        foreach ( $scored_posts as $index => $p ) {
            $post_time = strtotime( $p['date'] );
            $diff_days = $post_time > 0 ? (int) floor( ( $now - $post_time ) / DAY_IN_SECONDS ) : 0;
            $time_label = newsai_timeline_format_item_date( $post_time, $diff_days, $index, $total_posts, $lang );
            $time_label = trim( (string) $time_label );

            $permalink = get_permalink( $p['id'] );
            $title     = isset( $p['title'] ) ? trim( (string) $p['title'] ) : '';
            if ( $title === '' || ! $permalink ) {
                continue;
            }

            $thumb = get_the_post_thumbnail_url( $p['id'], 'thumbnail' );
            $html .= '<li class="newsai-story-cluster__item">';
            if ( $thumb ) {
                $html .= '<img class="newsai-story-cluster__thumb" src="' . esc_url( $thumb ) . '" alt="' . esc_attr( $title ) . '" loading="lazy" decoding="async" width="60" height="60">';
            }
            $html .= '<div class="newsai-story-cluster__body">';
            if ( $time_label !== '' ) {
                $html .= '<span class="newsai-story-cluster__date">' . esc_html( $time_label ) . '</span>';
            }
            $html .= '<a class="newsai-story-cluster__link" href="' . esc_url( $permalink ) . '">' . esc_html( $title ) . '</a>';
            $html .= '</div></li>';
        }

        $html .= '</ol>';

        // If every candidate failed (empty title / broken permalink), do not leave an empty shell.
        if ( strpos( $html, 'newsai-story-cluster__item' ) === false ) {
            return '';
        }

        $btn_text = newsai_timeline_resolve_label(
            'newsai_timeline_label_full',
            $is_el ? 'Όλο το χρονικό' : 'Full timeline'
        );
        if ( $btn_text !== '' && $topic_url !== '' ) {
            $html .= '<p class="newsai-story-cluster__footer"><a class="newsai-story-cluster__more" href="' . esc_url( $topic_url ) . '">' . esc_html( $btn_text ) . '</a></p>';
        }
        $html .= '</aside>';
        return $html;
    }
}

add_shortcode('newsroom_timeline', 'newsai_timeline_shortcode_callback');
if (!function_exists('newsai_timeline_shortcode_callback')) {
    function newsai_timeline_shortcode_callback($atts = []) {
        global $post;
        if ( ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) || ( function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) ) {
            return '';
        }
        if (!$post) return '';
        
        $a = shortcode_atts(['tag_id' => 0, 'keyword' => ''], $atts);
        
        $cache_key = 'nros_timeline_html_v6_' . $post->ID;
        if ( empty( $a['tag_id'] ) && empty( $a['keyword'] ) && function_exists( 'newsai_timeline_get_cached_html' ) ) {
            $cached = newsai_timeline_get_cached_html( (int) $post->ID );
            if ( $cached !== '' ) {
                return $cached;
            }
        } else {
            $cached = get_transient( $cache_key );
            if ( false !== $cached && ( ! function_exists( 'newsai_timeline_html_is_valid' ) || newsai_timeline_html_is_valid( $cached ) ) ) {
                return $cached;
            }
        }
        
        $html = nros_generate_timeline_html($post->ID, (int)$a['tag_id'], sanitize_text_field($a['keyword']));
        if (empty($a['tag_id']) && empty($a['keyword'])) {
            // Never persist empty shells — clear stale cache so Auto regenerates like Manual.
            if ( $html === '' || $html === null || ( function_exists( 'newsai_timeline_html_is_valid' ) && ! newsai_timeline_html_is_valid( $html ) ) ) {
                if ( function_exists( 'newsai_timeline_clear_cached_html' ) ) {
                    newsai_timeline_clear_cached_html( (int) $post->ID );
                } else {
                    delete_transient( $cache_key );
                }
                return '';
            } elseif ( function_exists( 'newsai_timeline_set_cached_html' ) ) {
                newsai_timeline_set_cached_html( (int) $post->ID, (string) $html );
            } else {
                set_transient( $cache_key, $html, 12 * HOUR_IN_SECONDS );
            }
        }
        return $html;
    }
}

if ( ! function_exists( 'newsai_timeline_mask_editor_blocks' ) ) {
	/**
	 * Replace Newsroom editor blocks (GEO summary, key points, FAQ) with placeholders so
	 * Auto-Timeline paragraph counting ignores their inner <p> tags.
	 *
	 * @param string $content Post HTML.
	 * @return array{0:string,1:array<string,string>} Tuple of masked HTML and placeholder map.
	 */
	function newsai_timeline_mask_editor_blocks( $content ) {
		$content = (string) $content;
		$blocks  = [];
		$index   = 0;
		$masked  = preg_replace_callback(
			'/<(section|aside)\b[^>]*class="[^"]*(?:nros-geo-summary|newsai-key-points|newsai-faq)[^"]*"[^>]*>.*?<\/\1>/is',
			static function ( $matches ) use ( &$blocks, &$index ) {
				$key            = '<!--NEWSAI_TL_BLOCK_' . $index . '-->';
				$blocks[ $key ] = $matches[0];
				$index++;
				return $key;
			},
			$content
		);
		if ( ! is_string( $masked ) ) {
			$masked = $content;
		}
		return [ $masked, $blocks ];
	}
}

if ( ! function_exists( 'newsai_timeline_unmask_editor_blocks' ) ) {
	/**
	 * @param string               $content Masked HTML.
	 * @param array<string,string> $blocks  Placeholder map.
	 * @return string
	 */
	function newsai_timeline_unmask_editor_blocks( $content, $blocks ) {
		if ( ! is_array( $blocks ) || $blocks === [] ) {
			return (string) $content;
		}
		return str_replace( array_keys( $blocks ), array_values( $blocks ), (string) $content );
	}
}

if ( ! function_exists( 'newsai_timeline_inject_after_body_paragraph' ) ) {
	/**
	 * Inject HTML after the Nth body paragraph, skipping plugin editor blocks.
	 *
	 * @param string $content    Post HTML.
	 * @param string $inject     Timeline HTML.
	 * @param int    $para_index 1-based paragraph index (Settings → Auto-Timeline).
	 * @return string
	 */
	function newsai_timeline_inject_after_body_paragraph( $content, $inject, $para_index ) {
		list( $masked, $blocks ) = newsai_timeline_mask_editor_blocks( $content );
		$paragraphs              = explode( '</p>', $masked );
		$para_index              = (int) $para_index;

		if ( $para_index <= 0 || $para_index >= count( $paragraphs ) ) {
			return $content . $inject;
		}

		$new_content = '';
		foreach ( $paragraphs as $index => $para ) {
			$new_content .= $para . '</p>';
			if ( ( $index + 1 ) === $para_index ) {
				$new_content .= $inject;
			}
		}

		return newsai_timeline_unmask_editor_blocks( $new_content, $blocks );
	}
}

add_filter('the_content', 'newsai_timeline_auto_inject', 20);
if (!function_exists('newsai_timeline_auto_inject')) {
    function newsai_timeline_auto_inject($content) {
        if ( function_exists( 'newsai_should_skip_auto_timeline_inject' ) && newsai_should_skip_auto_timeline_inject() ) {
            return $content;
        }
        if ( function_exists( 'newsai_lite_mode_skips_frontend_injections' ) && newsai_lite_mode_skips_frontend_injections() ) {
            return $content;
        }
        // Never mutate post content during REST/JSON — can corrupt API responses and Site Health checks.
        if ( ( function_exists( 'newsai_is_http_rest_request' ) && newsai_is_http_rest_request() ) || ( function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) ) {
            return $content;
        }
        if (!is_single() || is_admin() || get_option('newsai_timeline_mode', 'auto') !== 'auto') return $content;
        // Auto-Timeline auto-injection is a Pro feature; manual shortcode keeps working for free users.
        if (!function_exists('newsai_is_pro') || !newsai_is_pro()) return $content;
        if (get_post_meta(get_the_ID(), '_newsai_disable_auto_timeline', true) === 'yes' || has_shortcode($content, 'newsroom_timeline')) return $content;
        // Already has a story cluster in the body (manual insert) — do not inject a second (often empty) one.
        if ( strpos( $content, 'newsai-story-cluster' ) !== false || strpos( $content, 'newsroom-os-story-cluster' ) !== false ) {
            return $content;
        }
        
        $timeline = newsai_timeline_shortcode_callback();
        if (empty($timeline) || ( function_exists( 'newsai_timeline_html_is_valid' ) && ! newsai_timeline_html_is_valid( $timeline ) )) return $content;

        $para_index = (int) get_option( 'newsai_timeline_para', 3 );
        return newsai_timeline_inject_after_body_paragraph( $content, $timeline, $para_index );
    }
}
add_action('wp_ajax_newsai_build_timeline', 'newsai_manual_timeline_ajax');
if (!function_exists('newsai_manual_timeline_ajax')) {
    function newsai_manual_timeline_ajax() {
        newsai_editor_ajax_gate();
        $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
        newsai_editor_ajax_require_post_edit( $post_id );
        $tag_id = isset($_POST['tag_id']) ? intval($_POST['tag_id']) : 0;
        $keyword = isset($_POST['keyword']) ? sanitize_text_field($_POST['keyword']) : '';

        $post_tags = wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] );
        if ( empty( $post_tags ) && $tag_id < 1 && $keyword === '' ) {
            wp_send_json_error(
                [
                    'code'    => 'no_tags',
                    'message' => __( 'Add at least one tag to this post and save before building a timeline.', 'newsroom-ai-assistant' ),
                ]
            );
        }

        $html = nros_generate_timeline_html($post_id, $tag_id, $keyword);
        if ( $html ) {
            wp_send_json_success( $html );
        }
        wp_send_json_error(
            [
                'code'    => 'no_matches',
                'message' => __( 'Not enough related published posts found for a timeline. Try more tags, a lower relevance threshold in Settings, or Evergreen strategy.', 'newsroom-ai-assistant' ),
            ]
        );
    }
}

// 🤖 AI PROMPTS GENERATOR 
add_action('wp_ajax_newsai_get_prompt', 'newsai_ajax_get_prompt');
if (!function_exists('newsai_ajax_get_prompt')) {
    function newsai_ajax_get_prompt() {
        newsai_editor_ajax_gate();
        $type = isset($_POST['type']) ? sanitize_key( $_POST['type'] ) : '';
        $allowed_types = [ 'title', 'rewrite', 'seo' ];
        if ( ! in_array( $type, $allowed_types, true ) ) {
            wp_send_json_error( [ 'message' => 'invalid_type' ], 400 );
        }
        $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
        $content = isset($_POST['content']) ? sanitize_textarea_field($_POST['content']) : '';
        
        $lang = newsai_get_ui_lang();
        
        $prompt_template = get_option('newsai_prompt_' . $type, '');
        if (empty(trim($prompt_template)) && function_exists('newsai_get_default_prompt')) {
            $prompt_template = newsai_get_default_prompt($type, $lang);
        }
        
        // 🚀 FIX: Προσθήκη Site Name και Site Desc για το Rewrite Prompt!
        $site_name = get_option('nros_site_name', get_bloginfo('name'));
        $site_desc = get_option('newsai_site_desc', get_bloginfo('description'));
        
        $prompt = str_replace(
            ['{title}', '{content}', '{site}', '{site_desc}'], 
            [$title, $content, $site_name, $site_desc], 
            $prompt_template
        );
        
        if ($prompt) { wp_send_json_success($prompt); } else { wp_send_json_error(); }
    }
}

// ==========================================
// 5. CACHE CLEARING & SEO SCANNER RESTORATION
// ==========================================
add_action('save_post', 'newsroom_os_clear_timeline_cache');
if (!function_exists('newsroom_os_clear_timeline_cache')) {
    function newsroom_os_clear_timeline_cache($post_id) {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if ( function_exists( 'newsai_timeline_clear_cached_html' ) ) {
            newsai_timeline_clear_cached_html( (int) $post_id );
        }
        if ( function_exists( 'newsai_delete_transients_by_prefix' ) ) {
            newsai_delete_transients_by_prefix( 'nros_timeline_html_' );
        }
    }
}

if (!function_exists('newsai_invalidate_task_due_metrics_cache')) {
    function newsai_invalidate_task_due_metrics_cache() {
        delete_transient( 'newsai_dash_due_metrics_v1' );
        delete_transient( 'newsai_dash_due_metrics_v2' );
    }
}

if (!function_exists('newsai_bump_post_derived_dashboard_caches')) {
    function newsai_bump_post_derived_dashboard_caches( $post_id ) {
        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
            return;
        }
        if ( get_post_type( (int) $post_id ) !== 'post' ) {
            return;
        }
        delete_transient( 'newsai_missing_seo_cnt_v1' );
        delete_transient( 'newsai_dash_post_kpis_v1' );
    }
}
add_action( 'save_post', 'newsai_bump_post_derived_dashboard_caches', 15 );

if (!function_exists('newsai_compute_task_due_counts_from_rows')) {
    /**
     * Overdue / due-today counts for editorial tasks (matches dashboard recurrence rules).
     *
     * @param array<object{year:int,month:int,day:int}>|null $rows Lightweight rows (year, month, day only).
     * @return array{overdue:int,due_today:int}
     */
    function newsai_compute_task_due_counts_from_rows( $rows ) {
        $now_time = strtotime( gmdate( 'Y-m-d' ) );
        $overdue_count = 0;
        $due_today_count = 0;
        if ( empty( $rows ) ) {
            return [ 'overdue' => 0, 'due_today' => 0 ];
        }
        foreach ( $rows as $r ) {
            $t_year = ( (int) $r->year > 0 ) ? (int) $r->year : (int) gmdate( 'Y' );
            $t_month = ( (int) $r->month > 0 ) ? (int) $r->month : (int) gmdate( 'm' );
            $task_time = strtotime( sprintf( '%04d-%02d-%02d', $t_year, $t_month, (int) $r->day ) );
            if ( (int) $r->year === 0 && (int) $r->month > 0 ) {
                if ( ( $now_time - $task_time ) > ( 15 * 86400 ) ) {
                    $t_year++;
                    $task_time = strtotime( sprintf( '%04d-%02d-%02d', $t_year, $t_month, (int) $r->day ) );
                }
            } elseif ( (int) $r->year === 0 && (int) $r->month === 0 ) {
                if ( ( $now_time - $task_time ) > ( 5 * 86400 ) ) {
                    $t_month++;
                    if ( $t_month > 12 ) {
                        $t_month = 1;
                        $t_year++;
                    }
                    $task_time = strtotime( sprintf( '%04d-%02d-%02d', $t_year, $t_month, (int) $r->day ) );
                }
            }
            $diff_days = ( $task_time - $now_time ) / 86400;
            if ( $diff_days < 0 ) {
                $overdue_count++;
            } elseif ( $diff_days === 0 ) {
                $due_today_count++;
            }
        }
        return [ 'overdue' => $overdue_count, 'due_today' => $due_today_count ];
    }
}

if (!function_exists('newsai_get_dashboard_due_metrics_cached')) {
    /**
     * Cached overdue / due-today KPIs (non-not-done tasks only). Uses narrow SELECT + PHP recurrence logic.
     *
     * @param string $table Full alerts table name ($wpdb->prefix . 'newsai_alerts').
     */
    function newsai_get_dashboard_due_metrics_cached( $table ) {
        $cached = get_transient( 'newsai_dash_due_metrics_v2' );
        if ( false !== $cached && is_array( $cached ) && isset( $cached['overdue'], $cached['due_today'] ) ) {
            return $cached;
        }
        global $wpdb;
        $today = gmdate( 'Y-m-d' );
        // Fast path: tasks with a full calendar date (majority of rows).
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $overdue = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table}
                 WHERE status != 'done'
                 AND year > 0 AND month > 0 AND day > 0
                 AND STR_TO_DATE(CONCAT(year, '-', LPAD(month, 2, '0'), '-', LPAD(day, 2, '0')), '%%Y-%%m-%%d') < %s",
                $today
            )
        );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $due_today = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table}
                 WHERE status != 'done'
                 AND year > 0 AND month > 0 AND day > 0
                 AND STR_TO_DATE(CONCAT(year, '-', LPAD(month, 2, '0'), '-', LPAD(day, 2, '0')), '%%Y-%%m-%%d') = %s",
                $today
            )
        );
        // Recurrence rules for year/month rollover (smaller subset).
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $fuzzy_rows = $wpdb->get_results(
            "SELECT year, month, day FROM {$table} WHERE status != 'done' AND (year = 0 OR month = 0 OR day = 0)"
        );
        $fuzzy = newsai_compute_task_due_counts_from_rows( $fuzzy_rows );
        $counts = [
            'overdue'   => $overdue + (int) $fuzzy['overdue'],
            'due_today' => $due_today + (int) $fuzzy['due_today'],
        ];
        set_transient( 'newsai_dash_due_metrics_v2', $counts, 15 * MINUTE_IN_SECONDS );
        return $counts;
    }
}

if (!function_exists('newsai_get_dashboard_post_publish_kpis')) {
    /**
     * Weekly / 30-day publish counts for the Chief Dashboard (1h transient).
     *
     * @return array{posts_this_week:int,posts_last_week:int,total_posts_30d:int}
     */
    function newsai_get_dashboard_post_publish_kpis() {
        $cached = get_transient( 'newsai_dash_post_kpis_v1' );
        if ( false !== $cached && is_array( $cached ) && isset( $cached['posts_this_week'], $cached['posts_last_week'], $cached['total_posts_30d'] ) ) {
            return $cached;
        }
        global $wpdb;
        $week_ago   = newsai_sql_datetime_days_ago( 7, false );
        $two_weeks  = newsai_sql_datetime_days_ago( 14, false );
        $month_ago  = newsai_sql_datetime_days_ago( 30, false );
        $posts_this_week = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date > %s", $week_ago ) );
        $posts_last_week = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date > %s AND post_date <= %s", $two_weeks, $week_ago ) );
        $total_posts_30d = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date > %s", $month_ago ) );
        $out = [
            'posts_this_week'   => $posts_this_week,
            'posts_last_week'   => $posts_last_week,
            'total_posts_30d'   => $total_posts_30d,
        ];
        set_transient( 'newsai_dash_post_kpis_v1', $out, HOUR_IN_SECONDS );
        return $out;
    }
}

if (!function_exists('newsroom_os_get_missing_seo_count')) {
    function newsroom_os_get_missing_seo_count() {
        $cached = get_transient( 'newsai_missing_seo_cnt_v1' );
        if ( false !== $cached ) {
            return (int) $cached;
        }
        global $wpdb;
        // 🚀 FIX: Αγνόησε άρθρα που έχουν αρκετό κείμενο (πάνω από 600 chars), αφήνοντας το Rank Math να κάνει auto-generate.
        $month_ago = newsai_sql_datetime_days_ago( 30, false );
        $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(p.ID) FROM {$wpdb->posts} p WHERE p.post_type = 'post' AND p.post_status = 'publish' AND p.post_date > %s AND p.post_excerpt = '' AND LENGTH(p.post_content) < 600 AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key IN ('_yoast_wpseo_metadesc', 'rank_math_description') AND pm.meta_value != '')", $month_ago ) );
        set_transient( 'newsai_missing_seo_cnt_v1', $count, 15 * MINUTE_IN_SECONDS );
        return $count;
    }
}

add_action( 'pre_get_posts', 'newsroom_os_filter_missing_seo_posts' );
if (!function_exists('newsroom_os_filter_missing_seo_posts')) {
    function newsroom_os_filter_missing_seo_posts( $query ) {
        global $pagenow, $wpdb;
        if ( is_admin() && $pagenow === 'edit.php' && $query->is_main_query() && isset( $_GET['newsai_filter'] ) && $_GET['newsai_filter'] === 'missing_seo' ) {
            $month_ago = newsai_sql_datetime_days_ago( 30, false );
            $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p WHERE p.post_type = 'post' AND p.post_status = 'publish' AND p.post_date > %s AND p.post_excerpt = '' AND LENGTH(p.post_content) < 600 AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key IN ('_yoast_wpseo_metadesc', 'rank_math_description') AND pm.meta_value != '')", $month_ago ) );
            if (empty($post_ids)) { $post_ids = [0]; } 
            $query->set('post__in', $post_ids);
            add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>🚨 <strong>Newsroom OS:</strong> Προβολή άρθρων με "Thin Content" (κάτω από ~100 λέξεις) που δεν έχουν ούτε χειροκίνητο Meta Description.</p></div>'; });
        }
    }
}

// 🚀 FIX: Ασφαλής αποθήκευση του Checkbox για την Απενεργοποίηση του Timeline με Nonce Check
add_action('save_post', 'newsai_save_timeline_meta');
if (!function_exists('newsai_save_timeline_meta')) {
    function newsai_save_timeline_meta($post_id) {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (!current_user_can('edit_post', $post_id)) return;
        
        // Έλεγχος Ασφαλείας: Σιγουρευόμαστε ότι το request έρχεται από τη δεξιά μπάρα
        if (!isset($_POST['newsai_sidebar_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['newsai_sidebar_nonce'])), 'newsai_sidebar_save')) {
            return;
        }
        
        // Αν το checkbox είναι τσεκαρισμένο, σώσε το. Αλλιώς διέγραψέ το.
        if (isset($_POST['newsai_disable_auto_timeline'])) {
            update_post_meta($post_id, '_newsai_disable_auto_timeline', 'yes');
        } else {
            delete_post_meta($post_id, '_newsai_disable_auto_timeline');
        }

        if ( isset( $_POST['newsai_post_target_geo'] ) && function_exists( 'newsai_normalize_geo_code' ) ) {
            $post_geo = newsai_normalize_geo_code( wp_unslash( (string) $_POST['newsai_post_target_geo'] ) );
            if ( $post_geo !== '' ) {
                update_post_meta( $post_id, '_newsai_target_geo', $post_geo );
            } else {
                delete_post_meta( $post_id, '_newsai_target_geo' );
            }
        }

    }
}

// ==========================================
// 6. GOOGLE SEARCH STATUS API (CORE UPDATES)
// ==========================================
if (!function_exists('newsroom_os_check_google_updates')) {
    function newsroom_os_check_google_updates() {
        // 🚀 FIX: v9 για άμεση ενημέρωση και σωστό Rollout Date
        $cache_key = 'nros_google_status_v9';
        $cached_status = get_transient($cache_key);
        if (false !== $cached_status) { return $cached_status; }

        $is_gr = (newsai_get_ui_lang() === 'el');
        $status = ['state' => 'clear', 'message' => $is_gr ? 'Δεν εντοπίστηκαν ενεργές ενημερώσεις Google' : 'No active Google updates detected', 'link' => 'https://status.search.google.com/'];
        $response = wp_remote_get('https://status.search.google.com/incidents.json', ['timeout' => 5]);
        
        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
            $incidents = json_decode(wp_remote_retrieve_body($response), true);
            if (is_array($incidents) && !empty($incidents)) {
                $latest_incident = null;
                $highest_time = 0;
                
                foreach ($incidents as $incident) {
                    $begin_timestamp = !empty($incident['begin']) ? strtotime($incident['begin']) : (!empty($incident['created']) ? strtotime($incident['created']) : 0);
                    if ($begin_timestamp >= $highest_time) {
                        $highest_time = $begin_timestamp;
                        $latest_incident = $incident;
                    }
                }
                
                if ($latest_incident) {
                    $is_resolved = (!empty($latest_incident['end'])) ? true : false;
                    $formatted_end_date = $is_resolved ? date('j/n', strtotime($latest_incident['end'])) : '';

                    $incident_title = !empty($latest_incident['summary']) ? $latest_incident['summary'] : (!empty($latest_incident['title']) ? $latest_incident['title'] : 'Google Update');
                    if (mb_strlen($incident_title) > 60) { $incident_title = mb_substr($incident_title, 0, 57) . '...'; }
                    
                    $link = 'https://status.search.google.com/';
                    if (!empty($latest_incident['uri'])) { $link = 'https://status.search.google.com/' . ltrim($latest_incident['uri'], '/'); }
                    
                    $is_gr = (newsai_get_ui_lang() === 'el');

                    if (!$is_resolved) {
                        $status = ['state' => 'active', 'message' => '🚨 ' . sanitize_text_field($incident_title), 'link' => esc_url_raw($link)];
                    } else {
                        $msg = ($is_gr ? "✅ Ολοκληρώθηκε: " : "✅ Completed: ") . $incident_title;
                        if ($formatted_end_date) { $msg .= " (Rollout: $formatted_end_date)"; }
                        $status = ['state' => 'clear', 'message' => sanitize_text_field($msg), 'link' => esc_url_raw($link)];
                    }
                }
            }
        }
        set_transient($cache_key, $status, 12 * HOUR_IN_SECONDS);
        return $status;
    }
}
// 🚀 FIX: Αυτόματος καθαρισμός παλιών Tasks (Database Hygiene)
add_action('newsai_weekly_cleanup', 'newsai_cleanup_old_tasks');
add_filter('cron_schedules', 'newsai_register_weekly_cron_interval');
if (!function_exists('newsai_register_weekly_cron_interval')) {
    function newsai_register_weekly_cron_interval($schedules) {
        if (!isset($schedules['weekly'])) {
            $schedules['weekly'] = [
                'interval' => 7 * DAY_IN_SECONDS,
                'display'  => 'Once Weekly'
            ];
        }
        return $schedules;
    }
}

function newsai_cleanup_old_tasks() {
    global $wpdb;
    $table = $wpdb->prefix . 'newsai_alerts';
    // Διαγραφή ολοκληρωμένων one-time tasks (year > 0) με ημερομηνία > 90 μέρες παλιά.
    // year = 0 αφορά recurring entries — εξαιρούνται σκόπιμα από αυτό το cleanup.
    // Χρησιμοποιούμε LPAD για zero-padded lexical comparison ώστε να αποφύγουμε STR_TO_DATE
    // (που σε strict sql_mode μπορεί να επιστρέψει NULL και να αφήνει σιωπηλά τα orphan rows).
    $cutoff = gmdate('Y-m-d', strtotime('-90 days'));
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$table}
            WHERE status = 'done'
              AND year > 0
              AND CONCAT(LPAD(year, 4, '0'), '-', LPAD(month, 2, '0'), '-', LPAD(day, 2, '0')) < %s",
            $cutoff
        )
    );
}

// Προγραμματισμός του cleanup — admin/activation only (avoid wp_next_scheduled on every frontend hit).
if ( ! function_exists( 'newsai_schedule_weekly_cleanup' ) ) {
    function newsai_schedule_weekly_cleanup() {
        if ( wp_next_scheduled( 'newsai_weekly_cleanup' ) ) {
            return;
        }
        $first = time() + 6 * HOUR_IN_SECONDS;
        wp_schedule_event( $first, 'weekly', 'newsai_weekly_cleanup' );
    }
}
add_action( 'admin_init', 'newsai_schedule_weekly_cleanup' );

// ==========================================
// 8. INSTANT INDEXING PING (ASYNC ON SAVE)
// ==========================================
add_action( 'save_post', 'newsai_queue_indexing_ping', 20, 3 );
add_action( 'transition_post_status', 'newsai_queue_indexing_ping_on_publish', 20, 3 );
add_action( 'newsai_send_indexing_ping', 'newsai_send_indexing_ping_callback', 10, 1 );

if (!function_exists('newsai_base64url_encode')) {
    function newsai_base64url_encode($data) {
        return rtrim(strtr(base64_encode((string) $data), '+/', '-_'), '=');
    }
}

if (!function_exists('newsai_get_service_account_access_token')) {
    /**
     * Service-account OAuth access token (JWT bearer).
     *
     * @param string $error          Populated on failure.
     * @param string $scope          Google OAuth scope (default: Indexing API).
     * @param string $cache_transient Transient key for caching the token.
     * @return string Bearer token or empty string.
     */
    function newsai_get_service_account_access_token( &$error = '', $scope = '', $cache_transient = '' ) {
        if ( $scope === '' ) {
            $scope = 'https://www.googleapis.com/auth/indexing';
        }
        if ( $cache_transient === '' ) {
            $cache_transient = 'newsai_indexing_sa_access_token';
        }

        $cached = get_transient( $cache_transient );
        if ( is_string( $cached ) && $cached !== '' ) {
            return $cached;
        }

        $json_raw = function_exists( 'newsai_secret_get' )
            ? newsai_secret_get( 'indexing_service_json' )
            : (string) get_option( 'newsai_indexing_service_json', '' );
        if ($json_raw === '') {
            $error = 'Service account JSON is empty.';
            return '';
        }

        $sa = json_decode($json_raw, true);
        if (!is_array($sa)) {
            $error = 'Invalid service account JSON.';
            return '';
        }

        $client_email = isset($sa['client_email']) ? sanitize_email((string) $sa['client_email']) : '';
        $private_key = isset($sa['private_key']) ? (string) $sa['private_key'] : '';
        $token_uri = isset($sa['token_uri']) ? esc_url_raw((string) $sa['token_uri']) : 'https://oauth2.googleapis.com/token';

        if ($client_email === '' || $private_key === '') {
            $error = 'Service account JSON is missing client_email/private_key.';
            return '';
        }

        $private_key_raw = $private_key;
        if ( function_exists( 'newsai_normalize_sa_private_key' ) ) {
            $private_key = newsai_normalize_sa_private_key( $private_key );
            if ( $private_key !== $private_key_raw && function_exists( 'newsai_sa_openssl_private_key' ) && newsai_sa_openssl_private_key( $private_key ) ) {
                $sa['private_key'] = $private_key;
                $repaired = wp_json_encode( $sa, JSON_UNESCAPED_SLASHES );
                if ( is_string( $repaired ) && $repaired !== '' ) {
                    if ( function_exists( 'newsai_secret_update_option' ) ) {
                        newsai_secret_update_option( 'indexing_service_json', $repaired );
                    } else {
                        update_option( 'newsai_indexing_service_json', $repaired, false );
                    }
                }
            }
        } elseif ( strpos( $private_key, '\n' ) !== false ) {
            $private_key = str_replace( '\n', "\n", $private_key );
        }

        $pkey = function_exists( 'newsai_sa_openssl_private_key' )
            ? newsai_sa_openssl_private_key( $private_key )
            : openssl_pkey_get_private( $private_key );
        if ( ! $pkey ) {
            $ossl  = function_exists( 'newsai_sa_private_key_openssl_error' ) ? newsai_sa_private_key_openssl_error() : '';
            $error = $ossl !== ''
                ? 'Invalid service account private key: ' . $ossl
                : 'Failed to parse service account private key. Re-save the full JSON key from Google Cloud.';
            return '';
        }

        $now = time();
        $header = ['alg' => 'RS256', 'typ' => 'JWT'];
        $claim = [
            'iss'   => $client_email,
            'scope' => $scope,
            'aud'   => $token_uri,
            'iat'   => $now,
            'exp'   => $now + 3600
        ];

        $jwt_unsigned = newsai_base64url_encode(wp_json_encode($header)) . '.' . newsai_base64url_encode(wp_json_encode($claim));
        $signature = '';
        $sign_ok = openssl_sign($jwt_unsigned, $signature, $pkey, OPENSSL_ALGO_SHA256);
        if ( is_resource( $pkey ) ) {
            openssl_pkey_free( $pkey );
        }
        if (!$sign_ok) {
            $ossl  = function_exists( 'newsai_sa_private_key_openssl_error' ) ? newsai_sa_private_key_openssl_error() : '';
            $error = $ossl !== ''
                ? 'Failed to sign JWT: ' . $ossl
                : 'Failed to sign JWT with provided private key.';
            return '';
        }
        $jwt = $jwt_unsigned . '.' . newsai_base64url_encode($signature);

        $response = wp_remote_post($token_uri, [
            'timeout' => 8,
            'body' => [
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion'  => $jwt
            ]
        ]);
        if (is_wp_error($response)) {
            $error = $response->get_error_message();
            return '';
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $body = json_decode((string) wp_remote_retrieve_body($response), true);
        if ($code < 200 || $code >= 300 || empty($body['access_token'])) {
            $error = 'Token endpoint error (HTTP ' . $code . ').';
            if (is_array($body) && !empty($body['error_description'])) {
                $error = sanitize_text_field((string) $body['error_description']);
            } elseif (is_array($body) && !empty($body['error'])) {
                $error = sanitize_text_field((string) $body['error']);
            }
            return '';
        }

        $access_token = sanitize_text_field((string) $body['access_token']);
        $expires_in = isset($body['expires_in']) ? max(300, (int) $body['expires_in']) : 3600;
        set_transient( $cache_transient, $access_token, max( 300, $expires_in - 60 ) );
        update_option('newsai_indexing_service_email', $client_email, false);
        return $access_token;
    }
}

if ( ! function_exists( 'newsai_should_ping_post_for_indexing' ) ) {
	/**
	 * @param int      $post_id Post ID.
	 * @param WP_Post  $post    Post object.
	 * @param bool     $require_publish_transition When true, caller already verified publish transition.
	 * @return bool
	 */
	function newsai_should_ping_post_for_indexing( $post_id, $post, $require_publish_transition = false ) {
		unset( $require_publish_transition );
		if ( function_exists( 'newsai_is_pro' ) && ! newsai_is_pro() ) {
			return false;
		}
		$google_on   = ( get_option( 'newsai_indexing_enabled', 'no' ) === 'yes' );
		$indexnow_on = ( get_option( 'newsai_indexnow_enabled', 'no' ) === 'yes' );
		if ( ! $google_on && ! $indexnow_on ) {
			return false;
		}
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return false;
		}
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return false;
		}
		if ( ! $post || $post->post_type !== 'post' ) {
			return false;
		}
		// Cron / scheduled publish may have no logged-in user — still allow ping.
		if ( get_current_user_id() > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
			return false;
		}
		$publish_only = ( get_option( 'newsai_indexing_publish_only', 'yes' ) === 'yes' );
		if ( $publish_only && $post->post_status !== 'publish' ) {
			return false;
		}
		return true;
	}
}

if ( ! function_exists( 'newsai_acquire_indexing_ping_lock' ) ) {
	/**
	 * Debounce duplicate save_post / transition hooks for the same post.
	 *
	 * @param int $post_id Post ID.
	 * @return bool True when this request may proceed.
	 */
	function newsai_acquire_indexing_ping_lock( $post_id ) {
		$lock_key = 'newsai_index_ping_lock_' . (int) $post_id;
		if ( get_transient( $lock_key ) ) {
			return false;
		}
		$token = wp_generate_password( 20, false, false );
		set_transient( $lock_key, $token, 10 * MINUTE_IN_SECONDS );
		return ( (string) get_transient( $lock_key ) ) === $token;
	}
}

if ( ! function_exists( 'newsai_enqueue_indexing_ping' ) ) {
	/**
	 * Queue Google Indexing API ping for end of current request (reliable on publish).
	 *
	 * @param int $post_id Post ID.
	 */
	function newsai_enqueue_indexing_ping( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id < 1 ) {
			return;
		}
		if ( ! isset( $GLOBALS['newsai_pending_indexing_ping_ids'] ) || ! is_array( $GLOBALS['newsai_pending_indexing_ping_ids'] ) ) {
			$GLOBALS['newsai_pending_indexing_ping_ids'] = [];
		}
		$GLOBALS['newsai_pending_indexing_ping_ids'][ $post_id ] = true;
		if ( ! has_action( 'shutdown', 'newsai_flush_pending_indexing_pings' ) ) {
			add_action( 'shutdown', 'newsai_flush_pending_indexing_pings', 99 );
		}
		// Backup for hosts where shutdown is skipped (very rare).
		if ( ! wp_next_scheduled( 'newsai_send_indexing_ping', [ $post_id ] ) ) {
			wp_schedule_single_event( time() + 120, 'newsai_send_indexing_ping', [ $post_id ] );
		}
	}
}

if ( ! function_exists( 'newsai_flush_pending_indexing_pings' ) ) {
	function newsai_flush_pending_indexing_pings() {
		if ( empty( $GLOBALS['newsai_pending_indexing_ping_ids'] ) || ! is_array( $GLOBALS['newsai_pending_indexing_ping_ids'] ) ) {
			return;
		}
		$ids = array_keys( $GLOBALS['newsai_pending_indexing_ping_ids'] );
		$GLOBALS['newsai_pending_indexing_ping_ids'] = [];
		foreach ( $ids as $post_id ) {
			$post_id = (int) $post_id;
			wp_clear_scheduled_hook( 'newsai_send_indexing_ping', [ $post_id ] );
			newsai_send_indexing_ping_callback( $post_id );
		}
	}
}

if ( ! function_exists( 'newsai_dispatch_indexing_pings_for_post' ) ) {
	/**
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 * @param bool    $update  Whether this is an update.
	 */
	function newsai_dispatch_indexing_pings_for_post( $post_id, $post, $update = true ) {
		if ( ! newsai_should_ping_post_for_indexing( $post_id, $post ) ) {
			return;
		}
		if ( ! newsai_acquire_indexing_ping_lock( $post_id ) ) {
			return;
		}
		$google_on   = ( get_option( 'newsai_indexing_enabled', 'no' ) === 'yes' );
		$indexnow_on = ( get_option( 'newsai_indexnow_enabled', 'no' ) === 'yes' );
		if ( $google_on ) {
			newsai_enqueue_indexing_ping( $post_id );
		}
		if ( $indexnow_on && function_exists( 'newsai_indexnow_queue_submit' ) ) {
			newsai_indexnow_queue_submit( $post_id, (bool) $update );
		}
	}
}

if ( ! function_exists( 'newsai_queue_indexing_ping' ) ) {
	function newsai_queue_indexing_ping( $post_id, $post, $update ) {
		newsai_dispatch_indexing_pings_for_post( $post_id, $post, (bool) $update );
	}
}

if ( ! function_exists( 'newsai_queue_indexing_ping_on_publish' ) ) {
	/**
	 * Fires when a post becomes published (draft→publish etc.) so ping is not missed.
	 *
	 * @param string  $new_status New status.
	 * @param string  $old_status Old status.
	 * @param WP_Post $post       Post.
	 */
	function newsai_queue_indexing_ping_on_publish( $new_status, $old_status, $post ) {
		if ( $new_status !== 'publish' || $old_status === 'publish' ) {
			return;
		}
		if ( ! $post || $post->post_type !== 'post' ) {
			return;
		}
		newsai_dispatch_indexing_pings_for_post( (int) $post->ID, $post, $old_status !== 'publish' );
	}
}

if (!function_exists('newsai_send_indexing_ping_callback')) {
    function newsai_send_indexing_ping_callback($post_id) {
        // Defensive: a pending scheduled event from before the license lapsed should not
        // burn API quota on a now-free site.
        if (function_exists('newsai_is_pro') && !newsai_is_pro()) {
            return;
        }
        if ( get_option( 'newsai_indexing_enabled', 'no' ) !== 'yes' ) {
            return;
        }
        $post = get_post($post_id);
        if (!$post || $post->post_status !== 'publish') {
            return;
        }
        $url = get_permalink($post_id);
        if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
            return;
        }
        $safe_url = esc_url_raw((string) $url);

        $token_error = '';
        $token = newsai_get_service_account_access_token($token_error);
        if ($token === '') {
            // Backward-compatible fallback to manual bearer token.
            $token = trim((string) get_option('newsai_indexing_bearer_token', ''));
        }
        if ($token === '') {
            $err = $token_error !== '' ? sanitize_text_field( $token_error ) : __( 'No Google credentials configured for Indexing API.', 'newsroom-ai-assistant' );
            update_post_meta( $post_id, '_newsai_indexing_last_error', $err );
            update_post_meta( $post_id, '_newsai_indexing_last_code', 0 );
            update_post_meta( $post_id, '_newsai_indexing_last_ping', time() );
            update_option( 'newsai_indexing_last_code', 0, false );
            update_option( 'newsai_indexing_last_error', $err, false );
            update_option( 'newsai_indexing_last_ping', time(), false );
            update_option( 'newsai_indexing_last_url', $safe_url, false );
            return;
        }

        $response = wp_remote_post('https://indexing.googleapis.com/v3/urlNotifications:publish', [
            'timeout' => 8,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type'  => 'application/json; charset=utf-8'
            ],
            'body' => wp_json_encode([
                'url'  => esc_url_raw($url),
                'type' => 'URL_UPDATED'
            ])
        ]);

        if (is_wp_error($response)) {
            $err_msg = sanitize_text_field($response->get_error_message());
            update_post_meta($post_id, '_newsai_indexing_last_error', $err_msg);
            update_option('newsai_indexing_last_code', 0, false);
            update_option('newsai_indexing_last_error', $err_msg, false);
            update_option('newsai_indexing_last_ping', time(), false);
            update_option('newsai_indexing_last_url', $safe_url, false);
            return;
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        update_post_meta($post_id, '_newsai_indexing_last_code', $code);
        update_post_meta($post_id, '_newsai_indexing_last_ping', time());
        update_option('newsai_indexing_last_code', $code, false);
        update_option('newsai_indexing_last_ping', time(), false);
        update_option('newsai_indexing_last_url', $safe_url, false);
        if ($code >= 200 && $code < 300) {
            delete_post_meta($post_id, '_newsai_indexing_last_error');
            delete_option('newsai_indexing_last_error');
            update_option('newsai_indexing_last_success_code', $code, false);
            update_option('newsai_indexing_last_success_ping', time(), false);
            update_option('newsai_indexing_last_success_url', $safe_url, false);
        } else {
            $body = wp_remote_retrieve_body($response);
            if (!empty($body)) {
                $err_body = sanitize_text_field(wp_strip_all_tags((string) $body));
                update_post_meta($post_id, '_newsai_indexing_last_error', $err_body);
                update_option('newsai_indexing_last_error', $err_body, false);
            }
        }
    }
}

// ==========================================
// 9. CONTENT DECAY RADAR — retired (Phase D → GSC traffic-drop alerts)
// ==========================================
if ( ! function_exists( 'newsai_retire_content_decay_radar' ) ) {
	function newsai_retire_content_decay_radar() {
		static $done = false;
		if ( $done ) {
			return;
		}
		$done = true;
		wp_clear_scheduled_hook( 'newsai_content_decay_scan' );
		if ( get_option( 'newsai_decay_radar_enabled', 'no' ) === 'yes' ) {
			update_option( 'newsai_decay_radar_enabled', 'no', false );
		}
		if ( get_option( 'newsai_decay_retired_v1', '' ) !== 'yes' ) {
			if ( get_option( 'newsai_gsc_traffic_drop_enabled', '' ) === '' ) {
				update_option( 'newsai_gsc_traffic_drop_enabled', 'yes', false );
			}
			update_option( 'newsai_decay_retired_v1', 'yes', false );
		}
	}
}
add_action( 'newsai_content_decay_scan', 'newsai_retire_content_decay_radar' );
add_action( 'admin_init', 'newsai_retire_content_decay_radar', 4 );

if ( ! function_exists( 'newsai_schedule_decay_scan' ) ) {
	function newsai_schedule_decay_scan() {
		wp_clear_scheduled_hook( 'newsai_content_decay_scan' );
	}
}

if ( ! function_exists( 'newsai_run_content_decay_scan' ) ) {
	function newsai_run_content_decay_scan() {
		wp_clear_scheduled_hook( 'newsai_content_decay_scan' );
	}
}