<?php
/**
 * Plugin Name: TWX-Chat
 * Description: Show smart Avatar/Embedded/Floating chatbots on your website. Chatbot builder, Site Crawler and Knowledge Base included, plus loads more Features!
 * Version: 1.8.5
 * Plan: free
 * Author: TWXLabs.au
 * Author URI: https://twxchat.twxlabs.au
 * Requires PHP: 8.1
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/

if (!defined('ABSPATH')) exit; // Exit if accessed directly

$twxchat_max_chatlog_rows = 5000; // may allow Admins to change this, hence is a variable

// Initialise Client and DB Timezones 
twxchat_init_timezone_options();

// Include the Composer autoloader
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
    require_once __DIR__ . '/vendor/autoload.php';
}

require_once plugin_dir_path(__FILE__) . 'includes/ajax_handler.php';
require_once plugin_dir_path(__FILE__) . 'includes/ajax_handler_admin.php';
require_once plugin_dir_path(__FILE__) . 'includes/twx_spider.php';

$GLOBALS['twxchat_chatbot_rendered'] = false; // can be referenced in this script using: global $twxchat_chatbot_rendered;

if ( ! defined( 'TWXCHAT_DEFAULT_MIN_SIMILARITY' ) ) {
	define( 'TWXCHAT_DEFAULT_MIN_SIMILARITY', 75 );
}

function twxchat_init_timezone_options(){

    // Gets Client and DB timezones and stores in separate options for ready access by other scripts such as spider, Chat Log html generator etc
    // These needs to be in separate options otherwise they seem to get overwritten by admin_init scripts.

    // set client timezone each time in case changed in WP settings
    $client_timezone = get_option('timezone_string');
    if(empty($client_timezone)){
        // could be Offset is selected in General Settings instead of timezone name
        $client_timezone = twxchat_get_etc_gmt_timezone(get_option('gmt_offset'));  // Will be something like Etc/GMT-10 which is actually equivalent to UTC+10  (POSIX standard)
    }
    if(!empty($client_timezone)) {
        update_option('twx_chat_client_timezone', $client_timezone);
    }

    // DB timezone - normally only need to set this once, unlikely DB/Server will change it's timezone - not true, twice a year (Daylight Savings) !
    update_option('twx_chat_db_timezone', twxchat_get_etc_gmt_timezone(twxchat_get_db_utc_offset()));
}

function twxchat_get_etc_gmt_timezone($offset) {
    // Get POSIX standard timezone, eg: Etc/GMT-10 (for UTC+10), or Etc/GMT+6 (for UTC-6) etc
    if (!is_numeric($offset)) {
        return 'Etc/GMT';
    }
    // Reverse the sign to match PHP's "Etc/GMT" format
    $etc_offset = (int) $offset * -1;
    // Construct and return the Etc/GMT timezone string
    return 'Etc/GMT' . ($etc_offset == 0 ? '' : ($etc_offset > 0 ? "+$etc_offset" : $etc_offset));
}

function twxchat_get_db_utc_offset() {
    // Get WP Database Timezone Offset
    global $wpdb;
    $db_timezone = $wpdb->get_var("SELECT @@global.time_zone");

    if ($db_timezone == 'SYSTEM') {
        // Get the offset as signed integer (correctly handles negative values)
        $var = $wpdb->get_var("SELECT TIME_FORMAT(TIMEDIFF(NOW(), UTC_TIMESTAMP), '%H')");
        return (int) $var;
    }else{
        return 0;
    }
}

register_activation_hook(__FILE__, 'twxchat_activation_hook');
function twxchat_activation_hook() {

    // tiggered on plugin activation/update
    global $wpdb;

    // Get the correct table prefix
    $table_vectors = $wpdb->prefix . 'twxchat_vectors';
    $table_chatlog = $wpdb->prefix . 'twxchat_chatlog';

    // SQL statement to create the twxchat_vectors table
    $sql_vectors = "CREATE TABLE IF NOT EXISTS $table_vectors (
        id INT(11) NOT NULL AUTO_INCREMENT,
        source LONGTEXT NOT NULL,
        orig_text LONGTEXT NOT NULL,
        vector LONGTEXT NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";

    // SQL statement to create the twxchat_chatlog table
    $sql_chatlog = "CREATE TABLE IF NOT EXISTS $table_chatlog (
        id INT(11) NOT NULL AUTO_INCREMENT,
        thread_id VARCHAR(255) NOT NULL,
        properties LONGTEXT NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";

    // Include the upgrade file for dbDelta function
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');

    // Run the SQL statements using dbDelta
    dbDelta($sql_vectors);
    dbDelta($sql_chatlog);

    // Create upload dirs
    twxchat_createUploadsDirSpiderResults();
	twxchat_createUploadsDirGoogleDrive();

    // Schedule daily check of ChatLog
    if (!wp_next_scheduled('twx_chat_cleanup_chatlog_event')) {
        $time = current_time('timestamp');
        wp_schedule_event($time, 'daily', 'twx_chat_cleanup_chatlog_event');
        TWXChatUtil::adminLog("Scheduled daily chatlog cleanup starting at " . current_time('mysql'));
    } else {
        $scheduled = wp_next_scheduled('twx_chat_cleanup_chatlog_event');
        $scheduled_date = date('Y-m-d H:i:s', $scheduled);
        TWXChatUtil::adminLog("Scheduled daily chatlog cleanup already exists at " . $scheduled_date);
    }

    // Restore any previously saved site crawler jobs
    twxchat_restore_site_crawler_scheduled_jobs();

    // Generate Server Token
    if (!get_option('twx_chat_server_token')) {
        add_option('twx_chat_server_token', wp_generate_password(32, true, true));
    }    

}

register_deactivation_hook(__FILE__, 'twxchat_deactivation_hook');
function twxchat_deactivation_hook() {

    // tiggered on plugin deactivation
    TWXChatUtil::adminLog("twx_chat_deactivation_hook:: start");

    twxchat_clear_all_site_crawler_scheduled_jobs();

    // Allows Admin to reset Site Crawler Running Status
    update_option('twx_chat_site_crawler_running',false);
}

// Check if we have just updated the plugin
add_action('admin_init', function() {
    $pluginInfo = TWXChatUtil::getPluginInfo();
    $ver = $pluginInfo['Version'] ?? '0.0.0';
    $plan = $pluginInfo['Plan'] ?? 'free';
    $prevVer = get_option('twx_chat_plugin_ver','0.0.0');
    $prevPlan = get_option('twx_chat_plugin_plan','free');

    if($ver === $prevVer && $plan === $prevPlan) return;

    if(TWXChatUtil::isVersionUpgrade($prevVer, $prevPlan, $ver, $plan)){
        TWXChatUtil::adminLog("plugin version updated:: prevVer: $prevVer, prevPlan: $prevPlan, newVer: $ver, newPlan: $plan");
        update_option('twx_chat_plugin_ver', $ver);
        update_option('twx_chat_plugin_plan', $plan);
        twxchat_on_plugin_ver_change();
    }

});

// Verify customer
function twxchat_verifyCustomer(){

    if(!class_exists('TWXChatPlus')) {
        // Free version
        update_option('twx_chat_customer_plan', 'free');
        return;
    }
    $options = get_option('twx_chat_options');
    $customerKey = $options['twx_customer_key'] ?? '';
    if(!empty($customerKey)) {
        TWXChatPlus::verifyCustomer($customerKey);
    }
}
add_action('admin_init', 'twxchat_verifyCustomer');

function twxchat_on_plugin_ver_change(){

    // Triggers on Plugin Version Change - useful for future updates

    //TWXChatUtil::adminLog('twxchat_on_plugin_ver_change:: start');

    // Other stuff for this (or previous) versions
    twxchat_createUploadsDirSpiderResults();
	twxchat_createUploadsDirGoogleDrive();

    // v1.3.4
    twxchat_updateTableColumnTypeVectorSource();

    // v1.4.0
    twxchat_addTitleColumnToVectorsTable();
    twxchat_createChatbotsTable();

    // v1.4.1
    twxchat_createUploadsDirAvatarAssets();

}

function twxchat_createUploadsDirSpiderResults(){
    // only if not exists

    $dest_dir = TWXChatUtil::getUploadsDir('spider_results');
    TWXChatUtil::makeDir($dest_dir);
}

function twxchat_createUploadsDirGoogleDrive() {
    // Get the directory path
    $dest_dir = TWXChatUtil::getUploadsDir('google_drive');
    TWXChatUtil::makeDir($dest_dir);
}

function twxchat_createUploadsDirAvatarAssets(){
    
    $dest_dir = TWXChatUtil::getAvatarAssetsRootPath();
    TWXChatUtil::makeDir($dest_dir);
}

function twxchat_updateTableColumnTypeVectorSource() {
    global $wpdb;

    $table_name = $wpdb->prefix . 'twxchat_vectors';
    $column_name = 'source';

    // Validate identifiers (basic safeguard)
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name) || !preg_match('/^[a-zA-Z0-9_]+$/', $column_name)) {
        TWXChatUtil::adminLog("twxchat_updateTableColumnTypeVectorSource:: Invalid table or column name: $table_name / $column_name", 'a', true);
        return;
    }

    // Check current column data type
    $column_type = $wpdb->get_var($wpdb->prepare(
        "SELECT DATA_TYPE
         FROM INFORMATION_SCHEMA.COLUMNS
         WHERE TABLE_NAME = %s
           AND COLUMN_NAME = %s
           AND TABLE_SCHEMA = DATABASE()",
        $table_name,
        $column_name
    ));

    // If not already LONGTEXT, modify column
    if (strtolower($column_type) !== 'longtext') {
        $table_escaped = esc_sql($table_name);
        $column_escaped = esc_sql($column_name);

        $sql = "ALTER TABLE `$table_escaped` MODIFY COLUMN `$column_escaped` LONGTEXT NOT NULL";

        $result = $wpdb->query($sql); // Plugin Checker complains not using prepare(), however we cannot use prepare() safely with ALTER TABLE sql

        if ($result === false) {
            TWXChatUtil::adminLog("twxchat_updateTableColumnTypeVectorSource:: SQL failed: $sql. Error: " . $wpdb->last_error, 'a', true);
        }
    }
}

function twxchat_addTitleColumnToVectorsTable() {
    global $wpdb;

    $table_name  = $wpdb->prefix . 'twxchat_vectors';
    $column_name = 'title';

    // Validate identifiers
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name) || !preg_match('/^[a-zA-Z0-9_]+$/', $column_name)) {
        TWXChatUtil::adminLog("twxchat_addTitleColumnToVectorsTable:: Invalid table or column name: $table_name / $column_name", 'a', true);
        return;
    }

    // Check if column exists
    $column_exists = $wpdb->get_var($wpdb->prepare(
        "SHOW COLUMNS FROM `$table_name` LIKE %s",
        $column_name
    ));

    // Add the column if it doesn't exist
    if (empty($column_exists)) {
        $table_escaped  = esc_sql($table_name);
        $column_escaped = esc_sql($column_name);

        $sql = "ALTER TABLE `$table_escaped` ADD `$column_escaped` VARCHAR(255) NULL DEFAULT NULL";
        $result = $wpdb->query($sql);  // Plugin Checker complains not using prepare(), however we cannot use prepare() safely with ALTER TABLE sql

        if ($result === false) {
            TWXChatUtil::adminLog("twxchat_addTitleColumnToVectorsTable:: SQL failed: $sql. Error: " . $wpdb->last_error, 'a', true);
        }
    }
}

function twxchat_createChatbotsTable() {
    global $wpdb;

    $table_name = $wpdb->prefix . 'twxchat_chatbots';

    // Basic validation (safety check)
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) {
        TWXChatUtil::adminLog("twxchat_createChatbotsTable:: Invalid table name: $table_name", 'a', true);
        return;
    }

    $charset_collate = $wpdb->get_charset_collate();

    // Use backticks and match dbDelta quirks
    $sql = "CREATE TABLE {$table_name} (
        id INT NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        settings LONGTEXT NOT NULL,
        PRIMARY KEY  (id)
    ) $charset_collate;";

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

    // Optionally log dbDelta errors or results
    if (!empty($wpdb->last_error)) {
        TWXChatUtil::adminLog("twxchat_createChatbotsTable:: DB error: " . $wpdb->last_error, 'a', true);
    }
}

// Add a settings link to the plugin actions on the Plugins page
function twxchat_action_links($links) {
    $settings_link = '<a href="options-general.php?page=twx-chat-settings">Settings</a>';
    array_unshift($links, $settings_link);
    return $links;
}
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'twxchat_action_links');

function twxchat_admin_menu() {

    add_options_page(
        'TWX-Chat Settings',
        'TWX-Chat',
        'manage_options',
        'twx-chat-settings',
        'twxchat_settings_page'
    );
}
add_action('admin_menu', 'twxchat_admin_menu');

function twxchat_settings_page() {

    global $twxchat_max_chatlog_rows;
    $num_chat_log_entries = TWXChatLog::getNumEntries();
    $num_chat_log_entries_to_keep = $num_chat_log_entries;
    if($num_chat_log_entries > ($twxchat_max_chatlog_rows * 0.5)){
        $num_chat_log_entries_to_keep = intval($twxchat_max_chatlog_rows * 0.5);
    }

    ?>
    <div class="wrap">
        <h1>TWX-Chat Settings</h1>
        <h2 class="nav-tab-wrapper">
            <a href="#general" data-tab="general" class="nav-tab">General</a>
            <a href="#chatbots" data-tab="chatbots" class="nav-tab">Chatbots</a>
            <a href="#customize" data-tab="customize" class="nav-tab">Customise</a>
            <a href="#site-crawler" data-tab="site-crawler" class="nav-tab">Site Crawler</a>
            <a href="#knowledgebase" data-tab="knowledgebase" class="nav-tab">Knowlegde Base</a>
            <a href="#chatlog" data-tab="chatlog" class="nav-tab">Chat Log</a>
            <a href="#plugininfo" data-tab="plugininfo" class="nav-tab">Plugin Info</a>
        </h2>
        
        <div id="general" class="twx-chat-tab-content">
            <form method="post" action="options.php">
                <?php
                settings_fields('twx_chat_all_options_group');
                do_settings_sections('twx-chat-general');
                submit_button();
                ?>
            </form>
        </div>

        <div id="chatbots" class="twx-chat-tab-content">
            <h2>Chatbots</h2>
            <div id="twx-chat-chatbots-messages" class="twx-chat-admin-message"></div>
            <button id="twx-chat-chatbots-new-btn" class="button-primary">Create New Chatbot</button>
            <span id="twx-chat-chatbots-new-limit-reached" style="display:none;" class="twx-plus-button">Chatbot Limit Reached. Please upgrade to get more.</span>
            <table id="twx-chat-chatbots-table" class="widefat striped"></table>
            <div id="twx-chat-chatbot-edit"></div>
        </div>

        <div id="customize" class="twx-chat-tab-content">
            <form method="post" action="options.php">
                <?php
                settings_fields('twx_chat_all_options_group');
                do_settings_sections('twx-chat-customize');
                submit_button();
                ?>
            </form>
        </div>
        
        <div id="site-crawler" class="twx-chat-tab-content">
            <form method="post" action="options.php">
                <?php
                settings_fields('twx_chat_all_options_group');
                do_settings_sections('twx-chat-site-crawler');
                submit_button();
                ?>
            </form>
        </div>

        <div id="knowledgebase" class="twx-chat-tab-content">
            <div id="twx-chat-kb-top-buttons">
                <button id="twx-chat-knowledgebase-search-btn">Load Knowledge Base</button>
                &nbsp;<button id="twx-chat-knowledgebase-clearfilters-btn" style="display:none;">Clear Filters</button>
                &nbsp;<button id="twx-chat-knowledgebase-newentry-btn" style="display:none;">New Entry</button>
                &nbsp;<button id="twx-chat-knowledgebase-editentries-btn" style="display:none;">Edit Entries</button>
                &nbsp;<button id="twx-chat-knowledgebase-deleteentries-btn" style="display:none;">Delete Entries</button>
                &nbsp;<button id="twx-chat-knowledgebase-saveentries-btn" style="display:none;">Save</button>
                &nbsp;<button id="twx-chat-knowledgebase-cancelaction-btn" style="display:none;">Cancel</button>
                <form id="twx-chat-kb-upload-file-form" method="post" enctype="multipart/form-data">
                    <input type="file" name="twx_chat_kb_upload_file" required>
                    <button type="submit" class="button button-primary">Upload File</button>
                </form>
            </div>
            <div id="twx-chat-kb-messages" class="twx-chat-admin-message"></div>
			<div id="twx-chat-knowledgebase-grid"></div>
        </div>

        <div id="chatlog" class="twx-chat-tab-content">
            <?php if(TWXChatUtil::customerPlanAccess('plus')) {
                ?>
                <br/>
                <button id="twx-chat-download-chatlog-btn" style="display:none">Download the Chat Log (CSV format)</button>&nbsp;
                <button id="twx-chat-delete-chatlog-entries-btn" data-max-entries="<?php echo esc_attr($twxchat_max_chatlog_rows);?>" data-entries-to-keep="<?php echo esc_attr($num_chat_log_entries_to_keep);?>" data-current-entries="<?php echo esc_attr($num_chat_log_entries);?>">Delete old Chat Log entries</button>
                <br/><div id="twx-chat-chatlog-periods"></div>
                <div id="twx-chat-chatlog-messages" class="twx-chat-admin-message"></div>
                <br/><div id="twx-chat-chatlog-grid"></div>
                <?php
            }else{
                ?>
                <br/><p class="description">The <span class="twx-chat-feature-button" data-feature="chatlog">Chatlog feature</span> is a powerful tool that lets you view all previous User-Chatbot conversations (even the links they clicked) to gain deeper insights into their requirements. Available in <span class="twx-plus-button">Plus Plan</span> and <span class="twx-pro-button">Pro Plan</span>.</p>
                <?php
            }?>
        </div>

        <div id="plugininfo" class="twx-chat-tab-content">
            <form method="post" action="options.php">
                <?php
                settings_fields('twx_chat_all_options_group');
                do_settings_sections('twx-chat-plugininfo');
                submit_button();
                ?>
            </form>
        </div>
    </div>
	
    <?php
}


function twxchat_settings_init() {

    // Register all options group and validator
    register_setting('twx_chat_all_options_group','twx_chat_options','twxchat_all_options_validate');

    // General
    add_settings_section('twx_chat_general_section', 'General Settings', 'twxchat_general_section_text', 'twx-chat-general');
    add_settings_field('twx_chat_openai_apikey', '*OpenAI API Key', 'twxchat_openai_apikey_setting', 'twx-chat-general', 'twx_chat_general_section');
    add_settings_field('twx_chat_min_similarity', 'Min. Similarity % (non OpenAI Assistant)', 'twxchat_min_similarity_setting', 'twx-chat-general', 'twx_chat_general_section');
    add_settings_field('twx_chat_google_apikey', 'Google API Key', 'twxchat_google_apikey_setting', 'twx-chat-general', 'twx_chat_general_section');
    add_settings_field('twx_chat_aws_cognito_identity_pool_id', 'AWS Cognito Pool Identity Key', 'twxchat_aws_cognito_identity_pool_id_setting', 'twx-chat-general', 'twx_chat_general_section');

    // Customize
    add_settings_section('twx_chat_customize_section', 'Customize Settings', 'twxchat_customize_section_text', 'twx-chat-customize');
    add_settings_field('twx_chat_starting_comments', 'Default Starting Comments', 'twxchat_starting_comments_setting', 'twx-chat-customize', 'twx_chat_customize_section');
    add_settings_field('twx_chat_frontpage_shortcode', 'Front Page Shortcode', 'twxchat_frontpage_shortcode_setting', 'twx-chat-customize', 'twx_chat_customize_section');
    add_settings_field('twx_chat_custom_css', 'Custom CSS', 'twxchat_custom_css_setting', 'twx-chat-customize', 'twx_chat_customize_section');
    //add_settings_field('twx_chat_custom_js', 'Custom JavaScript', 'twxchat_custom_js_setting', 'twx-chat-customize', 'twx_chat_customize_section');

    // Site Crawler
    add_settings_section('twx_chat_site_crawler_section', 'Site Crawler', 'twxchat_site_crawler_section_text', 'twx-chat-site-crawler');
    add_settings_field('twx_chat_site_crawler_public_posts', 'Crawl Public Posts/Pages', 'twxchat_site_crawler_public_posts_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_private_posts', 'Crawl Private Posts/Pages', 'twxchat_site_crawler_private_posts_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_onsite_media', 'Crawl onsite Media items', 'twxchat_site_crawler_onsite_media_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_sitemap', 'Crawl Sitemap', 'twxchat_site_crawler_sitemap_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_google_drive', 'Crawl Google Drive PDFs', 'twxchat_site_crawler_google_drive_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_update_assistant', 'Update OpenAI Assistant', 'twxchat_site_crawler_update_assistant_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_schedule', 'Schedule Site Crawler', 'twxchat_site_crawler_schedule_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_schedule_dayofweek', 'Schedule Site Crawler Day', 'twxchat_site_crawler_schedule_dayofweek_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_schedule_time', 'Schedule Site Crawler Time', 'twxchat_site_crawler_schedule_time_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');
    add_settings_field('twx_chat_site_crawler_status', 'Site Crawler Last Run', 'twxchat_site_crawler_status_setting', 'twx-chat-site-crawler', 'twx_chat_site_crawler_section');

    // Chatlog
    add_settings_section('twx_chat_chatlog_section', 'Chatlog Viewer', 'twxchat_chatlog_section_text', 'twx-chat-chatlog');

    // Plugin Info
    add_settings_section('twx_chat_plugininfo_section', 'Plugin Info', 'twxchat_plugininfo_section_text', 'twx-chat-plugininfo');
    add_settings_field('twx_chat_plugin_ver', 'Plugin Version', 'twxchat_plugin_ver_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
    add_settings_field('twx_chat_show_watermark', 'Display Powered-by Watermark', 'twxchat_show_watermark_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
    add_settings_field('twx_chat_copy_support_info', 'Copy Support Info', 'twxchat_copy_support_info_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
    if(class_exists('TWXChatPlus')){
        // has paid plan or has had in past
        add_settings_field('twx_chat_twx_customer_key', 'TWX-Chat Customer Key', 'twxchat_twx_customer_key_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
        add_settings_field('twx_chat_available_updates', 'Available Updates', 'twxchat_available_updates_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
    } else {
        add_settings_field('twx_chat_upgrade_promo', 'Upgrade Now!', 'twxchat_upgrade_promo_setting', 'twx-chat-plugininfo', 'twx_chat_plugininfo_section');
    }

}
add_action('admin_init', 'twxchat_settings_init');


function twxchat_general_section_text() {
    echo '<p>Enter your general settings below. <span class="twx-chat-feature-button" data-feature="first_time_setup">First-Time setup instructions here</span></p>';
    // 
}

function twxchat_site_crawler_section_text() {
    echo '<p>Enter your Site Crawler settings below:</p>';
}

function twxchat_customize_section_text() {
    echo '<p>Enter your Customize settings below:</p>';
}

function twxchat_plugininfo_section_text() {
    echo '<p>Plugin information and settings:</p>';
}

function twxchat_plugininfo_show_update_now($new_version, $plan = 'free'){
    $plugin_slug = plugin_basename(__FILE__);
    $plan = ucwords($plan);
    echo '<div class="notice notice-warning">';
    echo "<p><strong>Update Available:</strong> TWX-Chat version ".esc_html($new_version)." (".esc_html($plan)." Plan)</p>";
    echo '<p><a href="' . esc_url(wp_nonce_url(self_admin_url("update.php?action=upgrade-plugin&plugin=$plugin_slug"), 'upgrade-plugin_' . $plugin_slug)) . '" class="button button-primary">Update Now</a></p>';
    echo '</div>';
}

// Field callbacks
function twxchat_csv_upload_setting() {
    echo '<input type="file" name="twx_chat_csv_file" />';
}

function twxchat_section_text() {
    echo '<p>Enter your settings below:</p>';
}

function twxchat_plugin_ver_setting() {
    $pluginInfo = TWXChatUtil::getPluginInfo();
    echo esc_html($pluginInfo['Version']) . '&nbsp;&nbsp;<span class="twx-' . esc_attr($pluginInfo['Plan']) . '-button">' . esc_html(ucwords($pluginInfo['Plan'])) . '</span>';
}

function twxchat_show_watermark_setting(){
    $options = get_option('twx_chat_options');
    $value = isset($options['show_watermark']) ? $options['show_watermark'] : 1;
    echo '<input type="checkbox" id="twx_chat_show_watermark_chk" name="twx_chat_options[show_watermark]" value="1" ' . checked(1, $value, false) . '>'
        .'<p class="description">Check this ON if you like our plugin and you want to allow your visitors to find us. It will show an unobtrusive link below the Chatbot. Checking this OFF will remove the watermark, and that\'s totally fine also :)</p>';
}

function twxchat_twx_customer_key_setting() {
    $options = get_option('twx_chat_options');
    $type = 'text';
    $description = '';
    $style = '';
    $url = TWXChatUtil::getTwxlabsUrl();
    if(isset($options['twx_customer_key']) && !empty($options['twx_customer_key'])){
        $type = 'password';
        if(get_option('twx_chat_customer_valid',false)){
            $plan = get_option('twx_chat_customer_plan','');
            $plan = $plan ? ' ('.ucwords($plan).' Plan)' : '';
            $description = 'Valid Customer Key'.$plan;
            $style = 'color:green;';
        }else{
            $validity_message = get_option('twx_chat_customer_valid_message','').'.';
            $description = 'Invalid Customer Key. '. esc_html($validity_message) . ' Obtain or Renew from <a href="'.esc_url($url).'" target="_blank">'.esc_url($url).'</a>';
            $style = 'color:red;';
        }
    }else{
        $description = 'You are currently on the Free Plan. Upgrade to TWX-Chat Plus or Pro at <a href="'.TWXChatUtil::getTwxlabsUrl(false).'" target="_blank">'.esc_url(TWXChatUtil::getTwxlabsUrl(false)).'</a>';
    }

    echo "<input id='twx_chat_twx_customer_key' name='twx_chat_options[twx_customer_key]' size='40' type='".esc_attr($type)."' value='" . esc_attr($options['twx_customer_key'] ?? '') . "' />&nbsp;&nbsp;".esc_html(TWXChatUtil::obscureApiKey($options['twx_customer_key'] ?? ''),'twx-').'<p class="description" style="'.esc_attr($style).'">'.wp_kses_post($description).'</p>';
}

function twxchat_copy_support_info_setting(){
    $description = 'In the event of plugin errors or failures, TWXLabs support may require you to click this button which copies support information to the clipboard. You can then paste this into an email and send to <a href="'.esc_url(TWXChatUtil::getTwxlabsUrl().'contact-us').'">TWXLabs support</a>.';
    echo wp_kses_post('<button id="twx_chat_copy_support_info_btn" class="button button-primary">Copy Support Info to Clipboard</button><p class="description">'.$description.'</p>');
}

function twxchat_available_updates_setting(){
    echo '<button id="twx_chat_check_for_updates_btn" class="button button-primary">Check for Updates</button>&nbsp;&nbsp;<button id="twx_chat_update_plugin_now_btn" class="button button-primary" style="display:none;">Update Now!</button>&nbsp;&nbsp;<button id="twx_chat_update_plugin_zip_btn" class="button button-primary" style="display:none;">Download ZIP</button>';
}

function twxchat_upgrade_promo_setting(){
    $html = wp_kses_post('
    <div class="twx-upgrade-notice">
        <h3>🚀 You’re on the <strong>Free Plan</strong></h3>
        <p>Unlock advanced features with <span class="twx-plus-button">Plus</span> or 
        <span class="twx-pro-button">Pro</span>:</p>
        <ul class="twx-promo-style-1">
            <li>Customer Chat Logs</li>
            <li>More Chatbots and Avatars</li>
            <li>Collect user fields</li>
            <li>Google Drive integration</li>
            <li>Priority Support</li>
            <li>... And Much More</li>
        </ul>
        <p><a href="'.esc_url(TWXChatUtil::getTwxlabsUrl().'features').'" target="_blank" class="twx-promo-style-2">🔓 Features & Pricing</a></p>
    </div>');
    echo $html;
}

function twxchat_openai_apikey_setting() {
    $options = get_option('twx_chat_options');
    $description = '<p class="description">Obtain an OpenAI API Key from <span class="twx-chat-feature-button" data-feature="openai_api_key">here</span>';
    echo "<input id='twx_chat_openai_apikey' name='twx_chat_options[openai_apikey]' size='40' type='password' value='" . esc_attr($options['openai_apikey'] ?? '') . "' />&nbsp;&nbsp;" . esc_html(TWXChatUtil::obscureApiKey($options['openai_apikey'] ?? '')) . wp_kses_post($description);
}

function twxchat_min_similarity_setting() {
    $options = get_option('twx_chat_options');
    echo "<input id='twx_chat_min_similarity' name='twx_chat_options[min_similarity]' type='number' min='50' max='99' value='" . esc_attr($options['min_similarity'] ?? TWXCHAT_DEFAULT_MIN_SIMILARITY) . "' /><p class='description'>The Default Minimum Similarity % that a User's query needs to match the Knowledge Base item for that item to be used as context in the Chatbot's response. The lower the value the more KB items will be matched however they may be less relevant. The higher the value, the less KB items will be matched, but the more relevant. Enter a value between 50 and 99. The recommended value is around ".TWXCHAT_DEFAULT_MIN_SIMILARITY.". This value can be overridden in each Chatbot.</p>";
}

function twxchat_google_apikey_setting() {
    $options = get_option('twx_chat_options');
	$type = isset($options['google_apikey']) && !empty($options['google_apikey']) ? 'password' : 'text' ;
    $note = TWXChatUtil::customerPlanAccess('plus') ? '' : ' Note this feature is available in the <span class="twx-pro-button">Pro Plan</span>.';
    echo "<input id='twx_chat_google_apikey' name='twx_chat_options[google_apikey]' size='40' type='" . esc_attr($type) . "' value='" . esc_attr($options['google_apikey'] ?? '') . "' />&nbsp;&nbsp;" . esc_html(TWXChatUtil::obscureApiKey($options['google_apikey'] ?? '')) . "<p class='description'>Enter Google API Key if you want Site Crawler to crawl Google Drive docs." . wp_kses_post($note) . "</p>";
}

function twxchat_aws_cognito_identity_pool_id_setting() {
    $options = get_option('twx_chat_options');
	$type = isset($options['aws_cognito_identity_pool_id']) && !empty($options['aws_cognito_identity_pool_id']) ? 'password' : 'text' ;
    echo '<input id="twx_chat_aws_cognito_identity_pool_id" name="twx_chat_options[aws_cognito_identity_pool_id]" size="40" type="' . esc_attr($type) . '" value="' . esc_attr($options['aws_cognito_identity_pool_id'] ?? '') . '" />&nbsp;&nbsp;' . esc_html(TWXChatUtil::obscureApiKey($options['aws_cognito_identity_pool_id'] ?? '')) . '<p class="description">Enter AWS Cognito Identity Pool ID if you want to use AWS neural voices for Avatar Chatbots.<br/>
          Instructions for getting this key can be found <span class="twx-chat-feature-button" data-feature="aws_cognito_identity_pool_id">here</span></p>';
}

function twxchat_starting_comments_setting() {
    $options = get_option('twx_chat_options');
    echo "<textarea id='twx_chat_starting_comments' name='twx_chat_options[starting_comments]' rows='5' cols='50'>" . esc_textarea($options['starting_comments'] ?? '') . "</textarea>";
}
function twxchat_frontpage_shortcode_setting() {
    $options = get_option('twx_chat_options');
    echo "<textarea id='twx_chat_frontpage_shortcode' name='twx_chat_options[frontpage_shortcode]' rows='2' cols='100'>" . esc_textarea($options['frontpage_shortcode'] ?? '') . "</textarea><p class='description'>Shortcode for chatbot that will be displayed on the front page, must be style=\"floating\"</p>";
}
function twxchat_custom_css_setting() {
    $options = get_option('twx_chat_options');
    echo "<textarea id='twx_chat_custom_css' name='twx_chat_options[custom_css]' rows='10' cols='100'>" . esc_textarea($options['custom_css'] ?? '') . "</textarea>";
}
/*
function twxchat_custom_js_setting() {
    $options = get_option('twx_chat_options');
    echo "<textarea id='twx_chat_custom_js' name='twx_chat_options[custom_js]' rows='10' cols='100'>" . esc_textarea($options['custom_js'] ?? '') . "</textarea>";
}
*/
function twxchat_site_crawler_public_posts_setting() {
    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_public_posts']) ? $options['site_crawler_public_posts'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_public_posts_chk" name="twx_chat_options[site_crawler_public_posts]" value="1" ' . checked(1, $value, false) . '>';
}

function twxchat_site_crawler_private_posts_setting() {
    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_private_posts']) ? $options['site_crawler_private_posts'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_private_posts_chk" name="twx_chat_options[site_crawler_private_posts]" value="1" ' . checked(1, $value, false) . '>';
}

function twxchat_site_crawler_onsite_media_setting() {
    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_onsite_media']) ? $options['site_crawler_onsite_media'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_onsite_media_chk" name="twx_chat_options[site_crawler_onsite_media]" value="1" ' . checked(1, $value, false) . '>&nbsp;&nbsp;<span class="description">Note: only Media items with either Alt Text, Captions or Descriptions are crawled</span>';
}

function twxchat_site_crawler_sitemap_setting() {

    if(!TWXChatUtil::customerPlanAccess('pro')){
    	echo '<p class="description">Available in <span class="twx-pro-button">Pro Plan</span></p>';
        return;
    }

    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_sitemap']) ? $options['site_crawler_sitemap'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_sitemap_chk" name="twx_chat_options[site_crawler_sitemap]" value="1" ' . checked(1, $value, false) . '>';
	echo "&nbsp;<input id='twx_chat_site_crawler_start_url' name='twx_chat_options[site_crawler_start_url]' size='40' type='text' value='" . esc_attr($options['site_crawler_start_url'] ?? '') . "' /><p style='margin-left:30px' class='description'>eg: https://yoursite.com.au/sitemap.xml</p>";
}

function twxchat_site_crawler_google_drive_setting() {

    if(!TWXChatUtil::customerPlanAccess('pro') || !method_exists('TWXChatPro', 'gdrive_get_folder_name')){
    	echo '<p class="description">Available in <span class="twx-pro-button">Pro Plan</span></p>';
        return;
    }

    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_google_drive']) ? $options['site_crawler_google_drive'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_google_drive_chk" name="twx_chat_options[site_crawler_google_drive]" value="1" ' . checked(1, $value, false) . '>';

	$folder_id = $options['site_crawler_gdrive_folder_id'] ?? '';
	$folder_name = '';
	if(!empty($folder_id)){
        $folder_name = TWXChatPro::gdrive_get_folder_name($folder_id);
		if($folder_name === false) $folder_name = 'Invalid Google Drive folder ID or invalid Google API Key in General tab';
	}
	echo "&nbsp;&nbsp;<input id='twx_chat_site_crawler_gdrive_folder_id' name='twx_chat_options[site_crawler_gdrive_folder_id]' size='40' type='text' value='" . esc_attr($options['site_crawler_gdrive_folder_id'] ?? '') . "' />&nbsp;&nbsp;" . esc_html($folder_name) . "<p style='margin-left:30px' class='description'>Google Drive Folder ID to crawl. NOTE: Currently only PDF files are crawled.</p>";
}

function twxchat_site_crawler_update_assistant_setting() {
    $options = get_option('twx_chat_options');
    $value = isset($options['site_crawler_update_assistant']) ? $options['site_crawler_update_assistant'] : 0;
    echo '<input type="checkbox" id="twx_chat_site_crawler_update_assistant_chk" name="twx_chat_options[site_crawler_update_assistant]" value="1" ' . checked(1, $value, false) . '>';
	echo "&nbsp;<input id='twx_chat_site_crawler_assistant_id' name='twx_chat_options[site_crawler_assistant_id]' size='40' type='text' value='" . esc_attr($options['site_crawler_assistant_id'] ?? '') . "' /><p style='margin-left:30px' class='description'>OpenAI Assistant ID. Must belong to the OpenAI API Key specified in the General tab</p>";
}

function twxchat_site_crawler_schedule_setting() {
    $options = get_option('twx_chat_options');
    $schedule = $options['site_crawler_schedule'] ?? '';
    $running = get_option('twx_chat_site_crawler_running',false);
    ?>
    <select id="twx_chat_site_crawler_schedule" name="twx_chat_options[site_crawler_schedule]">
        <option value="" <?php selected($schedule, ''); ?>>Select...</option>
        <!-- <option value="Now" <?php selected($schedule, 'Now'); ?>>Now</option> -->
        <option value="Daily" <?php selected($schedule, 'Daily'); ?>>Daily</option>
        <option value="Weekly" <?php selected($schedule, 'Weekly'); ?>>Weekly</option>
    </select>
    <button class="button button-primary" data-running="<?php echo esc_attr($running); ?>" id="twx_chat_site_crawler_crawl_now_btn">Crawl Now!</button>&nbsp;&nbsp;
    <button class="button button-primary" id="twx_chat_site_crawler_cancel_crawl_btn" disabled>Cancel Crawl</button>
    <?php
}

function twxchat_site_crawler_schedule_time_setting() {
    $options = get_option('twx_chat_options');
    echo "<input id='twx_chat_site_crawler_schedule_time' name='twx_chat_options[site_crawler_schedule_time]' type='time' value='" . esc_attr($options['site_crawler_schedule_time'] ?? '') . "' />";
}

function twxchat_site_crawler_schedule_dayofweek_setting() {
    $options = get_option('twx_chat_options');
    $schedule_dayofweek = $options['site_crawler_schedule_dayofweek'] ?? '';
    ?>
    <select id="twx_chat_site_crawler_schedule_dayofweek" name="twx_chat_options[site_crawler_schedule_dayofweek]">
        <option value="" <?php selected($schedule_dayofweek, ''); ?>>Select...</option>
        <option value="Monday" <?php selected($schedule_dayofweek, 'Monday'); ?>>Monday</option>
        <option value="Tuesday" <?php selected($schedule_dayofweek, 'Tuesday'); ?>>Tuesday</option>
        <option value="Wednesday" <?php selected($schedule_dayofweek, 'Wednesday'); ?>>Wednesday</option>
        <option value="Thursday" <?php selected($schedule_dayofweek, 'Thursday'); ?>>Thursday</option>
        <option value="Friday" <?php selected($schedule_dayofweek, 'Friday'); ?>>Friday</option>
        <option value="Saturday" <?php selected($schedule_dayofweek, 'Saturday'); ?>>Saturday</option>
        <option value="Sunday" <?php selected($schedule_dayofweek, 'Sunday'); ?>>Sunday</option>
    </select>
    <?php
}

function twxchat_site_crawler_status_setting() {

    echo '<div id="twx_chat_site_crawler_log"><ul>';
	$logFile = TWXChatUtil::getUploadsDir('spider_results').'twxchatspider_progress.txt';
	if(file_exists($logFile)){
		$logLines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
		foreach($logLines as $line){
			echo '<li>' . esc_html($line) . '</li>';
		}
	}
    echo '</ul></div>';
}

function twxchat_settings_error_exists($error_code){

    // Fetch existing settings errors
    $existing_errors = get_settings_errors('twx_chat_options');
    
    // Check if the specific error already exists
    if (!empty($existing_errors)) {
        foreach ($existing_errors as $error) {
            if ($error['code'] === $error_code) {
                return true;
            }
        }
    }

    return false;
}

function twxchat_remove_settings_error($error_code){
    $errors = get_settings_errors('twx_chat_options');
    if ($errors) {
        foreach ($errors as $key => $error) {
            if ($error['code'] === $error_code) {
                unset($errors[$key]);
            }
        }
        // Re-register the cleaned-up errors
        global $wp_settings_errors;
        $wp_settings_errors = $errors;
    }    
}

function twxchat_updateOption($opt_name, &$input, &$options, &$new_input, $default_val = '{NOT_SUPPLIED}'){
    // Updates $new_input and returns true if changed from prev

    if(isset($input[$opt_name])){

        // User entered value
        $val = $input[$opt_name];
        $new_input[$opt_name] = is_string($val) ? sanitize_text_field($val) : $val;
        // Check if changed
        if(isset($options[$opt_name]) && $val != $options[$opt_name]){
            return true;
        }else{
            return false;
        }

    }else{

        // no user value, use prev option
        if(isset($options[$opt_name])){
            $new_input[$opt_name] = $options[$opt_name];
            return false;
        }else{
            if($default_val !== '{NOT_SUPPLIED}') {
                $new_input[$opt_name] = $default_val;
                return true;
            }else{
                return false;
            }
        }
    }
}

function twxchat_options_hasChangedVal($optName, &$options, &$input){
    $oldVal = $options[$optName] ?? null;
    $newVal = $input[$optName] ?? null;
    return $oldVal !== $newVal;
}

function twxchat_get_admin_tab_by_options($options){
    if(isset($options['openai_apikey'])) return 'general';
    if(isset($options['site_crawler_schedule'])) return 'site-crawler';
    if(isset($options['custom_css'])) return 'customize';
    if(isset($options['twx_customer_key'])) return 'plugininfo';
    return false;
}

function twxchat_all_options_validate($input) {

	// Validate all Admin settings. Need to put all options (all tabs) in here otherwise values won't be saved

    $options = get_option('twx_chat_options', []);

    $tab = twxchat_get_admin_tab_by_options($input);

    // Initial Merge existing options into validated output
    $new_input = $options;

    switch($tab){
        case 'general':
            twxchat_validate_general_options($input, $options, $new_input);
            break;
        case 'customize':
            twxchat_validate_customize_options($input, $options, $new_input);
            break;
        case 'site-crawler':
            twxchat_validate_site_crawler_options($input, $options, $new_input);
            break;
        case 'plugininfo':
            twxchat_validate_plugininfo_options($input, $options, $new_input);
            break;
    }

    return $new_input;
}

function twxchat_validate_general_options(&$input, &$options, &$new_input){

    // Validate 'General' options - just set $new_input values as needed

    // OpenAI API Key - mandatory
    $error_code = 'twx_chat_openai_apikey_error';
    $optName = 'openai_apikey';
    twxchat_updateOption($optName, $input, $options, $new_input, '');
    if(empty($new_input[$optName])){
        if(!twxchat_settings_error_exists($error_code)){
            add_settings_error(
                'twx_chat_options',
                $error_code,
                __('OpenAI API Key is required.', 'twx-chat'),
                'error'
            );
        }
    }else{
        twxchat_remove_settings_error($error_code);
    }

    // Min Similarity
    if (isset($input['min_similarity'])) {
        $value = intval($input['min_similarity']);
        if ($value < 50) {
            $value = 50; // Minimum allowed value
        } elseif ($value > 99) {
            $value = 99; // Maximum allowed value
        }
        $new_input['min_similarity'] = $value;
    } else {
		$new_input['min_similarity'] = $options['min_similarity'] ?? TWXCHAT_DEFAULT_MIN_SIMILARITY;
	}

	// Google API Key
    twxchat_updateOption('google_apikey', $input, $options, $new_input);

    // AWS Congnito Identity Pool ID
    twxchat_updateOption('aws_cognito_identity_pool_id', $input, $options, $new_input);

}

function twxchat_validate_customize_options(&$input, &$options, &$new_input){

    // Validate 'Customize' options - just set $new_input values as needed

	$new_input['starting_comments'] = isset($input['starting_comments']) ? sanitize_textarea_field($input['starting_comments']) : $options['starting_comments'];
	$new_input['custom_css'] = isset($input['custom_css']) ? TWXChatUtil::sanitizeCSS($input['custom_css']) : $options['custom_css'];
	//$new_input['custom_js'] = isset($input['custom_js']) ? TWXChatUtil::sanitizeJS($input['custom_js']) : $options['custom_js'];

    $error_code = 'twx_chat_frontpage_shortcode_validation_error';
	if(isset($input['frontpage_shortcode'])){
        if(twxchat_validate_frontpage_shortcode($input['frontpage_shortcode'])){
            $new_input['frontpage_shortcode'] = $input['frontpage_shortcode'];
        }else{
            if(!twxchat_settings_error_exists($error_code)){
                add_settings_error(
                    'twx_chat_options',
                    $error_code,
                    __('Invalid Front Page Shortcode. Ensure ID exists and style="floating" in Chatbots.', 'twx-chat'),
                    'error'
                );
            }
        }
    }else{
        $new_input['frontpage_shortcode'] = $options['frontpage_shortcode'];
    }

}

function twxchat_validate_site_crawler_options(&$input, &$options, &$new_input){

    // Validate 'Site Crawler' options - just set $new_input values as needed

    // Checkbox options are handled differently as their input will be missing if unchecked
    $new_input['site_crawler_public_posts'] = isset($input['site_crawler_public_posts']) ? '1' : '0';
    $new_input['site_crawler_private_posts'] = isset($input['site_crawler_private_posts']) ? '1' : '0';
    $new_input['site_crawler_onsite_media'] = isset($input['site_crawler_onsite_media']) ? '1' : '0';
    $new_input['site_crawler_sitemap'] = isset($input['site_crawler_sitemap']) ? '1' : '0';

    twxchat_updateOption('site_crawler_start_url', $input, $options, $new_input);
    $error_code = 'twx_chat_site_crawler_start_url_not_exists';
    if($new_input['site_crawler_sitemap'] == '1' && isset($new_input['site_crawler_start_url']) && !empty($new_input['site_crawler_start_url']) && !TWXChatUtil::urlExists($new_input['site_crawler_start_url'])){
        if(!twxchat_settings_error_exists($error_code)){
            add_settings_error(
                'twx_chat_options',
                $error_code,
                __("Sitemap URL does not exist.", 'twx-chat'),
                'error'
            );
        }
    }else{
        twxchat_remove_settings_error($error_code);
    }

    $new_input['site_crawler_google_drive'] = isset($input['site_crawler_google_drive']) ? '1' : '0';
    $optName = 'site_crawler_gdrive_folder_id';
    if(twxchat_updateOption($optName, $input, $options, $new_input)){
        $new_input[$optName] = str_ireplace('https://drive.google.com/drive/u/1/folders/', '', $new_input[$optName]); //isset($input['site_crawler_gdrive_folder_id']) ? sanitize_text_field($input['site_crawler_gdrive_folder_id']) : $options['site_crawler_gdrive_folder_id']);
    }
    $error_code = 'twx_chat_site_crawler_gdrive_folder_id_missing_error';
    if(!empty($new_input['site_crawler_google_drive']) && empty($new_input['site_crawler_gdrive_folder_id'])){
        if(!twxchat_settings_error_exists($error_code)){
            add_settings_error(
                'twx_chat_options',
                $error_code,
                __('Google Driver Folder ID is required if selected to Crawl.', 'twx-chat'),
                'error'
            );
        }
    }else{
        twxchat_remove_settings_error($error_code);
    }

    $new_input['site_crawler_update_assistant'] = isset($input['site_crawler_update_assistant']) ? '1' : '0';
    twxchat_updateOption('site_crawler_assistant_id', $input, $options, $new_input);
    $error_code = 'twx_chat_site_crawler_assistant_id_missing_error';
    if(!empty($new_input['site_crawler_update_assistant']) && empty($new_input['site_crawler_assistant_id'])){
        if(!twxchat_settings_error_exists($error_code)){
            add_settings_error(
                'twx_chat_options',
                $error_code,
                __('OpenAI Assistant ID is required if selected to Update.', 'twx-chat'),
                'error'
            );
        }
    }else{
        twxchat_remove_settings_error($error_code);
    }

    twxchat_updateOption('site_crawler_schedule', $input, $options, $new_input, '');
    twxchat_updateOption('site_crawler_schedule_dayofweek', $input, $options, $new_input, '');
    twxchat_updateOption('site_crawler_schedule_time', $input, $options, $new_input, '');

    // validate schedule settings
    $error_code = 'twx_chat_site_crawler_schedule_time_error';
    if($new_input['site_crawler_schedule'] == 'Daily' || $new_input['site_crawler_schedule'] == 'Weekly'){
        // ensure time is set. Filter 'update_option_twxchat_options' will also be triggered which will set schedule if needed
        if(!isset($new_input['site_crawler_schedule_time']) || empty($new_input['site_crawler_schedule_time'])){
            if(!twxchat_settings_error_exists($error_code)){
                add_settings_error(
                    'twx_chat_options',
                    $error_code,
                    __('Site Crawler Schedule time is required.', 'twx-chat'),
                    'error'
                );
            }
        }else{
            twxchat_remove_settings_error($error_code);
        }
    }else{
        twxchat_clear_all_site_crawler_scheduled_jobs();
        twxchat_remove_settings_error($error_code);
    }

    $error_code = 'twx_chat_site_crawler_schedule_dayofweek_error';
    if($new_input['site_crawler_schedule'] == 'Weekly'){
        // ensure DOW set (and time from above)
        if(!isset($new_input['site_crawler_schedule_dayofweek']) || empty($new_input['site_crawler_schedule_dayofweek'])){
            if(!twxchat_settings_error_exists($error_code)){
                add_settings_error(
                    'twx_chat_options',
                    $error_code,
                    __('Site Crawler Schedule Day Of Week is required.', 'twx-chat'),
                    'error'
                );
            }
        }else{
            twxchat_remove_settings_error($error_code);
        }
    }else{
        twxchat_remove_settings_error($error_code);
    }

	if(isset($input['site_crawler_status'])){
		$new_input['site_crawler_status'] = $input['site_crawler_status'];
	}else{
		$new_input['site_crawler_status'] = [];
	}

}

function twxchat_validate_plugininfo_options(&$input, &$options, &$new_input){

    // Validate 'plugininfo' options - just set $new_input values as needed

    $new_input['show_watermark'] = isset($input['show_watermark']) ? 1 : 0; // checkbox values wont exist at all if they are unchecked, otherwise they are checked

    // TWX Customer Key
    if(class_exists('TWXChatPlus')){
        $optName = 'twx_customer_key';
        if(twxchat_updateOption($optName, $input, $options, $new_input, '')){
            // on change, verify customer
            TWXChatPlus::verifyCustomer($input[$optName]);
        }
    }

}


function twxchat_validate_frontpage_shortcode($shortcode_def){

    if(empty($shortcode_def)) return true;

    // ensure shortcode is valid for global use
    preg_match_all('/\[twxchat(.*?)\]/', $shortcode_def, $matches);
    foreach ($matches[1] as $shortcode_atts) {
        // Parse the attributes
        $atts = shortcode_parse_atts($shortcode_atts);
        if(isset($atts['id']) && !empty($atts['id'])){
            $atts = TWXChatUtil::getChatbotAttributesById($atts,true);
        }
        if(isset($atts['style']) && $atts['style'] === 'floating') return true;
    }
    return false;
}

// Shortcodes
function twxchat_shortcode($atts) {
    global $twxchat_chatbot_rendered;
    $twxchat_chatbot_rendered = true;

    // Load atts from saved chatbot id if exists
    if (isset($atts['id']) && !empty($atts['id'])) {
        $atts = TWXChatUtil::getChatbotAttributesById($atts, true);
    }else{
        // otherwise merge with all attributes and their defaults
        $atts = TWXChatUtil::mergeChatbotAttributesWithAll($atts);
    }

	// Set attribute defaults if empty
	$options = get_option('twx_chat_options');
	if(empty($atts['thread_id']) && empty($atts['assistant_id'])) $atts['thread_id'] = twxchat_generate_assistantless_thread_id();
	if(empty($atts['min_similarity'])) $atts['min_similarity'] = $options['min_similarity'];
	if(empty($atts['starting_comments'])) $atts['starting_comments'] = $options['starting_comments'];
	
	$topDiv = '<div id="twxchat-container"';
    $exclude = ['extra_instructions', 'starting_comments'];
	foreach($atts as $attr => $val){
        if(in_array($attr, $exclude)) continue;
		$newAttr = 'data-'.str_replace('_','-',$attr);
        if(is_array($val)) $val = json_encode($val);
        $topDiv .= ' '.$newAttr.'="'.htmlspecialchars(($val ?? ''), ENT_QUOTES, 'UTF-8').'"';
	}

    // detect if user logged in and can edit pages
    if (is_user_logged_in() && current_user_can('edit_posts')) {
		$topDiv .= ' data-user-can-edit="true"';
    }
	$topDiv .= '>';

	$type = $atts['type'] ?? '';
    if(!$type) return 'Missing twxchat shortcode attribute: type';

    $style = $atts['style'] ?? '';
    if(!$style) return 'Missing twxchat shortcode attribute: style';

    if($type === 'avatar'){
        if($style === 'embedded'){
            $inner_html = twx_get_avatar_container_html($atts);
            return $topDiv.'<div id="twx-chat-embed">'.$inner_html.'</div></div>';
        }else{
            return $topDiv.twx_get_chatbot_minimized_html($atts).'</div>';
        }
    }else{
        if($style === 'embedded'){
            $inner_html = twx_get_chatbot_container_html($atts);
            return $topDiv.'<div id="twx-chat-embed">'.$inner_html.'</div></div>';
        }elseif($style === 'floating'){
            return $topDiv.twx_get_chatbot_minimized_html($atts).'</div>';
        }else{
            return 'Invalid twxchat shortcode attributes';
        }
    }

}
add_shortcode('twxchat', 'twxchat_shortcode');

function twxchat_render_shortcode_in_footer() {
    global $twxchat_chatbot_rendered;
    if($twxchat_chatbot_rendered) return; // Chatbot already rendered

    // after footer rendered, check if we want to append floating frontpage chatbot
    $options = get_option('twx_chat_options');
    $frontpage_shortcode = $options['frontpage_shortcode'] ?? '';
    if($frontpage_shortcode && is_front_page()){
        echo do_shortcode($frontpage_shortcode);
    }
}
add_action('wp_footer', 'twxchat_render_shortcode_in_footer', 20);

function twxchat_generate_assistantless_thread_id(){

	// Define the characters to be used in the random string
	$length = 15;
	$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
	$charactersLength = strlen($characters);
	$randomString = '';
	
	// Generate the random string
	for ($i = 0; $i < $length; $i++) {
		$randomString .= $characters[wp_rand(0, $charactersLength - 1)];
	}
	
	return 'twxchat_thread_'.$randomString;
}

function twxchat_enqueue_scripts() {

    global $post;

    // ensure shortcode exists on page
    $render = false;
    $options = get_option('twx_chat_options');
    if(is_front_page()){
        if(!empty($options['frontpage_shortcode'] ?? '')) $render = true;
    }else if(is_page() || is_single()){
        if(has_shortcode($post->post_content, 'twxchat')) $render = true;
    }
    if(!$render) return;

    // Get Avatar options
    $is_avatar = false;
    $avatar_bkg_url = '';

    $atts = TWXChatUtil::getChatbotAttributesFromPost($post);
    if(empty($atts) && !empty($options['frontpage_shortcode'] ?? '')){
        // Try Frontpage Shortcode
        $id = '';
        if (preg_match('/\[twxchat\s+id=["\']?(\d+)["\']?\]/', $options['frontpage_shortcode'], $matches)) {
            $id = (int) $matches[1];
        }
        $atts = TWXChatUtil::getChatbotAttributesById(['id'=>$id], true);
    }

    if(isset($atts['id']) && !empty($atts['id'])){
        $atts = TWXChatUtil::getChatbotAttributesById($atts,false);
        if(isset($atts['type']) && $atts['type'] === 'avatar') $is_avatar = true;
        if(isset($atts['avatar_background_url']) && !empty($atts['avatar_background_url'])) $avatar_bkg_url = $atts['avatar_background_url'];

    }else{
        // Non-ID Chatbot. If the shortcode with the 'avatar_character' attribute is found, set flag
        preg_match_all( '/\[twxchat[^]]*avatar_character=["\'][^"\']*["\'][^]]*\]/', $post->post_content, $matches );
        $is_avatar = !empty($matches[0]);

        $matches = [];
        preg_match_all('/\[twxchat[^]]*avatar_background_url=["\']([^"\']+)["\'][^]]*\]/', $post->post_content, $matches);
        if (!empty($matches[1])) {
            $avatar_bkg_url = $matches[1][0]; // Get the first match
        }
    }

    wp_enqueue_script('twx_chat_script', plugins_url('assets/js/twx-chat.js', __FILE__), array('jquery'), '1.0', true);
	$opts = [
		'ajax_url' =>  admin_url('admin-ajax.php'),
		'assets_path' => plugins_url('assets', __FILE__),
        'avatar_assets_path' => TWXChatUtil::getAvatarAssetsRootPath('baseurl'),
		'images_path' => plugins_url('assets/images', __FILE__),
		'mp3_output_path' => wp_upload_dir()['baseurl'].'/twx-chat/mp3',
		'google_drive_upload_path' => wp_upload_dir()['baseurl'].'/twx-chat/google_drive',
        'stream_handler' => plugins_url('stream_handler.php', __FILE__),
        'test_stream_server' => plugins_url('test_stream_server.php', __FILE__),
        'openai_tts_handler' => plugins_url('openai_tts_handler.php', __FILE__),
        'nonce' => wp_create_nonce('twx_chat_nonce'),
	];

    if($avatar_bkg_url) $opts['avatar_background_url'] = $avatar_bkg_url;

    wp_localize_script('twx_chat_script', 'twxChat', $opts);

    // If Plus Plan
    if(TWXChatUtil::customerPlanAccess('plus')){
        wp_enqueue_script('twx_chat_plus_script', plugins_url('assets/js/twx-chat-plus.js', __FILE__), array('jquery'), '1.0', true);
    }

    // If Pro Plan
    if(TWXChatUtil::customerPlanAccess('pro')){
        wp_enqueue_script('twx_chat_pro_script', plugins_url('assets/js/twx-chat-pro.js', __FILE__), array('jquery'), '1.0', true);
    }

    if($is_avatar){
        wp_enqueue_style('twx_chat_css', plugins_url('assets/css/twx-chat-avatar.css', __FILE__));
    }else{
        wp_enqueue_style('twx_chat_css', plugins_url('assets/css/twx-chat.css', __FILE__));
    }

	// Custom inline CSS
    $custom_css = $options['custom_css'] ?? '';
    if (!empty($custom_css)) {
        // Enqueue a dummy style to attach the inline CSS to
        wp_register_style('twx_chat_custom_css', false); // False URL for a placeholder handle
        wp_enqueue_style('twx_chat_custom_css');

        // Add the inline CSS
        wp_add_inline_style('twx_chat_custom_css', $custom_css);
    }
	
    // Voice Synth classes
    wp_enqueue_script_module('twx_chat_script_voice_stt', plugins_url('assets/js/twx-voice-agent.js', __FILE__), array(), '1.0', false);

	// Add inline custom JavaScript
    /*
	$custom_js = $options['custom_js'] ?? '';
    if (!empty($custom_js)) {
        // Enqueue a dummy script to attach the inline JS to
        wp_register_script('twx_chat_custom_js', '');
        wp_enqueue_script('twx_chat_custom_js');

        // Add the inline JS
        wp_add_inline_script('twx_chat_custom_js', $custom_js); // cannot use escaping here or the javascript code will break/fail. It is santitized on save to the option
    }
    */

    // Avatar Character scripts
    if ($is_avatar) {

        // AWS SDK file for TTS - from https://sdk.amazonaws.com/js/aws-sdk-2.645.0.min.js
        wp_enqueue_script('twx_chat_script_avatar_aws_sdk', plugins_url('assets/js/aws-sdk-2.645.0.min.js', __FILE__), array(), '1.0', false);

        // Avatar HOST file (also contains BABYLON) - compiled from source in D:\dev\avatar\babylon-amazon-sumerian-hosts\packages\amazon-sumerian-hosts-babylon\test\integration_test\Babylon.js\
        wp_enqueue_script('twx_chat_script_avatar_babylon', plugins_url('assets/js/twxchatBabylon.js', __FILE__), array(), '1.0', false);

        // Avatar Main js module file
        wp_enqueue_script_module('twx_chat_script_avatar_main', plugins_url('assets/js/twx-chat-avatar.js', __FILE__), array(), '1.0', false);

    }

}
add_action('wp_enqueue_scripts', 'twxchat_enqueue_scripts');

function twxchat_enqueue_admin_scripts() {

    // Check if we're on twx-chat settings page
    $plugin_page = 'twx-chat-settings';
    if (!isset($_GET['page']) || $_GET['page'] !== $plugin_page) {
        return;
    }

    wp_enqueue_style(
        'twx_chat_admin_css', // Handle for the stylesheet
        plugins_url('assets/css/twx-chat-admin.css', __FILE__), 
        [], // Dependencies, if any
        '1.0.0' // Version number
    );
	
	wp_enqueue_script(
		'twx_chat_admin_script',
		plugins_url('assets/js/twx-chat-admin.js', __FILE__),
		array('jquery'), // dependencies
		'1.0',
		true // load in the footer
	);

    if(TWXChatUtil::customerPlanAccess('plus')){
        $plus_admin_script = 'assets/js/twx-chat-admin-plus.js';
        if(file_exists(plugin_dir_path(__FILE__).$plus_admin_script)){
            wp_enqueue_script(
                'twx_chat_admin_plus_script',
                plugins_url($plus_admin_script, __FILE__),
                array('jquery'), // dependencies
                '1.0',
                true // load in the footer
            );
        }
    }

    if(TWXChatUtil::customerPlanAccess('pro')){
        $plus_admin_script = 'assets/js/twx-chat-admin-pro.js';
        if(file_exists(plugin_dir_path(__FILE__).$plus_admin_script)){
            wp_enqueue_script(
                'twx_chat_admin_pro_script',
                plugins_url($plus_admin_script, __FILE__),
                array('jquery'), // dependencies
                '1.0',
                true // load in the footer
            );
        }
    }
    
    $options = get_option('twx_chat_options');
    $avatars = TWXChatUtil::fetchChatbotAvatars(($options['twx_customer_key'] ?? ''));
    
    $avatars_local = [];
    $thumb_path_rel = "assets/images/avatar-thumbs/";
    $thumb_path_remote = TWXChatUtil::getTwxlabsUrl().'assets/avatar-assets/characters/images/';
    foreach($avatars as $key => $avatar){
        $thumb_file_name = $avatar['images']['thumb'] ?? '';
        if(empty($thumb_file_name)) continue;
        $thumb_src_exists = file_exists(plugin_dir_path(__FILE__).$thumb_path_rel.$thumb_file_name);
        if($thumb_src_exists){
            // Thumb exists locally, use this
            $thumb_src_url = plugin_dir_url(__FILE__).$thumb_path_rel.$thumb_file_name;
            $thumb_src_url = str_replace(site_url(), '', $thumb_src_url);
        }else{
            // Thumb must live remotely, use that
            $thumb_src_url = $thumb_path_remote.$thumb_file_name;
        }
        $avatars_local[$key] = ['thumb_src' => $thumb_src_url];
    }

	$opts = [
		'ajax_url' =>  admin_url('admin-ajax.php'),
		'images_path' => plugins_url('assets/images', __FILE__),
        'twxchat_support_base_url' => TWXChatUtil::getTwxlabsUrl(false),
        'avatars' => $avatars_local,
        'nonce' => wp_create_nonce('twx_chat_nonce'),
	];

    if(TWXChatUtil::customerPlanAccess('plus')){
        $opts['proxy_url'] = plugins_url('twx_proxy.php', __FILE__);
    }

    wp_localize_script('twx_chat_admin_script', 'twxChat', $opts);
		
}
add_action('admin_enqueue_scripts', 'twxchat_enqueue_admin_scripts');

function twx_get_chatbot_minimized_html($atts){

    // Floating chatbot

    $starting_comments = $atts['starting_comments'] ?? 'How Can I Help?';
    $floating_title = $atts['floating_title'] ?? $starting_comments;
    $header = '';
    $main_container = '';
    $top_style = '';
    $avatar_img_min = isset($atts['floating_image_url']) && !empty($atts['floating_image_url']) ? '<img id="twx-chat-avatar-img-min" alt="Chatbot Image" src="'. $atts['floating_image_url'] . '">' : '';

    if($atts['type'] === 'avatar'){
        $main_container = twx_get_avatar_container_html($atts);
    }else{
        $starting_comments_max_div = isset($atts['starting_comments']) && !empty($atts['starting_comments']) ? '<div class="bot-greeting wp-block">'.$atts['starting_comments'].'</div>' : '';
        $main_container = twx_get_chatbot_container_html($atts,false);
        $header = '<div id="twx-chat-floating-header"><button id="minimize-chatbot" class="chat-toggle-button wp-element-button">-</button>'.$starting_comments_max_div.'</div>';
        $top_style = ' style="display:none;"';
    }

    $minimized_container = '
    <div id="twx-chat-minimized">
        '.$avatar_img_min.'
        <div style="display: flex;">
            <div class="chatbot-greeting">'.$floating_title.'</div>
            <button id="expand-chatbot" class="chat-toggle-button wp-element-button">+</button>
        </div>    
    </div>';

    return $minimized_container.
        '<div id="twx-chat-floating"'.$top_style.'">
            '.$header.$main_container.'
        </div>';

}

function twx_get_avatar_container_html($atts){

    $starting_comments = $atts['starting_comments'] ?? 'How Can I Help?';
    $floating_title = $atts['floating_title'] ?? $starting_comments;
    $load_screen_style = isset($atts['avatar_image_url']) && !empty($atts['avatar_image_url']) ? "background-image: url('".$atts['avatar_image_url']."')" : '';
    $icons_path = esc_url(plugins_url('assets/images/tabler-icons',__FILE__));
    $minimize_button = $atts['style'] === 'floating' ? '<button id="floating-avatar-minimize" class="avatar-control-btn"><img src="'.$icons_path.'/minus.svg" alt="Minimize"></button>' : '';
    $options = get_option('twx_chat_options');
    $show_watermark = $options['show_watermark'] ?? '1';
    $watermark = $show_watermark ? '<div id="twx-chat-watermark"><a href="'.TWXChatUtil::getTwxlabsUrl().'" target="_blank">Powered by TWXLabs.au</a></div>' : '';

    $html = '
    <div id="avatar-container">

        <!--Loading screen-->
        <div id="avatar-loadScreen" style="'.$load_screen_style.'">
            <div id="avatar-greeting-loadScreen">'.($atts['style'] === 'embedded' ? $starting_comments : $floating_title).'</div>
            '.$minimize_button.'
            <div id="avatar-start-text"></div>
            <div id="avatar-loader" class="hidden"></div>
            <div id="avatar-loader-progress" class="hidden"></div>
        </div>

        <!-- control buttons -->
        <button id="avatar-status"></button>
        <div id="user-utterance"></div>
        <button id="avatar-greeting-button">Click to start</button>
        <button id="avatar-minimize" class="avatar-control-btn"><img src="'.$icons_path.'/arrows-minimize.svg" alt="Minimize"></button>
        <button id="avatar-maximize" class="avatar-control-btn"><img src="'.$icons_path.'/arrows-maximize.svg" alt="Maximize"></button>
        <button id="avatar-exit" class="avatar-control-btn"><img src="'.$icons_path.'/square-x.svg" alt="Exit"></button>
        <button id="avatar-microphone-on" class="avatar-control-btn microphone"><img src="'.$icons_path.'/microphone.svg" alt="MicON"></button>
        <button id="avatar-microphone-off" class="avatar-control-btn microphone"><img src="'.$icons_path.'/microphone-off.svg" alt="MicOFF"></button>
        <button id="avatar-play" class="avatar-control-btn"><img src="'.$icons_path.'/player-play.svg" alt="Play"></button>
        <button id="avatar-pause" class="avatar-control-btn"><img src="'.$icons_path.'/player-pause.svg" alt="Pause"></button>
        <button id="avatar-camera-on" class="avatar-control-btn camera"><img src="'.$icons_path.'/camera.svg" alt="CamON"></button>
        <button id="avatar-camera-off" class="avatar-control-btn camera"><img src="'.$icons_path.'/camera-off.svg" alt="CamOFF"></button>
        <button id="avatar-conversation-on" class="avatar-control-btn conversation"><img src="'.$icons_path.'/message.svg" alt="ConvON"></button>
        <button id="avatar-conversation-off" class="avatar-control-btn conversation"><img src="'.$icons_path.'/message-off.svg" alt="ConvOFF"></button>
        <button id="avatar-debug-console-on" class="avatar-control-btn debug-console"><img src="'.$icons_path.'/script.svg" alt="ConsoleON"></button>
        <button id="avatar-debug-console-off" class="avatar-control-btn debug-console"><img src="'.$icons_path.'/script-x.svg" alt="ConsoleOFF"></button>

        <!-- Conversation widget -->
        <div id="avatar-conversation-widget" class="chatbot-container">
            <div id="avatar-greeting-conversation">'.$atts['starting_comments'].'</div>
            <div id="avatar-conversation-content" class="chat-messages">
            '.twxchat_common_questions_html($atts['common_questions']).'
            </div>
            <div id="avatar-conversation-footer">
                <input type="text" id="user-input" class="chat-input" placeholder="Enter your query/response here..." disabled>
                <button id="avatar-user-send-btn" class="wp-element-button">Send</button>
            </div>
        </div>

        <!-- Flashcards widget -->
        <div id="avatar-flashcards-widget"></div>

        <!-- Debug console widget -->
        <div id="avatar-debug-console-widget" class="chatbot-container">
            <div id="avatar-debug-console-content" class="chat-messages">
            </div>
        </div>

        <!-- scene canvas will be appended here after load -->

    </div>
    '.$watermark.'
	<!-- Sources -->
	<div id="sourcesContainer" class="sources-container" style="display: none;"></div>
    ';
    
    return $html;
}

function twx_get_chatbot_container_html($atts, $includeStartingComments = true) {
	
	extract($atts); // creates vars from keys in array
	$options = get_option('twx_chat_options');
	$apiKey = $options['openai_apikey'];
	
	$microphone_img_url = esc_url(plugins_url('assets/images/microphone.png', __FILE__));

    $chatbot_container_style = '';

    $cancel_stream_btn_svg = '
        <svg width="30" height="30" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
        <!-- Black background circle -->
        <circle cx="50" cy="50" r="48" fill="black"/>
        <!-- White square (Stop button) -->
        <rect x="25" y="25" width="50" height="50" fill="white"/>
        </svg>
        ';
	
	return '
    <div class="chatbot-container wp-block"'.$chatbot_container_style.'>
		'.twxchat_debugInfo($apiKey, $atts).'
        <!-- Chatbot body -->
        <div class="chatbot-body wp-block">
            '.($includeStartingComments ? '<div class="bot-greeting wp-block">'.$starting_comments.'</div>' : '').'
			'.twxchat_common_questions_html($common_questions).'
            <!-- Previous chat messages -->
            <div class="chat-messages wp-block">
                <!-- Previous messages here -->
            </div>
        </div>
        <!-- Chatbot footer -->
        <div class="chatbot-footer">
            <!-- Chat input field -->
            <input type="text" id="user-input" class="chat-input" placeholder="Enter your query/response here..." disabled>
			<div class="footer-buttons">
				<!-- Send button -->
				<button class="send-button wp-element-button" disabled>Send</button>
				<!-- Voice Input button -->
				<button class="voice-input-button wp-element-button"><img src="'.$microphone_img_url.'" /></button>
				<!-- Cancel Stream button -->
				<button class="cancel-stream-button wp-element-button" disabled>'.$cancel_stream_btn_svg.'</button>
			</div>
        </div>
    </div>
	<!-- Voice Input Dialog -->
	<div id="voice-input-dialog" style="display: none;">
		<div class="voice-input-dialog-content">
			<div class="voice-input-background-effect"></div>
			<div class="voice-input-listening">Listening...</div>
			<button class="cancel-voice-input">Cancel</button>
		</div>
	</div>
	<!-- TTS/Custom Voice Player -->
	<div id="twx-chat-custom-voice-player-container"></div>
	<!-- Popup -->
	<div id="productPopup" class="product-popup" style="display: none;">
		<div class="popup-content">
			<span class="close-button" onclick="closePopup()">&times;</span>
			<img id="productImage" src="" alt="Product Image">
			<h2 id="productDescription"></h2>
			<p id="productPrice"></p>
			<a id="productLink" href="" target="_blank">View Product</a>
		</div>
	</div>
	<!-- Sources -->
	<div id="sourcesContainer" class="sources-container" style="display: none;"></div>
	';
}

function twxchat_common_questions_html($questions) {

	if(empty($questions)) return '';
	
	if(!is_array($questions)) $questions = explode(',',$questions);

	$html = '<div class="common-questions"><h3>Common Questions</h3><ul>';
	
	foreach($questions as $q){
		$html .= '<li><button class="wp-element-button">'.trim($q).'</button></li>';
	}
	
	$html .= '</ul></div>';
	
	return $html;
}

function twxchat_debugInfo($apiKey, $atts){

    extract($atts);
    if(!isset($twx_debug) || $twx_debug !== 'true') return '';
	
	$debugInfoX['API Key'] = TWXChatUtil::obscureApiKey($apiKey);
	$debugInfoX['Thread ID'] = '<span id="twx-debug-threadId">'.$thread_id.'</span>';
	
	if(!empty($assistant_id)){
		
		$asstObj = new TWXChatOpenAIAssistant($apiKey, $assistant_id);
		$assistant = $asstObj->getAssistant();
		$debugInfoX['Assistant ID'] = $assistant_id;
		$debugInfoX['Assistant Name'] = $assistant['name'];
		$debugInfoX['Model'] = '<span id="twx-debug-model">'.$assistant['model'].'</span>'; // this may change if tokens/budget exceeded etc
		$debugInfoX['Temperature'] = $assistant['temperature'];

		$asstFiles = $asstObj->getAssistantFiles();

		if(!empty($asstFiles) && !isset($asstFiles['error'])){
			$i=1;
			foreach($asstFiles as $asstFile){
				$file = $asstObj->getFile($asstFile['id']);
				$debugInfoX["Attached File $i"] = $file['filename'].' ('.number_format($file['bytes']/1024,2).' Kb, created at '.$file['created_at'].')';
				$i++;
			}
		}
	}else{
		// Assistant Less
		$debugInfoX['Model'] = '<span id="twx-debug-model">'.($atts['llm_model'] ?? '').'</span>'; // this will be populated after query
		$debugInfoX['Min.Similarity'] = $min_similarity.'%';
	}
	
	if(!empty($extra_instructions)) $debugInfoX['Extra Instructions'] = $extra_instructions;
	if(!empty($allow_feedback)) $debugInfoX['Allow Feedback'] = $allow_feedback;
	if(!empty($flashcards)) $debugInfoX['Flashcards'] = $flashcards;
	if(!empty($stream)) $debugInfoX['Streaming'] = $stream;
    if(!empty($kb_source)) $debugInfoX['KB source'] = $kb_source .' ('.twxchat_get_source_count($kb_source).' sources)';
	
	$content = 'Debug Info:<br/>';
	foreach($debugInfoX as $key => $value){
		$content .= "<li>$key: <b>$value</b></li>";
	}
	
	return '<div class="twx-debug">'.$content.'</div>';
}

function twxchat_get_source_count($source) {
    global $wpdb;

    // Escape the source string for LIKE query and prepare the statement
    $like_source = '%' . $wpdb->esc_like($source) . '%';

    $query = $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}twxchat_vectors WHERE source LIKE %s",
        $like_source
    );

    // Get the count result
    $count = $wpdb->get_var($query);

    return (int) $count; // Cast to int to ensure consistent return type
}

add_action('update_option_twx_chat_options', function($old_options, $new_options, $option) {
    
	// Check if the option key is correct
    if ($option === 'twx_chat_options') {

        $oldSchedule = isset($old_options['site_crawler_schedule']) ? $old_options['site_crawler_schedule'] : '';
        $newSchedule = isset($new_options['site_crawler_schedule']) ? $new_options['site_crawler_schedule'] : '';

        $oldTime = isset($old_options['site_crawler_schedule_time']) ? $old_options['site_crawler_schedule_time'] : '';
        $newTime = isset($new_options['site_crawler_schedule_time']) ? $new_options['site_crawler_schedule_time'] : '';

        $oldDOW = isset($old_options['site_crawler_schedule_dayofweek']) ? $old_options['site_crawler_schedule_dayofweek'] : '';
        $newDOW = isset($new_options['site_crawler_schedule_dayofweek']) ? $new_options['site_crawler_schedule_dayofweek'] : '';

        if ($oldSchedule !== $newSchedule || $oldTime !== $newTime || $oldDOW !== $newDOW) twxchat_handle_change_site_crawler_schedule($newSchedule, $newTime, $newDOW);
    }
}, 10, 3);

function twxchat_handle_change_site_crawler_schedule($new_schedule, $new_time, $new_dayfoweek = '') {

    // Site Crawler schedule change

    $timezone_string = get_option('timezone_string');
    if (empty($timezone_string)) $timezone_string = 'UTC';
    
	switch($new_schedule){
        case '':
            twxchat_clear_all_site_crawler_scheduled_jobs();
            break;
		case 'Daily':
			// Schedule a daily event
            TWXChatUtil::adminLog('twxchat_handle_change_site_crawler_schedule:: Schedule a daily event');
            twxchat_clear_all_site_crawler_scheduled_jobs();
            $daily_time = $new_time;
            $datetime = new DateTime('today ' . $daily_time, new DateTimeZone($timezone_string));
            $datetime->setTimezone(new DateTimeZone('UTC')); // Convert to UTC
            $timestamp = $datetime->getTimestamp();
            wp_schedule_event($timestamp, 'daily', 'twx_chat_site_crawler_daily_event');
			break;
		case 'Weekly':
			// Schedule a weekly event
            TWXChatUtil::adminLog('twx_chat_handle_change_site_crawler_schedule:: Schedule a weekly event');
            twxchat_clear_all_site_crawler_scheduled_jobs();
            $weekly_day = $new_dayfoweek;
            $weekly_time = $new_time;
            $datetime = new DateTime('next ' . $weekly_day . ' ' . $weekly_time, new DateTimeZone($timezone_string));
            $datetime->setTimezone(new DateTimeZone('UTC')); // Convert to UTC
            $timestamp = $datetime->getTimestamp();
            wp_schedule_event($timestamp, 'weekly', 'twx_chat_site_crawler_weekly_event');
			break;
	}
	
}

function twxchat_restore_site_crawler_scheduled_jobs(){

    // in case site crawler jobs get deleted on plugin update etc, lets restore them

    $options = get_option('twx_chat_options');
    $siteCrawlerSchedule = $options['site_crawler_schedule'] ?? '';
    if(empty($siteCrawlerSchedule)) return;

    $scheduledTime = $options['site_crawler_schedule_time'] ?? '';
    if(empty($scheduledTime)) return;

    if($siteCrawlerSchedule === 'Daily'){
        $hook = 'twx_chat_site_crawler_daily_event';
        if(wp_next_scheduled($hook)) return;
        twxchat_handle_change_site_crawler_schedule($siteCrawlerSchedule, $scheduledTime);

    }else if($siteCrawlerSchedule === 'Weekly'){
        $hook = 'twx_chat_site_crawler_weekly_event';
        if(wp_next_scheduled($hook)) return;
        $scheduledDOW = $options['site_crawler_schedule_dayofweek'] ?? '';
        if(empty($scheduledDOW)) return;

        twxchat_handle_change_site_crawler_schedule($siteCrawlerSchedule, $scheduledTime, $scheduledDOW);
    }

}

function twxchat_clear_all_site_crawler_scheduled_jobs(){

    $hook = 'twx_chat_site_crawler_daily_event';
    wp_clear_scheduled_hook($hook);
    wp_cron(); // forces hooks out of memory - apparently
    $time = wp_next_scheduled($hook);
    if($time) {
        wp_unschedule_event($time, $hook);
        wp_cron();
    }

    $hook = 'twx_chat_site_crawler_weekly_event';
    wp_clear_scheduled_hook($hook);
    wp_cron(); // forces hooks out of memory - apparently
    $time = wp_next_scheduled($hook);
    if($time) {
        wp_unschedule_event($time, $hook);
        wp_cron();
    }

}

add_action('twx_chat_site_crawler_daily_event', 'twxchat_run_site_crawl_daily');
function twxchat_run_site_crawl_daily() {
    TWXChatSpider::runSiteCrawl('Daily');
}

add_action('twx_chat_site_crawler_weekly_event', 'twxchat_run_site_crawl_weekly');
function twxchat_run_site_crawl_weekly() {
    TWXChatSpider::runSiteCrawl('Weekly');
}

add_action('admin_notices', 'twxchat_chatlog_threshold_warning_notice');
function twxchat_chatlog_threshold_warning_notice() {

    //global $wpdb;
    global $twxchat_max_chatlog_rows;

    $row_count = TWXChatLog::getNumEntries();
    $warning_threshold = intval($twxchat_max_chatlog_rows * 0.8);
    $settings_url = admin_url('options-general.php?page=twx-chat-settings#chatlog'); //admin.php?page=your_plugin_settings');
    if ($row_count >= $warning_threshold) {
        if($row_count < $twxchat_max_chatlog_rows){
            // Approaching
            echo '<div class="notice notice-warning is-dismissible">';
            echo '<p><strong>TWX-Chat Warning:</strong> Your Chat Log is approaching its limit. Download your Chat Log <a href="' . esc_url($settings_url) . '">here</a> before automatic deletion occurs at ' . esc_html($twxchat_max_chatlog_rows) . ' entries.</p>';
            echo '</div>';
        }else{
            // Exceeded
            echo '<div class="notice notice-warning is-dismissible">';
            echo '<p><strong>TWX-Chat Warning:</strong> Your Chat Log has exceeded its entry limit of ' . esc_html($twxchat_max_chatlog_rows) . ' entries. Daily purges of old entries will now occur. Download your Chat Log <a href="' . esc_url($settings_url) . '">here</a>.</p>';
            echo '</div>';
        }
    }
}

add_action('twx_chat_cleanup_chatlog_event', 'twxchat_cleanup_chatlog_table');
function twxchat_cleanup_chatlog_table() {

    // Cleans up old rows in Chat Log table or warns if approaching limit. Sends emails to Admin.
    TWXChatUtil::adminLog("twxchat_cleanup_chatlog_table:: start");

    global $wpdb;
    global $twxchat_max_chatlog_rows;

    $table_name = $wpdb->prefix . 'twxchat_chatlog';
    $row_count = TWXChatLog::getNumEntries();
    
    $warning_threshold = intval($twxchat_max_chatlog_rows * 0.8);

    // Check if we need to warn with an email or start purging
    if ($row_count >= $warning_threshold) {
        if ($row_count < $twxchat_max_chatlog_rows) {
            twxchat_send_chatlog_warning_email($row_count);
        } else {
            // Purge
            $limit = absint($twxchat_max_chatlog_rows); // Ensure it's an integer
            $sql = "
                DELETE FROM `$table_name`
                WHERE id NOT IN (
                    SELECT id FROM (
                        SELECT id FROM `$table_name`
                        ORDER BY id DESC
                        LIMIT %d
                    ) AS t
                )
            ";
            $wpdb->query( $wpdb->prepare( $sql, $limit ) );

            $deleted_rows = $wpdb->rows_affected;

            twxchat_send_chatlog_deleted_email($deleted_rows);
            TWXChatUtil::adminLog("Scheduled chatlog cleanup deleted $deleted_rows old entries.");
        }
    }

}

function twxchat_send_chatlog_warning_email($row_count){

    TWXChatUtil::adminLog("twxchat_send_chatlog_warning_email:: start");

    global $twxchat_max_chatlog_rows;
    $admin_email = get_option('admin_email');
    
    if (empty($admin_email)) {
        return;
    }

    $sitename = get_bloginfo('name');

    $subject = "Warning: TWX-Chat Wordpress Plugin Chat Log Approaching Limit ($sitename)";
    $styleTag  = 'style'; // Cant use wp_register_script() here as it is an email body not webpage

    $message = "
        <html>
        <head>
            <$styleTag>
                body { font-family: Arial, sans-serif; line-height: 1.6; }
                .warning { color: red; font-weight: bold; }
                .recommend { background: #f8f8f8; padding: 10px; border-left: 4px solid #0073aa; }
            </$styleTag>
        </head>
        <body>
            <p><strong>WordPress Site:</strong> $sitename</p>
            <p><strong>Plugin:</strong> TWX-Chat</p>
            <p class='warning'>Warning: Your Chat Log has reached $row_count entries 
            (over 80% of the $twxchat_max_chatlog_rows entry limit). Old entries will be deleted within 24 hours.</p>
            
            <div class='recommend'>
                <p><strong>Recommended Action: Archive and Delete Chat old Log entries</strong></p>
                <ol>
                    <li>Go to the site's <strong>WordPress Admin</strong> console.</li>
                    <li>Navigate to <strong>Settings &gt; TWX-Chat &gt; Chat Log (tab)</strong>.</li>
                    <li>Select a <strong>Period</strong> and then click the <strong>'Download the Chatlog CSV'</strong> button. Perform the Download/Save file.</li>
                    <li>Click the <strong>Delete old Chat Log entries</strong> button and enter the number of entries to keep in the system. Click OK.</li>
                    <li><strong>We recommend keeping less than 50% of the maximum entries ($twxchat_max_chatlog_rows)</strong> to avoid frequent warning emails/alerts.</li>
                </ol>
            </div>

            <p>Kind Regards,<br><strong>The TWX-Chat Team</strong></p>
        </body>
        </html>";

    $headers = array('Content-Type: text/html; charset=UTF-8');

    $mail_result = wp_mail($admin_email, $subject, $message, $headers);
    // Dev server didnt send email, even with true result, but this may not be an issue on most client servers??
    //$mail_result = twxchat_send_mail($admin_email, $subject, $message);
    TWXChatUtil::adminLog("twxchat_send_chatlog_warning_email:: Chat log warning email sent to $admin_email. Result was ".var_export($mail_result,true));
}

function twxchat_send_chatlog_deleted_email($row_count){

    TWXChatUtil::adminLog("twxchat_send_chatlog_deleted_email:: start");

    global $twxchat_max_chatlog_rows;
    $admin_email = get_option('admin_email');
    
    if (empty($admin_email)) {
        return;
    }

    $sitename = get_bloginfo('name');

    $subject = "FYI: TWX-Chat Wordpress Plugin has deleted $row_count old Chat Log entries ($sitename)";
    $styleTag  = 'style'; // Cant use wp_register_script() here as it is an email body not webpage

    $message = "
        <html>
        <head>
            <$styleTag>
                body { font-family: Arial, sans-serif; line-height: 1.6; }
                .warning { color: red; font-weight: bold; }
                .recommend { background: #f8f8f8; padding: 10px; border-left: 4px solid #0073aa; }
            </$styleTag>
        </head>
        <body>
            <p><strong>WordPress Site:</strong> $sitename</p>
            <p><strong>Plugin:</strong> TWX-Chat</p>
            <p class='warning'>Note: $row_count old entries have been automatically deleted from your Chat Log. 
            This has likely ocurred due to the Chat Log exceeding maximum size of $twxchat_max_chatlog_rows entries.</p>
            
            <div class='recommend'>
                <p><strong>Recommended Action: Archive and Delete old Chat Log entries</strong></p>
                <ol>
                    <li>Go to the site's <strong>WordPress Admin</strong> console.</li>
                    <li>Navigate to <strong>Settings &gt; TWX-Chat &gt; Chat Log (tab)</strong>.</li>
                    <li>Select a <strong>Period</strong> and then click the <strong>'Download the Chatlog CSV'</strong> button. Perform the Download/Save file.</li>
                    <li>Click the <strong>Delete old Chat Log entries</strong> button and enter the number of entries to keep in the system. Click OK.</li>
                    <li><strong>We recommend keeping less than 50% of the maximum entries ($twxchat_max_chatlog_rows)</strong> to avoid frequent warning emails/alerts.</li>
                </ol>
            </div>

            <p>Kind Regards,<br><strong>The TWX-Chat Team</strong></p>
        </body>
        </html>";

    $headers = array('Content-Type: text/html; charset=UTF-8');

    $mail_result = wp_mail($admin_email, $subject, $message, $headers);
    // Dev server didnt send email, even with true result, but this may not be an issue on most client servers??
    //$mail_result = twxchat_send_mail($admin_email, $subject, $message);
    TWXChatUtil::adminLog("twxchat_send_chatlog_deleted_email:: Chat log warning email sent to $admin_email. Result was ".var_export($mail_result,true));
}

function twxchat_send_mail($toAddress, $subject, $bodyHtml){

    // Not used at the moment until we figure out why wp_mail() not working
    TWXChatUtil::adminLog("twxchat_send_mail:: start");

    require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
    require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
    require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';

    try {

        $site_email = get_option('admin_email');

        $mail = new PHPMailer\PHPMailer\PHPMailer();
        $mail->isSMTP();
        $mail->IsHTML(true);
        $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
        //$mail->Port = 587;
        $mail->setFrom($site_email);
        $mail->addAddress($toAddress);
        $mail->Subject = $subject;
        $mail->Body = $bodyHtml;
        $ret = $mail->send();
        TWXChatUtil::adminLog("twxchat_send_mail:: send result: ".var_export($ret,true));
        return true;

    } catch (Exception $ex){
        TWXChatUtil::adminLog("twxchat_send_mail:: error {$ex->getMessage()}",'a',true);
        return false;
    }

}

?>
