<?php
/**
 * Plugin Name:       EasyChat24 Live Chat
 * Plugin URI:        https://easychat24.ru/
 * Description:       Self-hosted live chat for WordPress with optional AI and messenger integrations.
 * Version:           1.14.0
 * Requires at least: 6.0
 * Tested up to:      7.0
 * Requires PHP:      7.4
 * Author:            murzein
 * Author URI:        https://profiles.wordpress.org/murzein/
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       easychat24-live-chat
 * Domain Path:       /languages
 */

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

define('EASYCHAT_WPORG_PLUGIN_FILE', __FILE__);
define('EASYCHAT_WPORG_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('EASYCHAT_WPORG_PLUGIN_URL', plugin_dir_url(__FILE__));
define('EASYCHAT_WPORG_CORE_DIR', EASYCHAT_WPORG_PLUGIN_DIR . 'lib/vendor/easychat-core');
define('EASYCHAT_WPORG_DB_PREFIX', 'easychat_');
define('EASYCHAT_WPORG_REST_NAMESPACE', 'easychat24-live-chat/v1');
define('EASYCHAT_WPORG_ADMIN_SLUG', 'easychat24-live-chat');
define('EASYCHAT_WPORG_OPERATOR_SLUG', 'easychat-login');
define('EASYCHAT_WPORG_CRON_HOOK', 'easychat_wporg_queue_tick');
define('EASYCHAT_WPORG_CRON_SCHEDULE', 'easychat_wporg_every_minute');
define('EASYCHAT_WPORG_CRON_LOCK_KEY', 'easychat_wporg_queue_lock');
define('EASYCHAT_WPORG_REWRITE_MARKER', 'easychat-login-v1');
define('EASYCHAT_WPORG_CREDENTIAL_CLEANUP_MARKER', 'handoff-v1');

function easychat_wporg_load_textdomain() {
    load_plugin_textdomain('easychat24-live-chat', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
add_action('plugins_loaded', 'easychat_wporg_load_textdomain');
define('EASYCHAT_WPORG_CRON_LOCK_TTL', 120);
define('EASYCHAT_WPORG_SUPPORT_EMAIL', 'support@easychat24.ru');

function easychat_wporg_version() {
    if (defined('EASYCHAT_WPORG_VERSION')) {
        return EASYCHAT_WPORG_VERSION;
    }

    $data = get_file_data(EASYCHAT_WPORG_PLUGIN_FILE, array('Version' => 'Version'), 'plugin');
    return isset($data['Version']) && $data['Version'] !== '' ? $data['Version'] : '1.0.0';
}

function easychat_wporg_runtime_upload_base() {
    $upload = wp_upload_dir(null, false);
    if (!is_array($upload)) {
        return array('', 'WordPress uploads lookup failed.');
    }

    if (!empty($upload['error'])) {
        return array('', (string)$upload['error']);
    }

    if (empty($upload['basedir'])) {
        return array('', 'WordPress uploads base directory is empty.');
    }

    $base = rtrim((string)$upload['basedir'], '/\\');
    if ($base === '') {
        return array('', 'WordPress uploads base directory is empty.');
    }

    return array($base, '');
}

function easychat_wporg_runtime_dir() {
    list($base) = easychat_wporg_runtime_upload_base();
    if ($base === '') {
        return '';
    }

    return $base . '/easychat-runtime';
}

function easychat_wporg_record_runtime_error($detail = '') {
    $message = 'EasyChat24 could not access the WordPress uploads directory. Please make sure WordPress uploads are configured and writable, then try again.';
    $detail = trim((string)$detail);
    if ($detail !== '') {
        $message .= ' WordPress reported: ' . sanitize_text_field($detail);
    }

    update_option('easychat_wporg_last_error', $message, false);
}

function easychat_wporg_core_base_url() {
    return trailingslashit(rest_url(EASYCHAT_WPORG_REST_NAMESPACE . '/core/'));
}

function easychat_wporg_public_rest_url($route) {
    $route = ltrim((string)$route, '/');
    return home_url('/index.php?rest_route=/' . EASYCHAT_WPORG_REST_NAMESPACE . '/' . $route);
}

function easychat_wporg_public_rest_query_base($route) {
    return easychat_wporg_public_rest_url($route) . '&';
}

function easychat_wporg_core_admin_url($route) {
    return admin_url('admin.php?page=' . EASYCHAT_WPORG_ADMIN_SLUG . '&easychat_view=core&_wpnonce='
        . rawurlencode(wp_create_nonce('easychat_wporg_core_view'))
        . '&easychat_route=' . rawurlencode((string)$route));
}

function easychat_wporg_core_dispatch_base_url() {
    return admin_url('admin-post.php?action=easychat_wporg_core&_wpnonce='
        . rawurlencode(wp_create_nonce('easychat_wporg_core_view'))
        . '&easychat_route=');
}

function easychat_wporg_operator_login_url($route = '') {
    $url = home_url('/' . EASYCHAT_WPORG_OPERATOR_SLUG . '/');
    $route = trim((string)$route, '/');
    if ($route !== '') {
        $url = add_query_arg('easychat_route', $route, $url);
    }

    return $url;
}

function easychat_wporg_core_admin_entry_url($route) {
    return easychat_wporg_core_admin_url($route);
}

function easychat_wporg_asset_url($asset_ref) {
    $asset_ref = ltrim((string)$asset_ref, '/');
    $fragment = '';
    $fragment_pos = strpos($asset_ref, '#');
    if ($fragment_pos !== false) {
        $fragment = substr($asset_ref, $fragment_pos);
        $asset_ref = substr($asset_ref, 0, $fragment_pos);
    }

    $query = '';
    $query_pos = strpos($asset_ref, '?');
    if ($query_pos !== false) {
        $query = substr($asset_ref, $query_pos + 1);
        $asset_ref = substr($asset_ref, 0, $query_pos);
    }

    if ($query === '') {
        $asset_path = EASYCHAT_WPORG_CORE_DIR . '/' . $asset_ref;
        if (is_file($asset_path)) {
            $query = 'ver=' . substr(hash_file('sha256', $asset_path), 0, 12);
        }
    }

    $url = plugins_url('lib/vendor/easychat-core/' . $asset_ref, __FILE__);
    if ($query !== '') {
        $url .= '?' . $query;
    }

    return $url . $fragment;
}

function easychat_wporg_split_db_host() {
    $host = DB_HOST;
    $port = '3306';

    if (strpos($host, ':') !== false && substr_count($host, ':') === 1) {
        list($host, $port) = explode(':', $host, 2);
    }

    return array($host, $port);
}

function easychat_wporg_require_core() {
    require_once EASYCHAT_WPORG_PLUGIN_DIR . 'lib/vendor/easychat-wporg-compat.php';
    easychat_wporg_bootstrap_defines();

    if (!defined('EASYCHAT_DISABLE_AUTOLOAD_CLASSMAP_WRITE')) {
        define('EASYCHAT_DISABLE_AUTOLOAD_CLASSMAP_WRITE', true);
    }

    require_once EASYCHAT_WPORG_CORE_DIR . '/core/autoload.php';
}

function easychat_wporg_current_admin_locale() {
    return function_exists('get_user_locale') ? get_user_locale() : get_locale();
}

function easychat_wporg_sync_core_locale() {
    if (!is_file(EASYCHAT_WPORG_CORE_DIR . '/core/autoload.php')) {
        return;
    }

    easychat_wporg_require_core();
    list($db_host, $db_port) = easychat_wporg_split_db_host();

    \EasyChat\Library\InstallService::syncCmsLocale(array(
        'host' => $db_host,
        'user' => DB_USER,
        'password' => DB_PASSWORD,
        'database' => DB_NAME,
        'port' => $db_port,
        'prefix' => EASYCHAT_WPORG_DB_PREFIX,
        'admin_locale' => easychat_wporg_current_admin_locale(),
        'widget_locale' => easychat_wporg_current_admin_locale(),
        'locale_source' => 'cms',
    ));
}

function easychat_wporg_core_setting($key) {
    global $wpdb;
    $table = EASYCHAT_WPORG_DB_PREFIX . 'settings';
    $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
    if ($exists !== $table) {
        return null;
    }

    $value = $wpdb->get_var($wpdb->prepare("SELECT `value` FROM `{$table}` WHERE `key` = %s LIMIT 1", $key));
    return $value === null ? null : (string)$value;
}

function easychat_wporg_set_core_setting($key, $value) {
    global $wpdb;
    $table = EASYCHAT_WPORG_DB_PREFIX . 'settings';
    $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
    if ($exists !== $table) {
        return false;
    }

    $wpdb->query($wpdb->prepare(
        "INSERT INTO `{$table}` (`key`, `value`) VALUES (%s, %s) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",
        $key,
        (string)$value
    ));
    return true;
}

function easychat_wporg_sync_widget_visibility_from_option($force = false) {
    $legacy = get_option('easychat_wporg_widget_enabled', '1') === '1' ? '1' : '0';
    $current = easychat_wporg_core_setting('chat_status');
    $migrated = easychat_wporg_core_setting('cms_widget_visibility_migrated') === '1';
    if ($force) {
        easychat_wporg_set_core_setting('chat_status', $legacy);
        easychat_wporg_set_core_setting('cms_widget_visibility_migrated', '1');
        return $legacy;
    }

    if (!$migrated) {
        if ($legacy === '0' || $current === null) {
            easychat_wporg_set_core_setting('chat_status', $legacy);
            $current = $legacy;
        }
        easychat_wporg_set_core_setting('cms_widget_visibility_migrated', '1');
    } elseif ($current === null) {
        easychat_wporg_set_core_setting('chat_status', $legacy);
        $current = $legacy;
    }

    update_option('easychat_wporg_widget_enabled', $current === '1' ? '1' : '0', false);
    return $current === '1' ? '1' : '0';
}

function easychat_wporg_sync_cms_integration() {
    global $wpdb;
    easychat_wporg_set_core_setting('chat_cms_type', 'wordpress');
    easychat_wporg_set_core_setting('chat_cms_prefix', isset($wpdb->prefix) ? (string)$wpdb->prefix : '');
}

function easychat_wporg_widget_enabled() {
    $status = easychat_wporg_core_setting('chat_status');
    if ($status === null) {
        return get_option('easychat_wporg_widget_enabled', '1') === '1';
    }

    return $status === '1';
}

function easychat_wporg_ensure_runtime_dirs() {
    list($base, $error) = easychat_wporg_runtime_upload_base();
    if ($base === '') {
        easychat_wporg_record_runtime_error($error);
        return false;
    }

    $runtime_dir = $base . '/easychat-runtime';
    $dirs = array(
        $runtime_dir,
        $runtime_dir . '/log',
        $runtime_dir . '/uploads',
        $runtime_dir . '/avatars',
        $runtime_dir . '/sessions',
    );

    foreach ($dirs as $dir) {
        if (!is_dir($dir) && !wp_mkdir_p($dir)) {
            easychat_wporg_record_runtime_error('Could not create ' . $dir . '.');
            return false;
        }
        if (!is_dir($dir)) {
            easychat_wporg_record_runtime_error('Could not create ' . $dir . '.');
            return false;
        }
    }

    // Runtime files are served only through EasyChat controllers with token
    // checks and nosniff headers. Direct web access to uploads/logs/sessions
    // must stay closed even when the runtime directory lives under wp-content.
    $deny = "Options -Indexes\n<IfModule mod_authz_core.c>\n    Require all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\n    Deny from all\n</IfModule>\n";
    foreach ($dirs as $dir) {
        if (is_dir($dir)) {
            $htaccess = $dir . '/.htaccess';
            if (!is_file($htaccess) || (string)@file_get_contents($htaccess) !== $deny) {
                @file_put_contents($htaccess, $deny);
            }
            $index = $dir . '/index.html';
            if (!is_file($index)) {
                @file_put_contents($index, '');
            }
        }
    }

    return true;
}

function easychat_wporg_install_core() {
    global $wpdb;

    easychat_wporg_remove_legacy_admin_credentials();

    if (!is_file(EASYCHAT_WPORG_CORE_DIR . '/library/install_service.php')) {
        update_option('easychat_wporg_last_error', 'EasyChat core library is missing.');
        return false;
    }

    if (get_option('easychat_wporg_installed') === '1') {
        easychat_wporg_sync_core_locale();
        easychat_wporg_sync_widget_visibility_from_option(false);
        easychat_wporg_sync_cms_integration();
        return easychat_wporg_ensure_runtime_dirs();
    }

    easychat_wporg_require_core();

    $runtime_dir = easychat_wporg_runtime_dir();
    if ($runtime_dir === '' || !easychat_wporg_ensure_runtime_dirs()) {
        update_option('easychat_wporg_installed', '0');
        return false;
    }

    $password = wp_generate_password(24, true, true);
    update_option('easychat_wporg_admin_login', 'admin', false);

    list($db_host, $db_port) = easychat_wporg_split_db_host();

    $result = \EasyChat\Library\InstallService::install(array(
        'host' => $db_host,
        'user' => DB_USER,
        'password' => DB_PASSWORD,
        'database' => DB_NAME,
        'port' => $db_port,
        'prefix' => EASYCHAT_WPORG_DB_PREFIX,
        'admin_login' => 'admin',
        'admin_password' => $password,
        'app_path' => EASYCHAT_WPORG_CORE_DIR,
        'runtime_path' => $runtime_dir,
        'write_config' => '0',
        'site_url' => home_url('/'),
        'edition' => 'wordpress_org',
        'cms_type' => 'wordpress',
        'cms_prefix' => isset($wpdb->prefix) ? (string)$wpdb->prefix : '',
        'admin_email' => '',
        'claim_easytokens_gift' => '0',
        'version' => easychat_wporg_version(),
        'admin_locale' => easychat_wporg_current_admin_locale(),
        'widget_locale' => easychat_wporg_current_admin_locale(),
        'locale_source' => 'cms',
    ));

    if (empty($result['success'])) {
        update_option('easychat_wporg_installed', '0');
        update_option('easychat_wporg_last_error', isset($result['error']) ? $result['error'] : 'Unknown install error.');
        return false;
    }

    update_option('easychat_wporg_installed', '1');
    update_option('easychat_wporg_installed_version', easychat_wporg_version(), false);
    update_option('easychat_wporg_rewrite_marker', EASYCHAT_WPORG_REWRITE_MARKER, false);
    update_option('easychat_wporg_widget_enabled', get_option('easychat_wporg_widget_enabled', '1') === '1' ? '1' : '0', false);
    easychat_wporg_sync_widget_visibility_from_option(false);
    easychat_wporg_sync_cms_integration();
    update_option('easychat_wporg_gift_state', 'new');
    delete_option('easychat_wporg_last_error');
    easychat_wporg_ensure_runtime_dirs();

    return true;
}

function easychat_wporg_remove_legacy_admin_credentials() {
    if ((string)get_option('easychat_wporg_credential_cleanup', '') === EASYCHAT_WPORG_CREDENTIAL_CLEANUP_MARKER) {
        return true;
    }

    // Старые версии сохраняли начальный пароль администратора в wp_options.
    // Server-side handoff использует manager password_hash и не нуждается в
    // plaintext. Marker исключает два DELETE на каждом proxied admin request.
    delete_option('easychat_wporg_admin_password');
    delete_option('easychat_wporg_admin_initial_password');
    if (get_option('easychat_wporg_admin_password', null) !== null
        || get_option('easychat_wporg_admin_initial_password', null) !== null
    ) {
        return false;
    }

    update_option('easychat_wporg_credential_cleanup', EASYCHAT_WPORG_CREDENTIAL_CLEANUP_MARKER, false);
    return true;
}

function easychat_wporg_cron_schedules($schedules) {
    if (!isset($schedules[EASYCHAT_WPORG_CRON_SCHEDULE])) {
        $schedules[EASYCHAT_WPORG_CRON_SCHEDULE] = array(
            'interval' => 60,
            'display' => __('Every minute (EasyChat24)', 'easychat24-live-chat'),
        );
    }

    return $schedules;
}
add_filter('cron_schedules', 'easychat_wporg_cron_schedules');

function easychat_wporg_schedule_cron() {
    if (wp_next_scheduled(EASYCHAT_WPORG_CRON_HOOK)) {
        return;
    }

    wp_schedule_event(time() + 60, EASYCHAT_WPORG_CRON_SCHEDULE, EASYCHAT_WPORG_CRON_HOOK);
}

function easychat_wporg_clear_cron() {
    wp_clear_scheduled_hook(EASYCHAT_WPORG_CRON_HOOK);
}

function easychat_wporg_maybe_schedule_cron() {
    if (get_option('easychat_wporg_installed') !== '1') {
        return;
    }

    easychat_wporg_schedule_cron();
}
add_action('init', 'easychat_wporg_maybe_schedule_cron');

function easychat_wporg_register_operator_rewrite() {
    add_rewrite_rule(
        '^' . preg_quote(EASYCHAT_WPORG_OPERATOR_SLUG, '/') . '/?$',
        'index.php?easychat_wporg_operator=1',
        'top'
    );
}
add_action('init', 'easychat_wporg_register_operator_rewrite');

function easychat_wporg_query_vars($vars) {
    $vars[] = 'easychat_wporg_operator';
    return $vars;
}
add_filter('query_vars', 'easychat_wporg_query_vars');

function easychat_wporg_activate() {
    easychat_wporg_register_operator_rewrite();
    easychat_wporg_install_core();
    easychat_wporg_schedule_cron();
    update_option('easychat_wporg_installed_version', easychat_wporg_version(), false);
    update_option('easychat_wporg_rewrite_marker', EASYCHAT_WPORG_REWRITE_MARKER, false);
    flush_rewrite_rules();
}
register_activation_hook(__FILE__, 'easychat_wporg_activate');

function easychat_wporg_deactivate() {
    easychat_wporg_clear_cron();
    delete_transient(EASYCHAT_WPORG_CRON_LOCK_KEY);
    flush_rewrite_rules();
}
register_deactivation_hook(__FILE__, 'easychat_wporg_deactivate');

function easychat_wporg_maybe_upgrade() {
    if (!is_admin() || !current_user_can('manage_options')) {
        return;
    }

    if (get_option('easychat_wporg_installed') !== '1') {
        return;
    }

    $current_version = easychat_wporg_version();
    $installed_version = (string)get_option('easychat_wporg_installed_version', '');
    $rewrite_marker = (string)get_option('easychat_wporg_rewrite_marker', '');
    if ($installed_version === $current_version && $rewrite_marker === EASYCHAT_WPORG_REWRITE_MARKER) {
        return;
    }

    easychat_wporg_sync_core_locale();
    easychat_wporg_ensure_runtime_dirs();
    easychat_wporg_schedule_cron();

    if ($rewrite_marker !== EASYCHAT_WPORG_REWRITE_MARKER) {
        easychat_wporg_register_operator_rewrite();
        flush_rewrite_rules();
        update_option('easychat_wporg_rewrite_marker', EASYCHAT_WPORG_REWRITE_MARKER, false);
    }

    if (!easychat_wporg_build_core_registry()) {
        return;
    }

    update_option('easychat_wporg_installed_version', $current_version, false);
    update_option('easychat_wporg_upgrade_notice', '1', false);
    delete_option('easychat_wporg_last_error');
}
add_action('admin_init', 'easychat_wporg_maybe_upgrade', 20);

function easychat_wporg_admin_notices() {
    if (!current_user_can('manage_options')) {
        return;
    }

    $error = (string)get_option('easychat_wporg_last_error', '');
    if ($error !== '') {
        echo '<div class="notice notice-error"><p>' . esc_html($error) . '</p></div>';
        return;
    }

    if (get_option('easychat_wporg_upgrade_notice') === '1') {
        delete_option('easychat_wporg_upgrade_notice');
        echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('EasyChat24 was updated. Database migrations, background tasks and runtime checks completed.', 'easychat24-live-chat') . '</p></div>';
    }
}
add_action('admin_notices', 'easychat_wporg_admin_notices');

function easychat_wporg_admin_menu() {
    add_menu_page(
        'EasyChat24',
        'EasyChat24',
        'manage_options',
        EASYCHAT_WPORG_ADMIN_SLUG,
        'easychat_wporg_admin_page',
        'dashicons-format-chat',
        58
    );
    add_submenu_page(
        EASYCHAT_WPORG_ADMIN_SLUG,
        'EasyChat24 Settings',
        'Settings',
        'manage_options',
        EASYCHAT_WPORG_ADMIN_SLUG . '-settings',
        'easychat_wporg_admin_page'
    );
}
add_action('admin_menu', 'easychat_wporg_admin_menu');

function easychat_wporg_admin_assets($hook_suffix) {
    if ($hook_suffix !== 'toplevel_page_' . EASYCHAT_WPORG_ADMIN_SLUG) {
        return;
    }

    wp_register_style('easychat-wporg-admin', false, array(), easychat_wporg_version());
    wp_enqueue_style('easychat-wporg-admin');
    wp_add_inline_style('easychat-wporg-admin', '
        .easychat-wporg-queue-card {
            max-width: 960px;
            margin-top: 16px;
            padding: 18px;
            background: #fff;
            border: 1px solid #dcdcde;
            box-shadow: 0 1px 1px rgba(0,0,0,.04);
        }
        .easychat-wporg-queue-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
            gap: 12px;
            margin: 14px 0;
        }
        .easychat-wporg-queue-stat {
            padding: 12px;
            border: 1px solid #dcdcde;
            background: #f6f7f7;
        }
        .easychat-wporg-queue-stat strong {
            display: block;
            margin-top: 4px;
            font-size: 16px;
        }
    ');
}
add_action('admin_enqueue_scripts', 'easychat_wporg_admin_assets');

function easychat_wporg_maybe_redirect_admin_entry() {
    if (!is_admin()) {
        return;
    }

    $page = filter_input(INPUT_GET, 'page', FILTER_UNSAFE_RAW);
    $page = is_string($page) ? sanitize_key($page) : '';
    if ($page !== EASYCHAT_WPORG_ADMIN_SLUG) {
        return;
    }

    $easychat_view = filter_input(INPUT_GET, 'easychat_view', FILTER_UNSAFE_RAW);
    $easychat_view = is_string($easychat_view) ? sanitize_key($easychat_view) : '';
    if ($easychat_view !== '') {
        return;
    }

    $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper((string)$_SERVER['REQUEST_METHOD']) : 'GET';
    if ($method !== 'GET') {
        return;
    }

    if (!current_user_can('manage_options')) {
        return;
    }

    wp_safe_redirect(easychat_wporg_core_admin_url('index'));
    exit;
}
add_action('admin_init', 'easychat_wporg_maybe_redirect_admin_entry', 1);

function easychat_wporg_maybe_dispatch_core_admin_view() {
    if (!is_admin()) {
        return;
    }

    $page = filter_input(INPUT_GET, 'page', FILTER_UNSAFE_RAW);
    $page = is_string($page) ? sanitize_key($page) : '';
    $easychat_view = filter_input(INPUT_GET, 'easychat_view', FILTER_UNSAFE_RAW);
    $easychat_view = is_string($easychat_view) ? sanitize_key($easychat_view) : '';
    if ($page !== EASYCHAT_WPORG_ADMIN_SLUG || $easychat_view !== 'core') {
        return;
    }

    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('You do not have permission to open EasyChat24 admin.', 'easychat24-live-chat'));
    }

    $nonce = filter_input(INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW);
    $nonce = is_string($nonce) ? sanitize_text_field($nonce) : '';
    if (!$nonce || !wp_verify_nonce($nonce, 'easychat_wporg_core_view')) {
        wp_die(esc_html__('EasyChat24 admin link expired. Please open EasyChat24 again from the WordPress menu.', 'easychat24-live-chat'));
    }

    $route = filter_input(INPUT_GET, 'easychat_route', FILTER_UNSAFE_RAW);
    $route = is_string($route) && $route !== '' ? sanitize_text_field($route) : 'index';

    // Ранний direct-dispatch важен для shared hosting: обычный wp-admin chrome
    // может подтянуть десятки сторонних JS-файлов до EasyChat и выбить rate-limit.
    // Здесь WordPress уже проверил администратора и nonce, а EasyChat получает
    // полноценную server-side сессию без пароля в URL/DOM/postMessage.
    easychat_wporg_dispatch_core_admin($route);
}
add_action('admin_init', 'easychat_wporg_maybe_dispatch_core_admin_view', 1);

function easychat_wporg_admin_page() {
    if (!current_user_can('manage_options')) {
        return;
    }

    $easychat_view = filter_input(INPUT_GET, 'easychat_view', FILTER_UNSAFE_RAW);
    $easychat_view = is_string($easychat_view) ? sanitize_key($easychat_view) : '';
    if ($easychat_view === 'core') {
        wp_die(esc_html__('EasyChat24 direct admin dispatcher is unavailable. Please open EasyChat24 again from the WordPress menu.', 'easychat24-live-chat'));
    }

    if ($easychat_view !== '' && $easychat_view !== 'settings') {
        wp_die(esc_html__('Unknown EasyChat24 admin view.', 'easychat24-live-chat'));
    }

    easychat_wporg_handle_admin_actions();

    $installed = easychat_wporg_install_core();
    $enabled = easychat_wporg_widget_enabled();
    $gift_state = (string)get_option('easychat_wporg_gift_state', 'new');
    $error = (string)get_option('easychat_wporg_last_error', '');
    if (easychat_wporg_is_legacy_gift_error($error)) {
        $error = '';
    }
    ?>
    <div class="wrap easychat-wporg-wrap">
        <h1><?php esc_html_e('EasyChat24 Live Chat', 'easychat24-live-chat'); ?></h1>
        <p><?php esc_html_e('Self-hosted live chat for WordPress. External AI and messenger services are optional and require explicit setup.', 'easychat24-live-chat'); ?></p>

        <?php if ($error !== '') : ?>
            <div class="notice notice-error inline"><p><?php echo esc_html($error); ?></p></div>
        <?php endif; ?>

        <?php easychat_wporg_render_gift_card($gift_state); ?>
        <?php easychat_wporg_render_queue_card(); ?>

        <form method="post" style="margin-top: 20px;">
            <?php wp_nonce_field('easychat_wporg_save'); ?>
            <input type="hidden" name="easychat_wporg_action" value="save">
            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row"><?php esc_html_e('Widget loader', 'easychat24-live-chat'); ?></th>
                    <td>
                        <label>
                            <input type="checkbox" name="easychat_wporg_widget_enabled" value="1" <?php checked($enabled); ?>>
                            <?php esc_html_e('Enable EasyChat24 widget on the public site', 'easychat24-live-chat'); ?>
                        </label>
                    </td>
                </tr>
                <tr>
                    <th scope="row"><?php esc_html_e('Operator admin', 'easychat24-live-chat'); ?></th>
                    <td>
                        <a class="button button-primary" href="<?php echo esc_url(easychat_wporg_core_admin_entry_url('index')); ?>"><?php esc_html_e('Open EasyChat24 admin', 'easychat24-live-chat'); ?></a>
                        <p class="description"><?php esc_html_e('Use this WordPress-only entry for the site administrator.', 'easychat24-live-chat'); ?></p>
                    </td>
                </tr>
                <tr>
                    <th scope="row"><?php esc_html_e('Operator login link', 'easychat24-live-chat'); ?></th>
                    <td>
                        <input
                            type="text"
                            class="regular-text code"
                            readonly
                            value="<?php echo esc_attr(easychat_wporg_operator_login_url()); ?>"
                            onclick="this.select();"
                        >
                        <a class="button" href="<?php echo esc_url(easychat_wporg_operator_login_url()); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Open operator login', 'easychat24-live-chat'); ?></a>
                        <p class="description"><?php esc_html_e('Send this link to EasyChat24 managers and operators. They sign in with EasyChat24 accounts and do not need WordPress access.', 'easychat24-live-chat'); ?></p>
                    </td>
                </tr>
                <tr>
                    <th scope="row"><?php esc_html_e('Runtime path', 'easychat24-live-chat'); ?></th>
                    <td><code><?php echo esc_html(easychat_wporg_runtime_dir()); ?></code></td>
                </tr>
            </table>
            <p><button class="button" type="submit"><?php esc_html_e('Save settings', 'easychat24-live-chat'); ?></button></p>
        </form>
    </div>
    <?php
}

function easychat_wporg_is_legacy_gift_error($error) {
    $error = trim((string)$error);
    return in_array($error, array(
        'Unable to connect to the WordPress database for EasyTokens gift request.',
        'EasyTokens gift request was not completed. You can try again later.',
    ), true);
}

function easychat_wporg_gift_email_value() {
    $stored = (string)get_option('easychat_wporg_gift_email', '');
    if (is_email($stored)) {
        return $stored;
    }

    $admin_email = (string)get_option('admin_email', '');
    return is_email($admin_email) ? $admin_email : '';
}

function easychat_wporg_gift_notice($gift_state) {
    $message = (string)get_option('easychat_wporg_gift_message', '');
    $code = (string)get_option('easychat_wporg_gift_error_code', '');
    if ($message === '') {
        if ($gift_state === 'email_required') {
            $message = 'Enter a valid email address to receive the EasyTokens gift.';
        } elseif ($gift_state === 'pending_review') {
            $message = 'The gift request was sent for manual review. This usually takes up to 1 business day.';
        } elseif ($gift_state === 'requested') {
            $message = 'The gift request is being processed. If it takes more than 15 minutes, a support contact button will appear here.';
        } elseif ($gift_state === 'already_issued') {
            $message = 'The EasyTokens gift has already been issued for this domain.';
        } elseif ($gift_state === 'retryable') {
            $message = 'EasyTokens gift request was not completed. You can try again later.';
        } elseif ($gift_state === 'rejected') {
            $message = 'EasyTokens gift request was rejected.';
        } elseif ($gift_state === 'failed') {
            $message = 'EasyTokens gift request was not completed. You can try again later.';
        }
    }

    if ($message === '') {
        return;
    }

    if ($gift_state === 'requested') {
        $class = 'notice-info';
    } elseif (in_array($gift_state, array('pending_review', 'already_issued'), true)) {
        $class = 'notice-warning';
    } else {
        $class = 'notice-error';
    }
    ?>
    <div class="notice <?php echo esc_attr($class); ?> inline" id="easychat-wporg-gift-notice">
        <p>
            <?php echo esc_html($message); ?>
            <?php if ($code !== '' && $gift_state !== 'email_required') : ?>
                <span class="description">Code: <?php echo esc_html($code); ?></span>
            <?php endif; ?>
        </p>
    </div>
    <?php
}

function easychat_wporg_gift_support_mailto_url($gift_state) {
    $email = (string)get_option('easychat_wporg_gift_email', '');
    $code = (string)get_option('easychat_wporg_gift_error_code', '');
    $requested_at = (int)get_option('easychat_wporg_gift_requested_at', 0);
    $updated_at = (int)get_option('easychat_wporg_gift_status_updated_at', 0);
    $site_url = home_url('/');
    $host = (string)parse_url($site_url, PHP_URL_HOST);
    if ($host === '') {
        $host = $site_url;
    }

    $body = array(
        'Hello EasyChat24 support,',
        '',
        'Please check my EasyTokens gift request.',
        '',
        'Site: ' . $site_url,
        'Domain: ' . $host,
        'Plugin version: ' . easychat_wporg_version(),
        'Gift status: ' . (string)$gift_state,
        'Gift code: ' . ($code !== '' ? $code : 'none'),
        'Gift email: ' . (is_email($email) ? $email : 'not provided'),
        'Requested at: ' . easychat_wporg_format_admin_time($requested_at),
        'Last status update: ' . easychat_wporg_format_admin_time($updated_at),
    );

    $body[] = '';
    $body[] = 'No chat messages, customer data, API tokens, passwords or installation secrets are included in this email.';

    $query = http_build_query(array(
        'subject' => 'EasyTokens gift request review: ' . $host,
        'body' => implode("\n", $body),
    ), '', '&', PHP_QUERY_RFC3986);

    return 'mailto:' . EASYCHAT_WPORG_SUPPORT_EMAIL . '?' . $query;
}

function easychat_wporg_should_show_gift_support($gift_state) {
    if (in_array($gift_state, array('pending_review', 'retryable'), true)) {
        return true;
    }

    if ($gift_state !== 'requested') {
        return false;
    }

    $requested_at = (int)get_option('easychat_wporg_gift_requested_at', 0);
    return $requested_at > 0 && time() - $requested_at >= 15 * 60;
}

function easychat_wporg_render_gift_support($gift_state) {
    if (!easychat_wporg_should_show_gift_support($gift_state)) {
        return;
    }
    ?>
    <div class="notice notice-info inline" style="margin-top: 14px;">
        <p>
            <strong><?php esc_html_e('Need help with the EasyTokens gift?', 'easychat24-live-chat'); ?></strong>
            <?php esc_html_e('Contact EasyChat24 support with safe request details. The email will open in your mail client before sending.', 'easychat24-live-chat'); ?>
        </p>
        <p>
            <a class="button button-secondary" href="<?php echo esc_url(easychat_wporg_gift_support_mailto_url($gift_state)); ?>"><?php esc_html_e('Contact support', 'easychat24-live-chat'); ?></a>
            <span class="description"><?php esc_html_e('No passwords, API tokens, installation secrets, chat messages or customer data are included.', 'easychat24-live-chat'); ?></span>
        </p>
    </div>
    <?php
}

function easychat_wporg_render_gift_card($gift_state) {
    if ($gift_state === 'issued') {
        echo '<div class="notice notice-success inline"><p><strong>' . esc_html__('EasyTokens gift tokens received.', 'easychat24-live-chat') . '</strong></p></div>';
        return;
    }

    if ($gift_state === 'dismissed') {
        return;
    }

    $email_value = easychat_wporg_gift_email_value();
    $email_invalid = $gift_state === 'email_required';
    $show_email_field = $email_invalid || $email_value === '';
    ?>
    <div class="card" style="max-width: 760px; margin-top: 16px;">
        <h2><?php esc_html_e('Get 30,000 EasyTokens AI tokens for free?', 'easychat24-live-chat'); ?></h2>
        <?php easychat_wporg_gift_notice($gift_state); ?>
        <p>
            <?php esc_html_e('If you agree, EasyChat24 will send the WordPress administrator email, site domain, installation ID, and a technical signature to EasyChat24/EasyTokens servers. Chat messages and attachments are not sent.', 'easychat24-live-chat'); ?>
        </p>
        <p>
            <a href="https://easychat24.ru/legal/privacy/" target="_blank" rel="noopener noreferrer"><?php esc_html_e('What data is sent?', 'easychat24-live-chat'); ?></a>
        </p>
        <form method="post" style="margin-top: 14px;">
            <?php wp_nonce_field('easychat_wporg_gift'); ?>
            <input type="hidden" name="easychat_wporg_action" value="claim_gift">
            <?php if (!$show_email_field) : ?>
                <input type="hidden" name="easychat_wporg_gift_email" value="<?php echo esc_attr($email_value); ?>">
            <?php endif; ?>
            <?php if ($show_email_field) : ?>
            <p>
                <label for="easychat-wporg-gift-email"><strong><?php esc_html_e('Email for gift tokens', 'easychat24-live-chat'); ?></strong></label><br>
                <input
                    type="email"
                    class="regular-text<?php echo $email_invalid ? ' form-invalid' : ''; ?>"
                    id="easychat-wporg-gift-email"
                    name="easychat_wporg_gift_email"
                    value="<?php echo esc_attr($email_value); ?>"
                    required
                    aria-describedby="easychat-wporg-gift-email-help<?php echo $email_invalid ? ' easychat-wporg-gift-notice' : ''; ?>"
                >
                <span class="description" id="easychat-wporg-gift-email-help"><?php esc_html_e('We will use this email only for the EasyTokens gift request.', 'easychat24-live-chat'); ?></span>
            </p>
            <?php endif; ?>
            <p>
                <button class="button button-primary" type="submit"><?php esc_html_e('Get tokens', 'easychat24-live-chat'); ?></button>
                <button class="button" type="submit" name="easychat_wporg_action" value="dismiss_gift" formnovalidate><?php esc_html_e('Not now', 'easychat24-live-chat'); ?></button>
            </p>
        </form>
        <?php easychat_wporg_render_gift_support($gift_state); ?>
    </div>
    <?php
}

function easychat_wporg_format_admin_time($timestamp) {
    $timestamp = (int)$timestamp;
    if ($timestamp <= 0) {
        return 'Never';
    }

    return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $timestamp);
}

function easychat_wporg_queue_counts() {
    $counts = array(
        'pending' => 0,
        'processing' => 0,
        'failed' => 0,
    );

    global $wpdb;
    $table = EASYCHAT_WPORG_DB_PREFIX . 'queue';
    if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
        return $counts;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Технический status read для локальных таблиц EasyChat без пользовательского ввода.
    $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
    if ($exists !== $table) {
        return $counts;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Имя таблицы фиксированное и прошло whitelist выше.
    $rows = $wpdb->get_results('SELECT status, COUNT(*) AS total FROM `' . esc_sql($table) . '` GROUP BY status', ARRAY_A);
    if (!is_array($rows)) {
        return $counts;
    }

    foreach ($rows as $row) {
        $status = isset($row['status']) ? (string)$row['status'] : '';
        if (isset($counts[$status])) {
            $counts[$status] = (int)$row['total'];
        }
    }

    return $counts;
}

function easychat_wporg_render_queue_card() {
    $next_run = wp_next_scheduled(EASYCHAT_WPORG_CRON_HOOK);
    $last_run = (int)get_option('easychat_wporg_cron_last_run_at', 0);
    $last_success = (int)get_option('easychat_wporg_cron_last_success_at', 0);
    $last_error = (string)get_option('easychat_wporg_cron_last_error', '');
    $last_result = get_option('easychat_wporg_cron_last_result', array());
    $lock_active = get_transient(EASYCHAT_WPORG_CRON_LOCK_KEY) !== false;
    $wp_cron_disabled = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;
    $counts = easychat_wporg_queue_counts();
    $notice = (string)get_option('easychat_wporg_queue_notice', '');
    if ($notice !== '') {
        delete_option('easychat_wporg_queue_notice');
    }

    $status = 'Scheduled';
    if ($lock_active) {
        $status = 'Running';
    } elseif (!$next_run) {
        $status = 'Schedule missing';
    } elseif ($last_error !== '') {
        $status = 'Last run failed';
    } elseif ($wp_cron_disabled) {
        $status = 'External cron required';
    }

    $processed = is_array($last_result) && isset($last_result['processed']) ? (int)$last_result['processed'] : 0;
    $failed = is_array($last_result) && isset($last_result['failed']) ? (int)$last_result['failed'] : 0;
    $reset = is_array($last_result) && isset($last_result['reset']) ? (int)$last_result['reset'] : 0;
    $rescheduled = is_array($last_result) && isset($last_result['rescheduled']) ? (int)$last_result['rescheduled'] : 0;
    ?>
    <div class="easychat-wporg-queue-card">
        <h2 style="margin-top: 0;"><?php esc_html_e('Background tasks', 'easychat24-live-chat'); ?></h2>
        <p><?php esc_html_e('EasyChat24 uses WP-Cron to process AI, messenger and indexing jobs. Operators do not need a separate public cron URL.', 'easychat24-live-chat'); ?></p>

        <?php if ($notice !== '') : ?>
            <div class="notice notice-success inline"><p><?php echo esc_html($notice); ?></p></div>
        <?php endif; ?>

        <?php if ($wp_cron_disabled) : ?>
            <div class="notice notice-warning inline">
                <p><strong><?php esc_html_e('WP-Cron is disabled.', 'easychat24-live-chat'); ?></strong> <?php esc_html_e('Configure a system cron call every minute:', 'easychat24-live-chat'); ?> <code><?php echo esc_html(site_url('wp-cron.php?doing_wp_cron')); ?></code></p>
            </div>
        <?php endif; ?>

        <div class="easychat-wporg-queue-grid">
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Status', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html($status); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Last run', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html(easychat_wporg_format_admin_time($last_run)); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Last success', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html(easychat_wporg_format_admin_time($last_success)); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Next scheduled run', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html($next_run ? easychat_wporg_format_admin_time($next_run) : __('Not scheduled', 'easychat24-live-chat')); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Pending', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html((string)$counts['pending']); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Processing', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html((string)$counts['processing']); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Failed', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html((string)$counts['failed']); ?></strong>
            </div>
            <div class="easychat-wporg-queue-stat">
                <?php esc_html_e('Last result', 'easychat24-live-chat'); ?>
                <strong><?php echo esc_html(sprintf(__('done %1$d, failed %2$d, reset %3$d, delayed %4$d', 'easychat24-live-chat'), $processed, $failed, $reset, $rescheduled)); ?></strong>
            </div>
        </div>

        <?php if ($last_error !== '') : ?>
            <div class="notice notice-error inline"><p><?php echo esc_html($last_error); ?></p></div>
        <?php endif; ?>

        <p>
            <input class="regular-text code" type="text" readonly onclick="this.select();" value="<?php echo esc_attr('* * * * * curl -s "' . site_url('wp-cron.php?doing_wp_cron') . '" > /dev/null 2>&1'); ?>">
        </p>

        <form method="post">
            <?php wp_nonce_field('easychat_wporg_queue'); ?>
            <input type="hidden" name="easychat_wporg_action" value="run_queue">
            <button class="button button-secondary" type="submit"><?php esc_html_e('Run background tasks now', 'easychat24-live-chat'); ?></button>
        </form>
    </div>
    <?php
}

function easychat_wporg_handle_admin_actions() {
    if (empty($_POST['easychat_wporg_action'])) {
        return;
    }

    $action = sanitize_text_field(wp_unslash($_POST['easychat_wporg_action']));

    if ($action === 'run_queue') {
        if (!current_user_can('manage_options')) {
            wp_die(
                esc_html__('You do not have permission to run EasyChat24 background tasks.', 'easychat24-live-chat'),
                esc_html__('EasyChat24 access denied', 'easychat24-live-chat'),
                array('response' => 403)
            );
        }

        check_admin_referer('easychat_wporg_queue');
        $result = easychat_wporg_run_queue_tick();
        $message = empty($result['error'])
            ? __('Background tasks ran successfully.', 'easychat24-live-chat')
            : __('Background tasks finished with an error. Check the status block below.', 'easychat24-live-chat');
        update_option('easychat_wporg_queue_notice', $message, false);
        wp_safe_redirect(admin_url('admin.php?page=' . EASYCHAT_WPORG_ADMIN_SLUG . '-settings&easychat_queue_ran=1'));
        exit;
    }

    if ($action === 'save') {
        check_admin_referer('easychat_wporg_save');
        update_option('easychat_wporg_widget_enabled', isset($_POST['easychat_wporg_widget_enabled']) ? '1' : '0');
        easychat_wporg_sync_widget_visibility_from_option(true);
        delete_option('easychat_wporg_admin_initial_password');
        return;
    }

    if ($action === 'dismiss_gift') {
        check_admin_referer('easychat_wporg_gift');
        update_option('easychat_wporg_gift_state', 'dismissed');
        delete_option('easychat_wporg_gift_error_code');
        delete_option('easychat_wporg_gift_message');
        delete_option('easychat_wporg_gift_requested_at');
        delete_option('easychat_wporg_gift_status_updated_at');
        return;
    }

    if ($action === 'claim_gift') {
        check_admin_referer('easychat_wporg_gift');
        easychat_wporg_claim_gift();
    }
}

function easychat_wporg_claim_gift() {
    if (!current_user_can('manage_options')) {
        return;
    }

    $posted_email = isset($_POST['easychat_wporg_gift_email']) ? sanitize_email(wp_unslash($_POST['easychat_wporg_gift_email'])) : '';
    if (!is_email($posted_email)) {
        update_option('easychat_wporg_gift_state', 'email_required');
        update_option('easychat_wporg_gift_email', $posted_email, false);
        update_option('easychat_wporg_gift_error_code', 'email_required', false);
        update_option('easychat_wporg_gift_message', 'Enter a valid email address to receive the EasyTokens gift.', false);
        update_option('easychat_wporg_gift_status_updated_at', time(), false);
        return;
    }

    update_option('easychat_wporg_gift_email', $posted_email, false);
    easychat_wporg_install_core();
    easychat_wporg_require_core();

    global $wpdb;
    $conn = isset($wpdb->dbh) && $wpdb->dbh instanceof mysqli ? $wpdb->dbh : null;
    if (!$conn) {
        update_option('easychat_wporg_gift_state', 'retryable');
        update_option('easychat_wporg_gift_error_code', 'database_unavailable', false);
        update_option('easychat_wporg_gift_message', 'Unable to connect to the WordPress database for EasyTokens gift request.', false);
        update_option('easychat_wporg_gift_status_updated_at', time(), false);
        return;
    }

    $conn->set_charset('utf8mb4');
    update_option('easychat_wporg_gift_state', 'requested');
    update_option('easychat_wporg_gift_requested_at', time(), false);
    update_option('easychat_wporg_gift_status_updated_at', time(), false);

    $result = \EasyChat\Library\EasyTokensGiftProvisioner::provisionWithMysqli($conn, EASYCHAT_WPORG_DB_PREFIX, array(
        'claim_easytokens_gift' => '1',
        'admin_email' => $posted_email,
        'edition' => 'wordpress_org',
        'version' => easychat_wporg_version(),
        'site_url' => home_url('/'),
    ));

    if (!empty($result['success']) && isset($result['status']) && $result['status'] === 'issued') {
        update_option('easychat_wporg_gift_state', 'issued');
        delete_option('easychat_wporg_gift_error_code');
        delete_option('easychat_wporg_gift_message');
        delete_option('easychat_wporg_gift_requested_at');
        update_option('easychat_wporg_gift_status_updated_at', time(), false);
        return;
    }

    $status = isset($result['status']) ? (string)$result['status'] : 'retryable';
    if (!in_array($status, array('email_required', 'retryable', 'pending_review', 'already_issued', 'rejected'), true)) {
        $status = 'retryable';
    }
    $code = isset($result['code']) ? (string)$result['code'] : 'gift_failed';
    if ($code === 'email_required') {
        $status = 'email_required';
    }

    update_option('easychat_wporg_gift_state', $status);
    update_option('easychat_wporg_gift_error_code', $code, false);
    update_option('easychat_wporg_gift_message', isset($result['message']) ? (string)$result['message'] : 'EasyTokens gift request was not completed. You can try again later.', false);
    update_option('easychat_wporg_gift_status_updated_at', time(), false);
}

function easychat_wporg_build_core_registry() {
    if (!easychat_wporg_install_core()) {
        return null;
    }

    easychat_wporg_require_core();

    try {
        $db = new \EasyChat\Library\DB(DB_DRIVER, DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_DATABASE, DB_PORT);
        $registry = new \EasyChat\Registry();
        $registry->set('db', $db);
        (new \EasyChat\Library\AppClock($registry))->syncDatabaseSessionTimezone();
        \EasyChat\Library\InstallService::ensureRuntimeSchema($db, DB_PREFIX);
        \EasyChat\Library\AppClock::resetTimezoneCache();
        (new \EasyChat\Library\AppClock($registry))->syncDatabaseSessionTimezone();
        $registry->set('cms_outbound_mail_bridge', new \EasyChat\Library\Email\CallableCmsMailBridge(
            'wordpress',
            array('send'=>true,'reply_to'=>true,'message_id'=>true,'threading'=>true,'attachments'=>false,'sender_email'=>(string)get_option('admin_email','')),
            function (\EasyChat\Library\Email\MailEnvelope $mail) {
                $headers = array_values(array_filter($mail->headers(), function ($header) {
                    return stripos($header, 'To:') !== 0 && stripos($header, 'Subject:') !== 0;
                }));
                return wp_mail($mail->to(), $mail->subject(), $mail->text(), $headers);
            }
        ));

        return $registry;
    } catch (\Throwable $e) {
        update_option('easychat_wporg_last_error', 'EasyChat core bootstrap failed.');
        return null;
    }
}

function easychat_wporg_sanitize_cron_message($message) {
    $message = trim((string)$message);
    if ($message === '') {
        return '';
    }

    $message = preg_replace('/(password|passwd|secret|token|key)\s*[:=]\s*[^\s,&]+/i', '$1=[redacted]', $message);
    return mb_substr($message, 0, 500);
}

function easychat_wporg_run_queue_tick() {
    if (get_transient(EASYCHAT_WPORG_CRON_LOCK_KEY)) {
        update_option('easychat_wporg_cron_last_skipped_at', time(), false);
        update_option('easychat_wporg_cron_last_skip_reason', 'locked', false);
        update_option('easychat_wporg_cron_last_result', array(
            'skipped' => true,
            'reason' => 'locked',
        ), false);

        return array('skipped' => true, 'reason' => 'locked');
    }

    set_transient(EASYCHAT_WPORG_CRON_LOCK_KEY, '1', EASYCHAT_WPORG_CRON_LOCK_TTL);
    update_option('easychat_wporg_cron_last_run_at', time(), false);

    $buffer_level = ob_get_level();
    ob_start();

    try {
        $registry = easychat_wporg_build_core_registry();
        if (!$registry) {
            throw new \RuntimeException('EasyChat core bootstrap failed.');
        }

        $telegram_polling_result = null;
        try {
            if (class_exists('\\EasyChat\\Library\\Messenger\\TelegramPollingService')) {
                $telegram_polling = new \EasyChat\Library\Messenger\TelegramPollingService($registry);
                $telegram_polling_result = $telegram_polling->run(array('limit' => 25));
            }
        } catch (\Throwable $e) {
            $telegram_polling_result = array(
                'success' => false,
                'error' => 'telegram_polling_failed',
            );
        }

        $worker = new \EasyChat\Library\Queue\QueueWorker($registry);
        $result = $worker->run(array(
            'batch_size' => 3,
            'time_limit' => 20,
            'runner' => 'wordpress_org',
        ));
        if (is_array($telegram_polling_result)) {
            $result['telegram_polling'] = $telegram_polling_result;
        }

        $output = '';
        if (ob_get_level() > $buffer_level) {
            $output = (string)ob_get_clean();
        }

        $output_preview = easychat_wporg_sanitize_cron_message($output);
        if ($output_preview !== '') {
            $result['unexpected_output'] = $output_preview;
            $queue = new \EasyChat\Library\Queue\QueueManager($registry);
            $queue->log('WP-Cron output suppressed: ' . $output_preview);
        }

        update_option('easychat_wporg_cron_last_success_at', time(), false);
        update_option('easychat_wporg_cron_last_result', $result, false);
        delete_option('easychat_wporg_cron_last_error');

        return $result;
    } catch (\Throwable $e) {
        while (ob_get_level() > $buffer_level) {
            ob_end_clean();
        }

        $message = easychat_wporg_sanitize_cron_message($e->getMessage());
        update_option('easychat_wporg_cron_last_error', $message, false);
        update_option('easychat_wporg_cron_last_result', array(
            'error' => $message,
        ), false);

        return array('error' => $message);
    } finally {
        delete_transient(EASYCHAT_WPORG_CRON_LOCK_KEY);
    }
}
add_action(EASYCHAT_WPORG_CRON_HOOK, 'easychat_wporg_run_queue_tick');

function easychat_wporg_register_rest_routes() {
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/widget.js', array(
        'methods' => 'GET',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/widget/init', array(
        'methods' => array('GET', 'POST'),
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget_init',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/widget/send', array(
        'methods' => 'POST',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget_send',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/widget/ai-stream', array(
        'methods' => 'POST',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget_ai_stream',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/attachment/image', array(
        'methods' => 'GET',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget_image',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/attachment/avatar', array(
        'methods' => 'GET',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_widget_avatar',
    ));
    register_rest_route(EASYCHAT_WPORG_REST_NAMESPACE, '/asset/(?P<asset_path>[A-Za-z0-9_./-]+)', array(
        'methods' => 'GET',
        'permission_callback' => 'easychat_wporg_public_rest_permission',
        'callback' => 'easychat_wporg_rest_asset',
    ));
}
add_action('rest_api_init', 'easychat_wporg_register_rest_routes');

function easychat_wporg_public_rest_permission() {
    // Эти endpoints намеренно публичны: widget loader/API и whitelist assets
    // обслуживают посетителей сайта. Админка и operator console проходят через
    // отдельные защищенные WordPress routes, а не через REST proxy.
    return true;
}

function easychat_wporg_admin_post_core() {
    if (!current_user_can('manage_options')) {
        wp_die(
            esc_html__('You do not have permission to open EasyChat24.', 'easychat24-live-chat'),
            esc_html__('EasyChat24 access denied', 'easychat24-live-chat'),
            array('response' => 403)
        );
    }

    $nonce = filter_input(INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW);
    $nonce = is_string($nonce) ? sanitize_text_field($nonce) : '';
    if (!$nonce || !wp_verify_nonce($nonce, 'easychat_wporg_core_view')) {
        wp_die(esc_html__('EasyChat24 admin link expired. Please open EasyChat24 again from the WordPress menu.', 'easychat24-live-chat'));
    }

    $route = filter_input(INPUT_GET, 'easychat_route', FILTER_UNSAFE_RAW);
    $route = is_string($route) && $route !== '' ? sanitize_text_field($route) : 'index';
    easychat_wporg_dispatch_core_admin($route);
}
add_action('admin_post_easychat_wporg_core', 'easychat_wporg_admin_post_core');

function easychat_wporg_template_redirect_operator() {
    if ((string)get_query_var('easychat_wporg_operator') !== '1') {
        return;
    }

    $route = filter_input(INPUT_GET, 'easychat_route', FILTER_UNSAFE_RAW);
    $route = is_string($route) && $route !== '' ? sanitize_text_field($route) : 'index';
    easychat_wporg_dispatch_core_operator($route);
}
add_action('template_redirect', 'easychat_wporg_template_redirect_operator');

function easychat_wporg_rest_widget() {
    easychat_wporg_dispatch_core_public('widget', true);
}

function easychat_wporg_rest_widget_init() {
    easychat_wporg_dispatch_core_public('widget/init', true);
}

function easychat_wporg_rest_widget_send() {
    easychat_wporg_dispatch_core_public('widget/send', true);
}

function easychat_wporg_rest_widget_ai_stream() {
    easychat_wporg_dispatch_core_public('widget/aiStream', false);
}

function easychat_wporg_rest_widget_image() {
    easychat_wporg_dispatch_core_public('widget/image', false);
}

function easychat_wporg_rest_widget_avatar() {
    easychat_wporg_dispatch_core_public('widget/avatar', false);
}

function easychat_wporg_rest_asset($request) {
    $asset_path = isset($request['asset_path']) ? rawurldecode((string)$request['asset_path']) : '';
    $asset_path = str_replace('\\', '/', $asset_path);
    $asset_path = ltrim($asset_path, '/');

    if ($asset_path === '' || strpos($asset_path, '..') !== false) {
        return new WP_Error('easychat_bad_asset', 'Invalid EasyChat asset path.', array('status' => 400));
    }

    $extension = strtolower(pathinfo($asset_path, PATHINFO_EXTENSION));
    $mime_map = array(
        'css' => 'text/css; charset=UTF-8',
        'js' => 'application/javascript; charset=UTF-8',
        'json' => 'application/json; charset=UTF-8',
        'map' => 'application/json; charset=UTF-8',
        'png' => 'image/png',
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'gif' => 'image/gif',
        'svg' => 'image/svg+xml',
        'webp' => 'image/webp',
        'ico' => 'image/x-icon',
        'woff' => 'font/woff',
        'woff2' => 'font/woff2',
        'ttf' => 'font/ttf',
        'otf' => 'font/otf',
        'eot' => 'application/vnd.ms-fontobject',
    );

    if (!isset($mime_map[$extension])) {
        return new WP_Error('easychat_bad_asset_type', 'Unsupported EasyChat asset type.', array('status' => 404));
    }

    $core_root = realpath(EASYCHAT_WPORG_CORE_DIR);
    $file_path = realpath(EASYCHAT_WPORG_CORE_DIR . '/' . $asset_path);
    if (!$core_root || !$file_path || strpos($file_path, $core_root . DIRECTORY_SEPARATOR) !== 0 || !is_file($file_path)) {
        return new WP_Error('easychat_asset_not_found', 'EasyChat asset not found.', array('status' => 404));
    }

    header('Content-Type: ' . $mime_map[$extension]);
    header('X-Content-Type-Options: nosniff');
    if (!function_exists('WP_Filesystem')) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    WP_Filesystem();
    global $wp_filesystem;
    $contents = $wp_filesystem ? $wp_filesystem->get_contents($file_path) : false;
    if ($contents === false) {
        return new WP_Error('easychat_asset_read_failed', 'EasyChat asset could not be read.', array('status' => 404));
    }

    header('Cache-Control: public, max-age=86400');
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Отдаем уже проверенные бинарные asset-файлы с nosniff и whitelist MIME.
    echo $contents;
    exit;
}

function easychat_wporg_dispatch_core_public($route, $rewrite_public_output = false) {
    easychat_wporg_install_core();
    easychat_wporg_dispatch_core($route, false, $rewrite_public_output);
}

function easychat_wporg_dispatch_core_admin($route) {
    easychat_wporg_install_core();
    easychat_wporg_dispatch_core($route, true, false, false);
}

function easychat_wporg_dispatch_core_operator($route) {
    easychat_wporg_install_core();
    easychat_wporg_dispatch_core($route, false, false, true);
}

function easychat_wporg_dispatch_core($route, $admin_context, $rewrite_public_output = false, $operator_context = false) {
    $registry = easychat_wporg_build_core_registry();

    if (!$registry || !class_exists('\\EasyChat\\Router')) {
        status_header(500);
        echo 'EasyChat core bootstrap failed.';
        exit;
    }

    if ($admin_context) {
        ob_start('easychat_wporg_rewrite_admin_output');
    } elseif ($operator_context) {
        ob_start('easychat_wporg_rewrite_operator_output');
    } elseif ($rewrite_public_output) {
        ob_start('easychat_wporg_rewrite_public_output');
    }

    $normalized_route = trim((string)$route, '/');
    $session_ready = easychat_wporg_start_session_if_needed($normalized_route);
    if (($admin_context || $operator_context) && !$session_ready) {
        status_header(503);
        header('Retry-After: 60');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        echo 'EasyChat could not open an isolated operator session. Return to WordPress and try again.';
        exit;
    }

    if ($admin_context && empty($_SESSION['manager_id']) && strpos(strtolower($normalized_route), 'auth') !== 0) {
        if (!easychat_wporg_open_admin_session($registry)) {
            if (class_exists('\\EasyChat\\Library\\SessionBootstrap')
                && \EasyChat\Library\SessionBootstrap::isAvailable()
            ) {
                \EasyChat\Library\SessionBootstrap::destroy();
            }
            status_header(503);
            header('Retry-After: 60');
            header('Cache-Control: no-store, no-cache, must-revalidate');
            echo 'EasyChat could not confirm the protected WordPress admin session. Return to WordPress and try again.';
            exit;
        }
    }

    if ($operator_context && strtolower($normalized_route) === 'auth/logout') {
        if (class_exists('\\EasyChat\\Library\\SessionBootstrap')) {
            \EasyChat\Library\SessionBootstrap::destroy();
        }
        wp_safe_redirect(easychat_wporg_operator_login_url());
        exit;
    }

    if (($admin_context || $operator_context) && empty($_SESSION['manager_id']) && strpos($normalized_route, 'auth') !== 0) {
        $normalized_route = 'auth';
    }

    if (($admin_context || $operator_context) && !defined('EASYCHAT_ROUTE_BASE_URL')) {
        define('EASYCHAT_ROUTE_BASE_URL', $admin_context
            ? easychat_wporg_core_dispatch_base_url()
            : easychat_wporg_operator_login_url() . '?easychat_route=');
    }

    $router = new \EasyChat\Router($registry);
    $router->setPath(EASYCHAT_WPORG_CORE_DIR . DS . 'controllers');
    $router->setRoute($normalized_route);
    $router->start();

    if ($admin_context || $operator_context || $rewrite_public_output) {
        ob_end_flush();
    }
    exit;
}

function easychat_wporg_open_admin_session($registry) {
    if (!current_user_can('manage_options')
        || !class_exists('\\EasyChat\\Library\\SessionBootstrap')
        || !\EasyChat\Library\SessionBootstrap::isAvailable()
    ) {
        return false;
    }

    if (!empty($_SESSION['cms_handoff_pending_claim'])) {
        \EasyChat\Library\SessionBootstrap::destroy();
        return false;
    }

    if (!$registry || !class_exists('\\EasyChat\\Model\\Manager')) {
        return false;
    }

    try {
        $model_manager = new \EasyChat\Model\Manager($registry);
        $manager = $model_manager->getManagerById(1);
        if (!$manager || (int)$manager['status'] !== 1 || (string)$manager['role'] !== 'admin') {
            return false;
        }

        $session =& \EasyChat\Library\RequestContext::session();
        if (!empty($session['manager_id'])) {
            $database_hash = md5((string)$manager['password']);
            $session_hash = (string)($session['manager_pass_hash'] ?? '');
            if ((int)$session['manager_id'] === (int)$manager['id']
                && $session_hash !== ''
                && hash_equals($database_hash, $session_hash)
            ) {
                // Существующая валидная сессия не требует rotation на каждом
                // внутреннем переходе. Поля роли берем из БД, а не доверяем
                // старому session payload после обновления аккаунта.
                $session['manager_role'] = (string)$manager['role'];
                $session['manager_name'] = (string)$manager['name'];
                $session['manager_img'] = (string)$manager['image'];
                $session['login_source'] = 'cms_adapter';
                $session['login_cms_source'] = 'wordpress';
                return true;
            }

            // Не отправляем WordPress-администратора на форму с неизвестным
            // паролем из-за stale/поврежденной EasyChat-сессии. Текущий nonce
            // и manage_options уже дают право выпустить новую локальную сессию.
            \EasyChat\Library\RequestContext::replaceSession(array());
        }

        // WordPress уже проверил manage_options и nonce у admin-post route.
        // Здесь открываем локальную EasyChat-сессию без передачи пароля в URL,
        // DOM или postMessage, чтобы direct admin view сразу показывал админку.
        if (!\EasyChat\Library\SessionBootstrap::regenerateId()) {
            return false;
        }
        \EasyChat\Library\RequestContext::replaceSession(array());
        $_SESSION['manager_id'] = (int)$manager['id'];
        $_SESSION['manager_role'] = (string)$manager['role'];
        $_SESSION['manager_name'] = (string)$manager['name'];
        $_SESSION['manager_img'] = (string)$manager['image'];
        $_SESSION['manager_pass_hash'] = md5((string)$manager['password']);
        $_SESSION['login_source'] = 'cms_adapter';
        $_SESSION['login_cms_source'] = 'wordpress';

        // Подсказка полезна, но не является частью security boundary. Ошибка
        // чтения необязательного marker-а не должна оставлять полусессию или
        // превращать корректный WordPress handoff в login-loop.
        try {
            if (!$model_manager->isAdminPasswordCustomized()) {
                $_SESSION['suggest_password_change'] = 1;
            }
        } catch (\Throwable $e) {
        }

        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence(array(
            'manager_id' => (int)$manager['id'],
            'manager_pass_hash' => md5((string)$manager['password']),
        ))) {
            \EasyChat\Library\SessionBootstrap::destroy();
            return false;
        }

        try {
            $model_manager->runMigrations();
        } catch (\Throwable $e) {
        }

        if (class_exists('\\EasyChat\\Library\\PresenceService')) {
            try {
                $presence = new \EasyChat\Library\PresenceService($registry);
                $presence->markManagerActive((int)$manager['id'], 'web_login', true);
            } catch (\Throwable $e) {
            }
        }

        return true;
    } catch (\Throwable $e) {
        if (class_exists('\\EasyChat\\Library\\SessionBootstrap')
            && \EasyChat\Library\SessionBootstrap::isAvailable()
        ) {
            \EasyChat\Library\SessionBootstrap::destroy();
        }
        update_option('easychat_wporg_last_error', 'EasyChat24 could not open the secure WordPress admin session.');
        return false;
    }
}

function easychat_wporg_start_session_if_needed($route) {
    $normalized = strtolower(trim((string)$route, '/'));
    $no_session = in_array($normalized, array('widget', 'widget/index', 'messenger/webhook'), true);
    if ($no_session) {
        return true;
    }

    if (class_exists('\\EasyChat\\Library\\SessionBootstrap')) {
        return \EasyChat\Library\SessionBootstrap::start(easychat_wporg_runtime_dir() . '/log');
    }

    if (session_status() === PHP_SESSION_ACTIVE) {
        return false;
    }
    $session_dir = easychat_wporg_runtime_dir() . '/sessions';
    if (is_dir($session_dir)) {
        session_save_path($session_dir);
    }
    return (bool)session_start();
}

function easychat_wporg_rewrite_admin_output($html) {
    $base = easychat_wporg_core_dispatch_base_url();
    $core_route = easychat_wporg_core_base_url() . 'index.php?route=';
    // В WordPress admin view нельзя оставлять standalone <base>: он ломает
    // относительные якоря и усложняет диагностику. Все нужные ссылки ниже
    // переписываются явно.
    $html = preg_replace('#<base\s+href=["\'][^"\']*["\']\s*/?>#i', '', $html);
    $html = str_replace($core_route, $base, $html);
    $html = str_replace('index.php?route=', $base, $html);
    return easychat_wporg_rewrite_core_asset_urls($html);
}

function easychat_wporg_rewrite_operator_output($html) {
    $base = easychat_wporg_operator_login_url() . '?easychat_route=';
    $core_route = easychat_wporg_core_base_url() . 'index.php?route=';
    // Операторская страница живет вне wp-admin, но core все еще генерирует
    // standalone-ссылки. Переписываем их на публичный WordPress route, не
    // раскрывая vendor/core entrypoint и не требуя WordPress session.
    $html = preg_replace('#<base\s+href=["\'][^"\']*["\']\s*/?>#i', '', $html);
    $html = str_replace($core_route, $base, $html);
    $html = str_replace('index.php?route=', $base, $html);
    // Public operator login должен быть только ручным входом EasyChat. CMS-prefill
    // остается внутри защищенного wp-admin direct view и не должен работать на
    // ссылке, которую администратор отправляет менеджерам.
    $html = preg_replace('#\s*<input\b[^>]*\bname=["\']cms_prefill["\'][^>]*>#i', '', $html);
    $html = preg_replace('#\s*<input\b[^>]*\bname=["\']cms_source["\'][^>]*>#i', '', $html);
    $html = preg_replace(
        '#\s*function\s+decodeEasyChatBase64Url\s*\([^)]*\)\s*\{.*?applyCmsPrefillFromHash\(\);\s*#is',
        "\n",
        $html
    );
    return easychat_wporg_rewrite_core_asset_urls($html);
}

function easychat_wporg_rewrite_core_asset_urls($html) {
    $core_base = easychat_wporg_core_base_url();
    $asset_dirs = array('js', 'css', 'images', 'sound');
    foreach ($asset_dirs as $dir) {
        // Core получает HTTP_SERVER как internal /core/ route и генерирует
        // абсолютные ссылки. В WordPress.org их нельзя отдавать через core
        // dispatcher: браузер получит JSON/HTML вместо CSS/JS и заблокирует
        // файлы по MIME. Все статические файлы идут только через whitelist
        // static plugin URL. Это стандартный для WordPress путь: браузер не
        // делает отдельный PHP/REST hit на каждый JS/CSS файл и не упирается
        // в rate-limit на shared hosting.
        $html = preg_replace_callback(
            '#' . preg_quote($core_base . $dir . '/', '#') . '([^"\'\\s\\)<>]+)#',
            function ($matches) use ($dir) {
                return easychat_wporg_asset_url($dir . '/' . $matches[1]);
            },
            $html
        );
        $html = preg_replace_callback(
            '#(["\'])' . preg_quote($dir . '/', '#') . '([^"\']+)#',
            function ($matches) use ($dir) {
                return $matches[1] . easychat_wporg_asset_url($dir . '/' . $matches[2]);
            },
            $html
        );
    }
    return easychat_wporg_rewrite_admin_script_bundles($html);
}

function easychat_wporg_admin_script_tag($asset_ref) {
    return '<script src="' . esc_url(easychat_wporg_asset_url($asset_ref)) . '" defer></script>';
}

function easychat_wporg_script_src_pattern($asset_ref) {
    return '#\s*<script\b[^>]*\bsrc=(["\'])[^"\']*' . preg_quote($asset_ref, '#') . '(?:\?[^"\']*)?\1[^>]*>\s*</script>\s*#i';
}

function easychat_wporg_replace_first_script(&$html, $asset_ref, $replacement) {
    $count = 0;
    $html = preg_replace(easychat_wporg_script_src_pattern($asset_ref), "\n" . $replacement . "\n", $html, 1, $count);
    return $count > 0;
}

function easychat_wporg_remove_script(&$html, $asset_ref) {
    $html = preg_replace(easychat_wporg_script_src_pattern($asset_ref), "\n", $html);
}

function easychat_wporg_rewrite_admin_script_bundles($html) {
    $core_scripts = array(
        'js/bootstrap/js/bootstrap.bundle.min.js',
        'js/common.js',
        'js/qrcode-generator.js',
        'js/mobile_connect.js',
        'js/webpush.js',
        'js/notification_health.js',
    );

    $index_bundle = 'js/easychat-wporg-admin-index.js';
    if (is_file(EASYCHAT_WPORG_CORE_DIR . '/' . $index_bundle) && strpos($html, 'js/views/index.js') !== false) {
        // На WordPress shared hosting пачка одновременных JS-запросов из admin view
        // может попасть под 429. Для главного экрана отдаем один статический
        // bundle: это сохраняет внешний JS-файл и не добавляет inline-код.
        if (easychat_wporg_replace_first_script($html, $core_scripts[0], easychat_wporg_admin_script_tag($index_bundle))) {
            for ($i = 1; $i < count($core_scripts); $i++) {
                easychat_wporg_remove_script($html, $core_scripts[$i]);
            }
            easychat_wporg_remove_script($html, 'js/admin_conversation_state.js');
            easychat_wporg_remove_script($html, 'js/admin_instant_conversation_queue.js');
            easychat_wporg_remove_script($html, 'js/views/index.js');
        }
        return $html;
    }

    $core_bundle = 'js/easychat-wporg-admin-core.js';
    if (is_file(EASYCHAT_WPORG_CORE_DIR . '/' . $core_bundle)) {
        if (easychat_wporg_replace_first_script($html, $core_scripts[0], easychat_wporg_admin_script_tag($core_bundle))) {
            for ($i = 1; $i < count($core_scripts); $i++) {
                easychat_wporg_remove_script($html, $core_scripts[$i]);
            }
        }
    }

    return $html;
}

function easychat_wporg_rewrite_public_output($body) {
    $core_route = easychat_wporg_core_base_url() . 'index.php?route=';
    // Query REST routes are more tolerant of shared hosting setups where
    // permalinks or reverse proxies mishandle POST requests to /wp-json/...
    $body = easychat_wporg_rewrite_public_route($body, $core_route . 'widget/init', easychat_wporg_public_rest_url('widget/init'));
    $body = easychat_wporg_rewrite_public_route($body, $core_route . 'widget/send', easychat_wporg_public_rest_url('widget/send'));
    $body = easychat_wporg_rewrite_public_route($body, $core_route . 'widget/aiStream', easychat_wporg_public_rest_url('widget/ai-stream'));
    $body = easychat_wporg_rewrite_public_route($body, $core_route . 'widget/image&', easychat_wporg_public_rest_query_base('attachment/image'));
    $body = easychat_wporg_rewrite_public_route($body, $core_route . 'widget/avatar&', easychat_wporg_public_rest_query_base('attachment/avatar'));
    return $body;
}

function easychat_wporg_rewrite_public_route($body, $from, $to) {
    $body = str_replace($from, $to, $body);
    return str_replace(str_replace('/', '\\/', $from), str_replace('/', '\\/', $to), $body);
}

function easychat_wporg_enqueue_widget() {
    if (is_admin() || !easychat_wporg_widget_enabled()) {
        return;
    }

    easychat_wporg_install_core();
    wp_enqueue_script(
        'easychat-widget',
        easychat_wporg_public_rest_url('widget.js'),
        array(),
        easychat_wporg_version(),
        true
    );
    wp_script_add_data('easychat-widget', 'strategy', 'async');
}
add_action('wp_enqueue_scripts', 'easychat_wporg_enqueue_widget');

function easychat_wporg_visitor_identity() {
    nocache_headers();
    if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST'
        || strtolower((string)($_SERVER['HTTP_SEC_FETCH_SITE'] ?? 'same-origin')) === 'cross-site') {
        status_header(405); wp_send_json_error(array('error' => 'method_not_allowed'));
    }
    $session_id = sanitize_text_field((string)($_POST['session_id'] ?? ''));
    if (!preg_match('/^[a-f0-9]{32}$/i', $session_id) || !is_user_logged_in()) {
        wp_send_json(array('authenticated' => false));
    }
    easychat_wporg_require_core();
    $user = wp_get_current_user();
    $assertion = \EasyChat\Library\Cms\CmsVisitorIdentityAssertion::issue(array(
        'provider' => 'wordpress', 'scope' => 'wordpress|' . $GLOBALS['wpdb']->prefix,
        'subject' => (string)$user->ID, 'name' => (string)$user->display_name,
        'email' => (string)$user->user_email,
        'phone' => (string)get_user_meta((int)$user->ID, 'billing_phone', true),
    ), $session_id, (string)easychat_wporg_core_setting('chat_install_secret'));
    wp_send_json(array('authenticated' => $assertion !== '', 'assertion' => $assertion));
}
add_action('wp_ajax_easychat_wporg_visitor_identity', 'easychat_wporg_visitor_identity');
add_action('wp_ajax_nopriv_easychat_wporg_visitor_identity', 'easychat_wporg_visitor_identity');

function easychat_wporg_ai_page_context_enabled() {
    return easychat_wporg_core_setting('chat_ai_status') === '1'
        || easychat_wporg_core_setting('chat_ai_suggestions_status') === '1';
}

function easychat_wporg_current_page_context() {
    if (function_exists('is_product') && is_product()) {
        $id = (int)get_the_ID();
        return $id > 0 ? array('type' => 'product', 'id' => (string)$id) : array();
    }

    if (function_exists('is_product_category') && is_product_category()) {
        $term = get_queried_object();
        $id = is_object($term) && isset($term->term_id) ? (int)$term->term_id : 0;
        return $id > 0 ? array('type' => 'category', 'id' => (string)$id) : array();
    }

    if (is_singular()) {
        $id = (int)get_queried_object_id();
        return $id > 0 ? array('type' => 'information', 'id' => (string)$id) : array();
    }

    return array();
}

function easychat_wporg_page_context_meta_tag($name, $value) {
    return '<meta name="easychat:' . esc_attr((string)$name) . '" content="' . esc_attr((string)$value) . '">' . "\n";
}

function easychat_wporg_page_context_meta() {
    if (is_admin() || !easychat_wporg_widget_enabled()) {
        return;
    }

    easychat_wporg_sync_cms_integration();
    if (!easychat_wporg_ai_page_context_enabled()) {
        return;
    }

    $context = easychat_wporg_current_page_context();
    if (empty($context)) {
        return;
    }

    echo easychat_wporg_page_context_meta_tag('cms-provider', 'wordpress');
    echo easychat_wporg_page_context_meta_tag('page-type', $context['type']);
    echo easychat_wporg_page_context_meta_tag('page-id', $context['id']);
    echo easychat_wporg_page_context_meta_tag('schema-version', '1');
}
add_action('wp_head', 'easychat_wporg_page_context_meta', 20);

function easychat_wporg_widget_script_tag($tag, $handle, $src) {
    if ($handle !== 'easychat-widget') {
        return $tag;
    }
    return '<script src="' . esc_url($src) . '" data-easychat-identity-endpoint="' . esc_url(admin_url('admin-ajax.php?action=easychat_wporg_visitor_identity')) . '" async></script>';
}
add_filter('script_loader_tag', 'easychat_wporg_widget_script_tag', 10, 3);

function easychat_wporg_uninstall() {
    easychat_wporg_clear_cron();
    delete_transient(EASYCHAT_WPORG_CRON_LOCK_KEY);

    if (get_option('easychat_wporg_delete_data_on_uninstall', '0') !== '1') {
        return;
    }

    global $wpdb;
    $tables = array(
        'settings', 'managers', 'sessions', 'messages', 'reply', 'app_session',
        'moderation_tokens', 'queue', 'knowledge', 'vectors', 'vector_meta',
        'messenger_channels', 'messenger_contacts', 'messenger_deliveries',
        'message_attachments',
    );
    foreach ($tables as $table) {
        $table_name = EASYCHAT_WPORG_DB_PREFIX . $table;
        if (!preg_match('/^[A-Za-z0-9_]+$/', $table_name)) {
            continue;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Таблицы удаляются только при явном uninstall cleanup и проходят строгий whitelist.
        $wpdb->query('DROP TABLE IF EXISTS `' . esc_sql($table_name) . '`');
    }

    delete_option('easychat_wporg_installed');
    delete_option('easychat_wporg_installed_version');
    delete_option('easychat_wporg_rewrite_marker');
    delete_option('easychat_wporg_upgrade_notice');
    delete_option('easychat_wporg_widget_enabled');
    delete_option('easychat_wporg_admin_login');
    delete_option('easychat_wporg_admin_password');
    delete_option('easychat_wporg_admin_initial_password');
    delete_option('easychat_wporg_credential_cleanup');
    delete_option('easychat_wporg_gift_state');
    delete_option('easychat_wporg_gift_email');
    delete_option('easychat_wporg_gift_error_code');
    delete_option('easychat_wporg_gift_message');
    delete_option('easychat_wporg_gift_requested_at');
    delete_option('easychat_wporg_gift_status_updated_at');
    delete_option('easychat_wporg_last_error');
    delete_option('easychat_wporg_cron_last_run_at');
    delete_option('easychat_wporg_cron_last_success_at');
    delete_option('easychat_wporg_cron_last_error');
    delete_option('easychat_wporg_cron_last_result');
    delete_option('easychat_wporg_cron_last_skipped_at');
    delete_option('easychat_wporg_cron_last_skip_reason');
    delete_option('easychat_wporg_queue_notice');
}

register_uninstall_hook(__FILE__, 'easychat_wporg_uninstall');
