<?php
// Gets included in main plugin php, no need to get Wordpress Context or vendor include

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

require_once __DIR__ . '/twx_util.php';
require_once __DIR__ . '/openai_include.php';
require_once __DIR__ . '/knowledge_base_include.php';
require_once __DIR__ . '/chatlog_include.php';
require_once __DIR__ . '/twx_spider.php';

if(file_exists(__DIR__ . '/twx_chat_plus.php')){
	require_once __DIR__ . '/twx_chat_plus.php';
}
if(file_exists(__DIR__ . '/twx_chat_pro.php')){
	require_once __DIR__ . '/twx_chat_pro.php';
}

function twxchat_getKnowledgeBaseAdminHTML(){

	// AJAX Handler - Return HTML for Knowlede Base Admin page
    if (! check_ajax_referer( 'twx_chat_nonce', 'nonce', false )) {
	    wp_send_json_error('Session expired. Please refresh the page and try again.');
		wp_die();
    }

	$columns = TWXChatKnowledgeBaseUtil::getAdminColumns();
	$html = '';
	
	$filters_raw = $_POST['filters'] ?? []; // sanitization on next line
	$filters = is_array($filters_raw) ? array_map('sanitize_text_field', $filters_raw) : []; // sanitization

	$similarQueryColIndexes_raw = $_POST['similarQueryColIndexes'] ?? []; // sanitization on next line
	$similarQueryColIndexes = is_array($similarQueryColIndexes_raw) ? array_map('intval', $similarQueryColIndexes_raw) : []; // sanitization
	
	//$showColumns_raw = $_POST['showColumns'] ?? [];
	//$showColumns = is_array($showColumns_raw) ? array_map('sanitize_text_field', $showColumns_raw) : []; // sanitization
	$showColumns = []; // Implement later if we want sortable/hidable columns like ChatLog

	$sortFieldIdx = sanitize_text_field($_POST['sortField'] ?? ''); 
	$sortAsc = (sanitize_text_field($_POST['sortAsc'] ?? 'true') == 'true'); 

	$filtersParsed = [];
	foreach($filters as $colIdx => $filter){
		if(!isset(array_keys($columns)[$colIdx])) continue;
		$colName = array_keys($columns)[$colIdx];
		if(isset($columns[$colName]['canFilter']) && $columns[$colName]['canFilter'] === true && !in_array($colIdx, $similarQueryColIndexes)){
			$filtersParsed[$colName] = $filter;
		}
	}

	$sortColName = null;
	if($sortFieldIdx !== ''){
		$sortFieldIdx = intval($sortFieldIdx);
		if($sortFieldIdx >= 0 && $sortFieldIdx < count($columns)){
			$sortColName = array_keys($columns)[$sortFieldIdx];
		}
	}

	$entries = TWXChatKnowledgeBaseUtil::getFilteredEntries($filtersParsed, $sortColName, $sortAsc);

	// Further refine by AI/Embedding Search
	if(count($similarQueryColIndexes)>0 && count($entries)>0)	{
		$aiFilteredEntries = [];
		$openaiApiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';
		$minSimilarity = intval(get_option('twx_chat_options')['min_similarity'] ?? TWXCHAT_DEFAULT_MIN_SIMILARITY)/100;
		foreach($similarQueryColIndexes as $colIdx){
			$query = $filters[$colIdx] ?? '';
			$queryEmbedding = TWXChatUtil::getEmbedding($openaiApiKey, $query);
			//TWXChatUtil::adminLog("twx_chat_getKnowledgeBaseAdminHTML: similar query: $query. min similarity: $minSimilarity");

			foreach($entries as $kbId => $entry){
				$similarity = TWXChatUtil::cosineSimilarity($queryEmbedding, $entry['embedding']);
				if($similarity >= $minSimilarity){
					$entry['similarity'] = $similarity;
					$aiFilteredEntries[$kbId] = $entry;
				}
			}
		}

		if($sortColName){
			$aiFilteredEntries = TWXChatKnowledgeBaseUtil::sortEntries($aiFilteredEntries, $sortColName, $sortAsc);
		}else{
			$aiFilteredEntries = TWXChatKnowledgeBaseUtil::sortEntries($aiFilteredEntries, 'similarity', false);
		}
		$entries = $aiFilteredEntries;
	}

	$html = '<h3>Knowledge Base entries ('.count($entries).')</h3>';
	$html .= TWXChatKnowledgeBaseUtil::getAdminHtml($entries, $columns, $showColumns, $filters, $sortColName, $sortAsc, $similarQueryColIndexes);

	$retCols = [];
	$retCols[] = ['isSelectRow' => true]; // Checkbox col

	foreach($columns as $colName => $data){
		$thisCol = [];
		$thisCol['colName'] = $colName;
		$thisCol['display'] = isset($data['display']) ? $data['display'] : $colName;
		if(isset($data['defaultVal'])) $thisCol['defaultVal'] = $data['defaultVal'];
		$thisCol['required'] = $data['required'] ?? false;
		if(isset($data['sortable'])) $thisCol['sortable'] = $data['sortable'];
		if(isset($data['canFilter'])) $thisCol['canFilter'] = $data['canFilter'];
		if(isset($data['canEdit'])) $thisCol['canEdit'] = $data['canEdit'];
		$retCols[] = $thisCol;
	}
	$ret = ['html' => $html, 'columns' => $retCols];

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getKnowledgeBaseAdminHTML', 'twxchat_getKnowledgeBaseAdminHTML');

function twxchat_updateKnowledgeBaseAdmin(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	$entries_raw = $_POST['entries'] ?? []; // sanitization on next lines
	$entries = [];
	if (is_array($entries_raw)) {
		foreach ($entries_raw as $entry) {
			if (is_array($entry)) {
				$entries[] = [
					'kbId'		=> isset($entry['kbId']) ? intval($entry['kbId']) : '',
					'title'     => sanitize_text_field($entry['title'] ?? ''),
					'source'    => sanitize_text_field($entry['source'] ?? ''),
					'orig_text' => sanitize_textarea_field($entry['orig_text'] ?? ''),
				];
			}
		}
	}

	if(empty($entries)) {
		wp_send_json_success('nothing to update');
		wp_die();
	}

	$apiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';
	$added = 0; $updated = 0;
	$errors = [];

	foreach($entries as $entry){
		$kbId = $entry['kbId'] ?? '';
		if($kbId === ''){
			// new kb entry
			try {
				TWXChatKnowledgeBaseUtil::storeEntry($apiKey, $entry['source'], $entry['title'], $entry['orig_text'], false);
				$added++;
			} catch (Exception $ex) {
				$errors[] = ['kbId' => $kbId, 'error' => $ex->getMessage()];
			}
		}else{
			// updated kb entry
			TWXChatKnowledgeBaseUtil::updateEntryById($apiKey, $kbId, $entry);
			$updated++;
		}
	}

	$ret = ['added'=>$added, 'updated'=>$updated, 'errors' => $errors];

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_updateKnowledgeBaseAdmin', 'twxchat_updateKnowledgeBaseAdmin');

function twxchat_getChatbotsAdminHTML(){

	// Gets HTML for Chatbots list in Admin area

	// AJAX Handler
    if (! check_ajax_referer( 'twx_chat_nonce', 'nonce', false )) {
	    wp_send_json_error('Session expired. Please refresh the page and try again.');
		wp_die();
    }

	$html = '';

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

    $limit = TWXChatUtil::customerPlanAccess('plus') ? null : 2;
    $limitReached = false;
    $rows = [];
    if($limit){
        $rows = $wpdb->get_results(
            $wpdb->prepare("SELECT * FROM $table_name ORDER BY id DESC LIMIT %d", $limit)
        );
        // reverse to get ascending order (lowest ID first)
        $rows = array_reverse($rows);
        if(count($rows) >= $limit) $limitReached = true;
    }else{
        $rows = $wpdb->get_results("SELECT * FROM $table_name ORDER BY id DESC");
    }

    $html .= '<thead><tr><th>ID</th><th>Name</th><th>Type</th><th>Shortcode</th><th>In Use?</th><th>Actions</th></tr></thead><tbody>';

    foreach ($rows as $row) {
		$atts = json_decode($row->settings ?? '{}', true);
		$type = $atts['type'] ?? '';
        $shortcode = '[twxchat id="' . esc_attr($row->id) . '"]';

		$inUse = '';
		$posts = TWXChatUtil::findPostsWithChatbotId($row->id);
		if(!empty($posts)){
			foreach ($posts as $post) {
				$edit_link = get_edit_post_link($post->ID);
				$view_link = get_permalink($post->ID);

				$inUse .= '<li>';
				$inUse .= esc_html($post->post_type) . ': <strong>' . esc_html($post->post_title) . '</strong> ';
				$inUse .= '(ID: ' . $post->ID . ') ';
				$inUse .= '[<a href="' . esc_url($view_link) . '" target="_blank">View</a>] ';
				$inUse .= '[<a href="' . esc_url($edit_link) . '" target="_blank">Edit</a>]';
				$inUse .= '</li>';
			}
		}else{
			$inUse = 'No';
		}
		if($inUse === 'No'){
			// Check if used as Frontpage Chatbot
			$frontpage_shortcode = get_option('twx_chat_options')['frontpage_shortcode'] ?? '';
			if($frontpage_shortcode && strpos($frontpage_shortcode, 'id="'.$row->id.'"') !== false) $inUse = 'Frontpage Shortcode';
		}

		$inUseDel = ($inUse === 'No' ? 'false' : 'true');
        $html .= "<tr>
                <td>{$row->id}</td>
                <td>{$row->name}</td>
                <td>{$type}</td>
                <td><code>$shortcode</code><button class=\"twx-chat-copy-shortcode-btn\">📋</button></td>
                <td>$inUse</td>
                <td>
                    <button class=\"button-secondary twx-chat-chatbot-edit-btn\" data-chatbot-id=\"{$row->id}\">Edit</button>&nbsp;&nbsp;
                    <button class=\"button-secondary twx-chat-chatbot-copy-btn\" data-chatbot-id=\"{$row->id}\">Copy</button>&nbsp;&nbsp;
                    <button class=\"button-secondary twx-chat-chatbot-delete-btn\" data-chatbot-id=\"{$row->id}\" data-in-use=\"{$inUseDel}\">Delete</button>
                </td>
              </tr>";
    }

    $html .= '</tbody>';

	$ret = ['html' => $html, 'limit_reached' => $limitReached];

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getChatbotsAdminHTML', 'twxchat_getChatbotsAdminHTML');

function twxchat_getEditChatbotAdminHTML(){

	// AJAX Handler
    if (! check_ajax_referer( 'twx_chat_nonce', 'nonce', false )) {
	    wp_send_json_error('Session expired. Please refresh the page and try again.');
		wp_die();
    }

	$atts = TWXChatUtil::getAllChatbotShortcodeAttributes();

	$options = get_option('twx_chat_options');
	$min_similarity_default = $options['min_similarity'] ?? TWXCHAT_DEFAULT_MIN_SIMILARITY;
	$clientToken = $options['twx_customer_key'] ?? '';

	$id = sanitize_text_field($_POST['id'] ?? ''); 

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

    $limit = TWXChatUtil::customerPlanAccess('plus') ? null : 2;
    if($limit && empty($id)){
        // Check if can add new chatbot
        $count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
        if($count >= $limit){
            wp_send_json_error('Chatbot limit reached');
            wp_die();
        }
    }

    $row = null;
	$chatbot_settings = [];
	$nameValidationMsg = '';
    if ($id) {
        $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id));
        $chatbot_settings = json_decode($row->settings, true);
    }else{
 		$nameValidationMsg = '<div class="validation-msg" style="color:red;">Must contain value</div>';
	}
	
	$avatars = TWXChatUtil::fetchChatbotAvatars($clientToken);
	
	function getAvatarOptions($avatars, $val){
		$html = '<option value=""'.(empty($val) ? ' selected' : '').'></option>';
		foreach($avatars as $avatar_id => $avatar){
			$selected = '';
			$display = $avatar_id.($avatar['exists'] ? ' (exists)' : ($avatar['available'] ? ' (get now)' : ' (get with '.$avatar['min_plan'].' plan)'));
			$data_exists = ' data-exists="'.($avatar['exists'] ? 'true' : 'false').'"';
			$data_check_file = ' data-check-file="'.$avatar['local_assets_path'].$avatar['model'].'"';
			$available = ' data-available="'.($avatar['available'] ? 'true' : 'false').'"';
			if($val){
				if($val === $avatar_id) $selected = ' selected';
			}
			$html .= "<option{$data_exists}{$data_check_file}{$available} value=\"{$avatar_id}\"$selected>{$display}</option>";
			
		}
		return $html;
	}

	$html = '
        <h2>'.($id ? 'Edit Chatbot' : 'New Chatbot').'</h2>
        <table class="form-table"><tr><th>Name*</th><td><input id="twx-chat-chatbot-edit-name" size="100px" type="text" name="name" value="'.esc_attr($row->name ?? '').'" required>'.$nameValidationMsg.'</td></tr>';

	foreach($atts as $att_name => $att){
		$label = $att['label'];
		$html .= "<tr><th>{$label}<br/></th>";
		$default_val = isset($att['default_val']) ? $att['default_val'] : '';
		if($att_name === 'min_similarity' && empty($default_val)) $default_val = $min_similarity_default;
		$val = isset($chatbot_settings[$att_name]) ? esc_attr($chatbot_settings[$att_name]) : $default_val;

		if(isset($att['min_plan']) && !TWXChatUtil::customerPlanAccess($att['min_plan'])){
			$html .= '<td><p>This <span class="twx-chat-feature-button" data-feature="chatbot_'.$att_name.'">feature</span> is available with our <span class="twx-'.$att['min_plan'].'-button">'.ucwords($att['min_plan']).' Plan</span> or above.</p>';
		}else{
			switch ($att['type']) {
				case 'select':
					$html .= "<td><select class=\"twx_chatbot_att\" name=\"{$att_name}\">";
					if($att_name === 'avatar_character' && !empty($avatars)){
						$html .= getAvatarOptions($avatars, $val);
					} else {
						foreach($att['options'] as $opt){
							$selected = '';
							if($val){
								if($val === $opt) $selected = ' selected';
							}else if(isset($att['default_val']) && $att['default_val'] === $opt){
								$selected = ' selected';
							}
							$html .= "<option value=\"{$opt}\"$selected>{$opt}</option>";
						}
					}					
					$html .= "</select>";
					break;
				case 'number':
					$html .= "<td><input type=\"number\" class=\"twx_chatbot_att\" name=\"{$att_name}\" min=\"{$att['min']}\" max=\"{$att['max']}\" step=\"1\" value=\"$val\" required>";
					break;
				case 'textarea':
					$html .= "<td><textarea cols=\"100\" rows=\"4\" class=\"twx_chatbot_att\" name=\"{$att_name}\">$val</textarea>";
					break;
				case 'datalist':
					$html .= "<td><input size=\"100px\" type=\"text\" class=\"twx_chatbot_att\" name=\"{$att_name}\" value=\"$val\" list=\"attr-datalist-{$att_name}\"><datalist id=\"attr-datalist-{$att_name}\">";
					if(isset($att['options'])){
						foreach($att['options'] as $opt){
							$selected = '';
							if($val){
								if($val === $opt) $selected = ' selected';
							}else if(isset($att['default_val']) && $att['default_val'] === $opt){
								$selected = ' selected';
							}
							$html .= "<option value=\"{$opt}\"$selected>{$opt}</option>";
						}
					}
					$html .= '</datalist>';
					break;
				case 'custom':
					if($val === 'Array') $val = $chatbot_settings[$att_name]; // revert to original value which was an array
					$function = $att['function'] ?? '';
					if($function && function_exists($function)){
						$html .= '<td>'.$function($att,$val);
					}else{
						$html .= '<td>Unknown custom function';
					}
					break;
				default:
					$html .= "<td><input size=\"100px\" type=\"text\" class=\"twx_chatbot_att\" name=\"{$att_name}\" value=\"$val\">";
			}
		}
		if(isset($att['description'])) $html .= "<p class=\"description\"><button class=\"twx-chat-more-info-btn\" title=\"More info\" data-chatbot-attr=\"{$att_name}\">i</button>&nbsp;{$att['description']}</p>";
		$html .= "</td></tr>";
	}

	$html .= '</table><button class="button-primary" id="twx-chat-chatbot-save-btn">'.($id ? 'Update Chatbot' : 'Create Chatbot').'</button>';
	$html .= '&nbsp;&nbsp;<button class="button-secondary" id="twx-chat-chatbot-cancel-btn">Cancel</button>';

	$ret = ['html' => $html];

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getEditChatbotAdminHTML', 'twxchat_getEditChatbotAdminHTML');

function twxchat_updateChatbotAdmin(){

	// AJAX Handler to insert or update a Chatbot definition
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	if(empty($_POST['name'] ?? '') || empty($_POST['settings'] ?? '')) {
		wp_send_json_success('missing name or attributes');
		wp_die();
	}

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

	$id = sanitize_text_field($_POST['id'] ?? '');
	$name = sanitize_text_field($_POST['name'] ?? '');

	$atts = TWXChatUtil::getAllChatbotShortcodeAttributes();

	$settings_raw = $_POST['settings'] ?? []; // sanitization on next lines
	// Strip out extra escape chars that Wordpress inserts automatically (aka: magic quotes)
	$settings_raw = array_map('stripslashes_deep', $settings_raw); // further sanitization on next lines

	$settings = [];
	if (is_array($settings_raw)) {
		foreach ($settings_raw as $key => $value) {
			if(!isset($atts[$key])) continue; // validates key
			$att = $atts[$key];
			if(isset($att['sanitize_fn'])){
				$sanitizeFn = $att['sanitize_fn'];
				$settings[sanitize_key($key)] = $sanitizeFn($value);
			}else{
				$settings[sanitize_key($key)] = sanitize_text_field($value);
			}
		}
	}else{
		wp_send_json_success('invalid settings array');
		wp_die();
	}

	$new_settings = json_encode($settings);

	$action = '';
	if ($id) {
		$action = 'updated';
		$wpdb->update($table_name, [
			'name' => $name,
			'settings' => $new_settings
		], ['id' => $id]);
	} else {
		$action = 'created';
		$wpdb->insert($table_name, [
			'name' => $name,
			'settings' => $new_settings
		]);
	}

	wp_send_json_success("chatbot $action successfully");
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_updateChatbotAdmin', 'twxchat_updateChatbotAdmin');

function twxchat_deleteChatbotAdmin(){

	// AJAX Handler to delete a Chatbot definition
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	if(empty($_POST['id'] ?? '')) {
		wp_send_json_success('missing id');
		wp_die();
	}

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

	$id = sanitize_text_field($_POST['id'] ?? '');

    // Delete the row
    $deleted = $wpdb->delete(
        $table_name,
        ['id' => $id],
        ['%d'] // id is an integer
    );

	$result = ($deleted !== false ? 'chatbot deleted successfully' : 'chatbot not deleted successfully');

	wp_send_json_success($result);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_deleteChatbotAdmin', 'twxchat_deleteChatbotAdmin');

function twxchat_copyChatbotAdmin(){

	// AJAX Handler to copy a Chatbot definition
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	if(empty($_POST['id'] ?? '')) {
		wp_send_json_success('missing id');
		wp_die();
	}

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

	$id = sanitize_text_field($_POST['id'] ?? '');

    // Get the row by ID
    $row = $wpdb->get_row(
        $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id),
        ARRAY_A
    );

    if (!$row) {
		wp_send_json_success('chatbot does not exist');
		wp_die();
    }

    // Remove the ID so a new one will be auto-inserted
    unset($row['id']);

    // Insert the copied row
    $result = $wpdb->insert($table_name, $row);
    $newId = $result ? $wpdb->insert_id : 'error'; // Return new ID if successful

	wp_send_json_success("chatbot copied successfully to new ID $newId");
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_copyChatbotAdmin', 'twxchat_copyChatbotAdmin');

function twxchat_sanitizeChatbotUserFields($userFields_raw){

	// Sanitize chatbot user fields user input
	$userFields = [];

	if(isset($userFields_raw['update_webhook_url'])){
		$userFields['update_webhook_url'] = esc_url_raw($userFields_raw['update_webhook_url']);
	}

	if(isset($userFields_raw['flashcard_title'])){
		$userFields['flashcard_title'] = sanitize_text_field($userFields_raw['flashcard_title']);
	}
	
	$fields_raw = $userFields_raw['fields'] ?? [];
	foreach($fields_raw as $field_raw){
		if(is_array($field_raw)){
			$userFields['fields'][] = [
				'name' => sanitize_text_field($field_raw['name'] ?? ''),
				'description' => sanitize_text_field($field_raw['description'] ?? ''),
			];
		}
	}

	return $userFields;
}

function twxchat_downloadAvatarAssets() {

    // AJAX Handler to download and install avatar assets to the local server
    check_ajax_referer('twx_chat_nonce', 'nonce');

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

    if (empty($_POST['id'] ?? '')) {
        wp_send_json_success(['error_msg' => 'missing id']);
        wp_die();
    }
    if (empty($_POST['check_file'] ?? '')) {
        wp_send_json_success(['error_msg' => 'missing check_file']);
        wp_die();
    }

    // Check if animation assets exist
    if (!file_exists(plugin_dir_path(dirname(__FILE__)) . 'assets/glTF/animations/adult_female/blink.glb')) {
        if (!twxchat_downloadAnimationAssets()) {
            wp_send_json_success(['error_msg' => 'avatar animation assets download failed']);
            wp_die();
        }
    }

    $options = get_option('twx_chat_options');
    $clientToken = $options['twx_customer_key'] ?? '';

    $avatarId = sanitize_text_field($_POST['id']);
    $check_file = sanitize_text_field($_POST['check_file']);
    $check_file_full_path = TWXChatUtil::getAvatarAssetsRootPath() . $check_file;
    $dest_path = dirname($check_file_full_path);

    $remote_url = TWXChatUtil::getTwxlabsUrl()."api/get_avatar_assets.php?token=$clientToken&avatar_id=$avatarId";

    $file_name = str_replace(':', '_', strtolower($avatarId)) . '_assets';
    $temp_dir = TWXChatUtil::getUploadsDir('tmp');
    $zip_path = $temp_dir . $file_name . '.zip';
    $unzip_dir = $temp_dir . $file_name . '/';

    if (!file_exists($temp_dir)) {
        TWXChatUtil::makeDir($temp_dir);
    }

    ignore_user_abort(true);
    
    // Fetch and stream ZIP file using WordPress HTTP API
    $response = TWXChatUtil::getHttpResponse($remote_url, [
        'timeout'  => 60,
        'stream'   => true,
        'filename' => $zip_path,
    ]);

    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        TWXChatUtil::adminLog("HTTP error: $error_message");
        wp_send_json_success(['error_msg' => "Download error: $error_message"]);
        wp_die();
    }

    $http_code = wp_remote_retrieve_response_code($response);
    if ($http_code !== 200) {
        TWXChatUtil::adminLog("twx_chat_downloadAvatarAssets() Download failed with HTTP code $http_code");
        wp_send_json_success(['error_msg' => "Download failed (HTTP $http_code)"]);
        wp_die();
    }

    // Confirm size
    $actual_size = filesize($zip_path);

    // Unzip
    TWXChatUtil::adminLog("twx_chat_downloadAvatarAssets() Unzip to: $unzip_dir");
    $zip = new ZipArchive;
    if ($zip->open($zip_path) === TRUE) {
        TWXChatUtil::makeDir($unzip_dir);
        $zip->extractTo($unzip_dir);
        $zip->close();
    } else {
	    TWXChatUtil::adminLog("twx_chat_downloadAvatarAssets() Unzip failed");
        wp_send_json_success(['error_msg' => "Unzip failed"]);
        wp_die();
    }

    // Copy to Final destination
    TWXChatUtil::recursiveCopy($unzip_dir, $dest_path);
    TWXChatUtil::recursiveDelete($unzip_dir);
    wp_delete_file($zip_path);

    if (!file_exists($check_file_full_path)) {
        wp_send_json_success(['error_msg' => "File check failed"]);
        wp_die();
    }

    $ret = ['success_msg' => 'Downloaded and installed successfully', 'zip_bytes_downloaded' => $actual_size];
    wp_send_json_success($ret);
    wp_die();
}
add_action('wp_ajax_twx_chat_downloadAvatarAssets', 'twxchat_downloadAvatarAssets');

function twxchat_downloadAnimationAssets() {

    // Download Avatar Animation assets which are now loaded dynamically as needed
    TWXChatUtil::adminLog("twx_chat_downloadAnimationAssets() start");

    $options = get_option('twx_chat_options');
    $clientToken = $options['twx_customer_key'] ?? '';

    $check_file_full_path = TWXChatUtil::getAvatarAssetsRootPath() . 'glTF/animations/adult_female/blink.glb';
    TWXChatUtil::adminLog("twx_chat_downloadAnimationAssets() check_file_full_path: $check_file_full_path");

    $dest_path = TWXChatUtil::getAvatarAssetsRootPath() . 'glTF/animations/';
    TWXChatUtil::adminLog("twx_chat_downloadAnimationAssets() dest_path: $dest_path");

    if (!file_exists($dest_path)) {
        TWXChatUtil::makeDir($dest_path);
    }

    $remote_url = TWXChatUtil::getTwxlabsUrl()."api/get_avatar_animations.php?token=$clientToken"; 

    $file_name = 'aws_animations_assets';
    $temp_dir = TWXChatUtil::getUploadsDir('tmp');
    $zip_path = $temp_dir . $file_name . '.zip';
    $unzip_dir = $temp_dir . $file_name . '/';

    if (!file_exists($temp_dir)) {
        TWXChatUtil::makeDir($temp_dir);
    }

    // Allow long execution and ignore client disconnects
    ignore_user_abort(true);

    // Use WordPress HTTP API to stream and save ZIP
    $response = TWXChatUtil::getHttpResponse($remote_url, [
        'timeout'  => 60,
        'stream'   => true,
        'filename' => $zip_path,
    ]);

    if (is_wp_error($response)) {
        TWXChatUtil::adminLog("twx_chat_downloadAnimationAssets() Download error: " . $response->get_error_message());
        return false;
    }

    $http_code = wp_remote_retrieve_response_code($response);
    if ($http_code !== 200) {
        TWXChatUtil::adminLog("Download failed with HTTP code $http_code");
        return false;
    }

    // Unzip
    $zip = new ZipArchive;
    if ($zip->open($zip_path) === TRUE) {
        TWXChatUtil::makeDir($unzip_dir);
        $zip->extractTo($unzip_dir);
        $zip->close();
    } else {
	    TWXChatUtil::adminLog("twx_chat_downloadAnimationAssets() Failed Unzip to: $unzip_dir");
        return false;
    }

    // Move files to destination
    TWXChatUtil::recursiveCopy($unzip_dir, $dest_path);
    TWXChatUtil::recursiveDelete($unzip_dir);
    wp_delete_file($zip_path);

	if (!file_exists($check_file_full_path)) {
        return false;
    }

    return true;
}

function twxchat_deleteKnowledgeBaseAdmin(){

	// AJAX Handler
	check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	$kbIds = array_map('absint', isset($_POST['kbIds']) && is_array($_POST['kbIds']) ? $_POST['kbIds'] : []);

	$deleted = 0;
	foreach($kbIds as $kbId){
		if(TWXChatKnowledgeBaseUtil::deleteEntryById($kbId) !== false) $deleted++;
	}

	$ret = ['deleted'=>$deleted];

	wp_send_json_success($ret);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_deleteKnowledgeBaseAdmin', 'twxchat_deleteKnowledgeBaseAdmin');

function twxchat_startSiteCrawlerNow(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// Start Site Crawler if not already running

	$running = get_option('twx_chat_site_crawler_running') ?? false;
	if($running){
		// already running
		$ret = ['status' => 'already_running'];
		wp_send_json_success($ret);
		wp_die();
	}

	TWXChatUtil::adminLog("twxchat_startSiteCrawlerNow:: start");
	TWXChatSpider::runSiteCrawl('Now');

	$ret = ['status' => 'started'];
	wp_send_json_success($ret);

	// Always die in AJAX handlers
	wp_die();
}
add_action('wp_ajax_twx_chat_startSiteCrawlerNow', 'twxchat_startSiteCrawlerNow');

function twxchat_cancelRunningSiteCrawler(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// Cancel Site Crawler if already running
	update_option('twx_chat_site_crawler_running',false);

	$ret = ['status' => 'cancelled'];
	wp_send_json_success($ret);

	// Always die in AJAX handlers
	wp_die();
}
add_action('wp_ajax_twx_chat_cancelRunningSiteCrawler', 'twxchat_cancelRunningSiteCrawler');

function twxchat_getSiteCrawlerProgress(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	// Get Site Crawler progress
	$running = get_option('twx_chat_site_crawler_running', false);

	$logFile = TWXChatUtil::getUploadsDir('spider_results').'twxchatspider_progress.txt';
	$ret = [];
	if(file_exists($logFile)){
		$logLines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
		$ret['log'] = '<ul>';
		foreach($logLines as $line){
			$ret['log'] .= '<li>'.$line.'</li>';
		}
		$ret['log'] .= '</ul>';
	}
	$ret['running'] = $running;

	wp_send_json_success(['progress' => $ret]);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getSiteCrawlerProgress', 'twxchat_getSiteCrawlerProgress');

function twxchat_getChatLogHTML(){
	
	// AJAX Handler
    if (! check_ajax_referer( 'twx_chat_nonce', 'nonce', false )) {
	    wp_send_json_error('Session expired. Please refresh the page and try again.');
		wp_die();
    }

	if(!class_exists('TWXChatPlus')){
		wp_send_json_error('Permission denied');
		wp_die();
	}

	$columns = TWXChatLog::getChatLogColumns();
	$html = '';
	
	$period = sanitize_text_field($_POST['period'] ?? 'Today'); 
	$showColumns_raw = $_POST['showColumns'] ?? []; // sanitization on next lines
	$showColumns = is_array($showColumns_raw) ? array_map('absint', $showColumns_raw) : []; 

	$sortFieldIdx = sanitize_text_field($_POST['sortField'] ?? ''); 
	$sortAsc = sanitize_text_field($_POST['sortAsc'] ?? 'true'); 

	$entries = TWXChatPlus::getChatLogEntries($period);

	if(!empty($sortFieldIdx)) TWXChatLog::sortEntries($entries, intval($sortFieldIdx), ($sortAsc == 'true' ? 'ASC' : 'DESC'), $columns);

	$sortColName = null;
	if($sortFieldIdx !== ''){
		$sortFieldIdx = intval($sortFieldIdx);
		if($sortFieldIdx >= 0 && $sortFieldIdx < count($columns)){
			$sortColName = array_keys($columns)[$sortFieldIdx];
		}
	}

	$sortAsc = ($sortAsc == 'true') ? 'ASC': 'DESC';

	$html = '<h3>'.$period.' ('.count($entries).' entries)</h3>';
	$html .= TWXChatPlus::getChatLogHtml($entries, $columns, $showColumns, $sortColName, $sortAsc);
	
	wp_send_json_success($html);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getChatLogHTML', 'twxchat_getChatLogHTML');

function twxchat_getChatLogCSV(){
	
	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	if(!class_exists('TWXChatPlus')){
		wp_send_json_error('Permission denied');
		wp_die();
	}

	$columns = TWXChatLog::getChatLogColumns();

	$period = sanitize_text_field($_POST['period'] ?? 'Today'); 
	$sortFieldIdx = sanitize_text_field($_POST['sortField'] ?? ''); 
	$sortAsc = sanitize_text_field($_POST['sortAsc'] ?? 'true'); 

	$entries = TWXChatPlus::getChatLogEntries($period);
	if(!empty($sortFieldIdx)) TWXChatLog::sortEntries($entries, intval($sortFieldIdx), ($sortAsc == 'true' ? 'ASC' : 'DESC'), $columns);

	$csvData = TWXChatPlus::getCSVArray($entries, $columns);

    // Convert array to CSV format - Cant use WP Filesystem for buffering output
    $output = fopen('php://output', 'w');
    ob_start(); // Start output buffering
    foreach ($csvData as $row) {
        fputcsv($output, $row); // Cant use WP Filesystem for buffering output
    }
    fclose($output); // Cant use WP Filesystem for buffering output
    $csvContent = ob_get_clean(); // Get the buffered output

    // Return the CSV data as a response
	wp_send_json_success($csvContent);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getChatLogCSV', 'twxchat_getChatLogCSV');

function twxchat_parseSources(){

}

function twxchat_parseUserFields(){

}

function twxchat_parseHyperlinkClicks(){

}

function twxchat_deleteChatLogEntries(){
	
	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	$num_to_keep = sanitize_text_field($_POST['numToKeep'] ?? ''); 

	if(empty($num_to_keep) || !ctype_digit($num_to_keep)){
		$response = [
			'status' => 'error',
			'message' => 'Missing/invalid numToKeep value',
			'data' => [],
		];
		wp_send_json_error($response);
		wp_die();
	}

	if(!method_exists('TWXChatPlus','deleteChatLogEntries')){
		wp_send_json_error('Permission denied');
		wp_die();
	}

	$num_deleted = TWXChatPlus::deleteChatLogEntries($num_to_keep);

	wp_send_json_success(['numDeleted'=> $num_deleted]);

	// Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_deleteChatLogEntries', 'twxchat_deleteChatLogEntries');

function twxchat_uploadKnowledgeBaseFile(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	TWXChatUtil::adminLog("twx_chat_uploadKnowledgeBaseFile::");

	if(!isset($_FILES['twx_chat_vs_upload_file'])){
        wp_send_json_error('No file uploaded', 400);
		wp_die();	
	}

	$sanitized_file = TWXChatUtil::sanitizeFileUpload( $_FILES['twx_chat_vs_upload_file'], 2 * 1024 * 1024 ); // function performs deep sanitization
	if ( is_wp_error( $sanitized_file ) ) {
        wp_send_json_error(esc_html($sanitized_file->get_error_message()), 400);
		wp_die();
	}

	if ( empty( $sanitized_file['tmp_name'] ) || ! is_uploaded_file( (string) $sanitized_file['tmp_name'] ) ) {
		wp_send_json_error( __( 'Invalid file upload', 'twx-chat' ), 400 );
		wp_die();	
	}

    if ($sanitized_file['error'] !== UPLOAD_ERR_OK) {
        wp_send_json_error('File upload error', 400);
		wp_die();	
    }

    // Read file contents
	$file_contents = @file_get_contents( $sanitized_file['tmp_name'] );
	if ( $file_contents === false ) {
		wp_send_json_error( __( 'Could not read uploaded file', 'twx-chat' ), 400 );
		wp_die();	
	}

    // Parse file contents
	$pattern = '/source:\s*(?<source>[^\n]+)\s*title:\s*(?<title>[^\n]*)\s*(?<text>.+?)(?=(source:\s*|$))/s';

    preg_match_all($pattern, $file_contents, $matches, PREG_SET_ORDER);

	$apiKey = get_option('twx_chat_options')['openai_apikey'] ?? '';

    $count = 0;
    foreach ($matches as $match) {
		$title = trim($match['title']); // can be empty ''
		$source = trim($match['source']);
		$orig_text = trim($match['text']);


		// insert/update Knowledge Base entry
		try {
			TWXChatKnowledgeBaseUtil::storeEntry($apiKey, $source, $title, $orig_text, true);
			$count++;
		} catch (Exception $ex) {
			//
		}
    }

	if ($count == 0){
		wp_send_json_error("No content uploaded. Ensure your file contains Text/ASCII only and contains 'source:' delimiters, eg:<br/>source: https://mywebsite.com/page1<br/>content here for page 1<br/>source: https://mywebsite.com/page2<br/>conent here for page2 etc");
		wp_die();	
	}

    // Send parsed results back to the client
	wp_send_json_success("$count sources uploaded successfully into the Knowledge Base.");

    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_uploadKnowledgeBaseFile', 'twxchat_uploadKnowledgeBaseFile');

function twxchat_checkForPluginUpdates(){

	// AJAX Handler to check TWXChat server for Plugin Updates
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

	if(!class_exists('TWXChatPlus')){
        wp_send_json_error('Free plan uses Wordpress.org updates');
		wp_die();
	}

	$updates = TWXChatPlus::checkForPluginUpdates();

	if(isset($updates['error'])){
        wp_send_json_error($updates['error']);
		wp_die();
	}

	$ret = [];

	if(isset($updates['update_is_newer']) && $updates['update_is_newer']){
		$ret['new_version'] = $updates['update_version'];
		$ret['new_plan'] = $updates['update_plan'];
	}

	wp_send_json_success(json_encode($ret));
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_checkForPluginUpdates', 'twxchat_checkForPluginUpdates');

function twxchat_getSupportInfo(){

	// AJAX Handler
    check_ajax_referer( 'twx_chat_nonce', 'nonce' );

    $options = get_option('twx_chat_options');
	$datetime = new DateTime( current_time('mysql'), wp_timezone() );

	$info = [];
	$info['timezone'] = wp_timezone_string();
	$info['current_time'] = $datetime->format('Y-m-d H:i:s');
	$info['home_url'] = home_url();
	$info['twx_customer_key'] = $options['twx_customer_key'] ?? null;
	$info['twx_chat_server_token'] = get_option('twx_chat_server_token');
	$info['chat_log'] = TWXChatUtil::getChatLogText();
	$info['admin_log'] = TWXChatUtil::getAdminLogText();
	$info['spider_log'] = TWXChatSpider::getSpiderLogText();
	$info['num_kb_entries'] = count(TWXChatKnowledgeBaseUtil::getAllKnowledgeBaseEntries());
	$info['num_chat_log_entries'] = TWXChatLog::getNumEntries();

	wp_send_json_success(['supportInfoText' => json_encode($info)]);
	
    // Always die in AJAX handlers
    wp_die();
}
add_action('wp_ajax_twx_chat_getSupportInfo', 'twxchat_getSupportInfo');


//// END AJAX HANDLERS ////


function twxchat_getEditChatbotUserFieldsHtml($att, $val){

	// Called by Name from within twxchat_getEditChatbotAdminHTML()
	// Renders control for entry of user field definitions, flashcard title and webhook for processing

	$userFields = [];
	if(isset($val['fields']) && !empty($val['fields'])) $userFields = $val['fields'];

	$newButtonHtml = '<button id="twx-chat-chatbot-new-user-field-btn" class="button-primary">Create New Field</button>';

	if(empty($userFields)) return $newButtonHtml.'<table id="twx-chat-chatbot-user-fields-table" class="widefat striped no-margin" style="display:none;"></table>';

	$html = $newButtonHtml.'<table id="twx-chat-chatbot-user-fields-table" class="widefat striped no-margin"><thead><tr><th>#</th><th>Name</th><th>Description</th><th>Actions</th></tr></thead><tbody>';
	$idx = 0;
	foreach($userFields as $field){
		$html .= '<tr><td>'.($idx+1).'</td>'
		 . '<td><input size="30px" type="text" name="user_fields[fields]['.$idx.'][name]" class="twx_chatbot_att chatbot_user_fields_name" value="'.$field['name'].'" required></td>'
		 . '<td><input size="100px" type="text" name="user_fields[fields]['.$idx.'][description]" class="twx_chatbot_att" value="'.$field['description'].'"></td>'
		 . '<td><button class="button-secondary twx-chat-chatbot-user-field-delete-btn" data-idx="'.$idx.'">Delete</button></td>'
		 . '</tr>';
		$idx++;
	}

	$flashcardTitleHtml = '<div class="field-group"><label style="display:block; font-weight:600; margin-bottom:4px;">Flashcard Title (if empty, no Flashcard will show to the User)</label>
  			<input type="text" name="user_fields[flashcard_title]" placeholder="eg: Your info" class="twx_chatbot_att regular-text" size="100px" value="'.($val['flashcard_title'] ?? '').'" /></div>';

	$webhookHtml = '<div class="field-group"><label style="display:block; font-weight:600; margin-bottom:4px;">Update Webhook URL</label>
  			<input type="text" name="user_fields[update_webhook_url]" class="twx_chatbot_att regular-text" size="100px" value="'.($val['update_webhook_url'] ?? '').'" /></div>';

	$html .= '</tbody></table>';
	return $html.$flashcardTitleHtml.$webhookHtml;
}

