<?php
/**
 * Chat Log Manager.
 *
 * Centralizes all chat log operations including database table management,
 * message storage, retrieval, and updates. Messages are keyed by thread_id
 * (bigint foreign key to threads table).
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://maximum.software/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	return;
}

/**
 * Chat_Log_Manager class.
 */
class Chat_Log_Manager extends Manager {

	/**
	 * Constructor - initializes the manager
	 * @since     1.0.0
	 */
	protected function __construct() {
		// Call parent constructor with option suffix
		parent::__construct( 'chat_log_settings' );
	}

	/**
	 * Get default configuration
	 *
	 * Returns default chat log settings values.
	 *
	 * @return array Default configuration
	 * @since     1.0.0
	 */
	protected function get_default_config() {
		// No configuration needed - chat log persists until user deletes it
		return array();
	}

	/**
	 * Plugin activation tasks for chat log
	 * @since     1.0.0
	 */
	public function activate_plugin() {
		// Call parent method to set default config
		parent::activate_plugin();

		// Create database tables
		$this->create_database_tables();
	}

	/**
	 * Uninstall plugin - removes all chat log data
	 * @since     1.0.0
	 */
	public function uninstall_plugin() {
		// Call parent method to delete config
		parent::uninstall_plugin();

		// Delete chat log table
		$this->drop_database_tables();
	}

	/**
	 * Get the table name for chat log.
	 *
	 * @return string The full table name with WordPress prefix
	 * @since     1.0.0
	 */
	public function get_table() {
		global $wpdb;
		return $wpdb->prefix . 'agenistrator_chat_log';
	}

	/**
	 * Create chat log database table
	 */
	private function create_database_tables() {
		global $wpdb;
		$charset_collate = $wpdb->get_charset_collate();

		// Chat log table for storing chat messages
		$chat_log_table_name = $this->get_table();
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name is a hardcoded string literal with a trusted prefix, not user input.
		$sql_chat_log = "CREATE TABLE {$chat_log_table_name} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			thread_id bigint(20) unsigned NOT NULL,
			role varchar(20) NOT NULL,
			content text NULL,
			metadata JSON NULL,
			timestamp bigint(20) NOT NULL,
			PRIMARY KEY (id),
			KEY thread_timestamp (thread_id, timestamp)
		) {$charset_collate};";

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

	/**
	 * Drop chat log database table
	 */
	private function drop_database_tables() {
		global $wpdb;
		$chat_log_table_name = $this->get_table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Custom table DDL; table name is a hardcoded string literal with a trusted prefix, not user input; caching not applicable.
		$wpdb->query( "DROP TABLE IF EXISTS {$chat_log_table_name}" );
	}

	/**
	 * Get chat log for a thread
	 *
	 * @param int $thread_id The thread ID.
	 * @return Message[] Chat log as Message instances.
	 * @since     1.0.0
	 */
	public function get_chat_log( $thread_id ) {
		global $wpdb;
		$table_name = $this->get_table();

		// Initialize chat log array
		$chat_log = array();

		// Get messages from the database for this thread
		// Use 'id AS message_id' to get the correct key name directly
		// Order by id (not timestamp) because streaming updates modify timestamp
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; table name from get_table() is hardcoded; caching not applicable.
		$messages = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded
				"SELECT id AS message_id, role, content, metadata, timestamp FROM {$table_name}
				WHERE thread_id = %d
				ORDER BY id ASC",
				$thread_id
			),
			ARRAY_A
		);

		if ( ! empty( $messages ) ) {
			// Process database results and convert to Message instances
			foreach ( $messages as $message ) {
				// Decode metadata JSON
				if ( ! empty( $message['metadata'] ) ) {
					$message['metadata'] = json_decode( $message['metadata'], true );
				} else {
					$message['metadata'] = null;
				}

				// Cast numeric fields to proper types
				$message['message_id'] = (int) $message['message_id'];
				$message['timestamp']  = (int) $message['timestamp'];

				// Create Message instance from database result
				$message_instance = Message::from_array( $message );
				if ( null !== $message_instance ) {
					$chat_log[] = $message_instance;
				}
			}
		}

		return $chat_log;
	}

	/**
	 * Create a temporary message record in chat log
	 *
	 * This method creates a placeholder message (typically with empty content)
	 * that can be updated later during streaming or processing.
	 *
	 * @param Thread $thread The thread instance.
	 * @param string $role   Either 'user' or 'assistant'.
	 * @return int|false The inserted ID or false on failure.
	 * @since     1.0.0
	 */
	public function create_message( Thread $thread, $role ) {
		global $wpdb;
		$table_name = $this->get_table();

		// Insert placeholder message
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table; uses internal $wpdb methods.
		$result = $wpdb->insert(
			$table_name,
			array(
				'thread_id' => $thread->get_id(),
				'role'      => $role,
				'content'   => '',
				'timestamp' => time(),
			),
			array( '%d', '%s', '%s', '%d' )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Update an existing message in chat log
	 *
	 * @param int $message_id The message ID to update
	 * @param string $content The new message content
	 * @param int|null $timestamp Optional timestamp (defaults to current time)
	 * @return bool True on success, false on failure
	 * @since     1.0.0
	 */
	public function update_message( $message_id, $content, $timestamp = null ) {
		global $wpdb;
		$timestamp  = $timestamp ?? time();
		$table_name = $this->get_table();

		// Update message
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable.
		$result = $wpdb->update(
			$table_name,
			array(
				'content'   => $content,
				'timestamp' => $timestamp,
			),
			array(
				'id' => $message_id,
			),
			array( '%s', '%d' ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Update message metadata only
	 *
	 * @param int $message_id The message ID to update
	 * @param array $metadata The metadata array
	 * @return bool True on success, false on failure
	 * @since     1.0.0
	 */
	public function update_message_metadata( $message_id, array $metadata ) {
		global $wpdb;
		$table_name = $this->get_table();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable.
		$result = $wpdb->update(
			$table_name,
			array( 'metadata' => wp_json_encode( $metadata ) ),
			array( 'id' => $message_id ),
			array( '%s' ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Get metadata for a specific message
	 *
	 * @param int $message_id The message ID
	 * @return array|null Metadata array or null if not found
	 * @since     1.0.0
	 */
	public function get_message_metadata( $message_id ) {
		global $wpdb;
		$table_name = $this->get_table();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; table name from get_table() is hardcoded; caching not applicable.
		$metadata_json = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT metadata FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded
				$message_id
			)
		);

		if ( null === $metadata_json ) {
			return null;
		}

		$metadata = json_decode( $metadata_json, true );
		return is_array( $metadata ) ? $metadata : null;
	}

	/**
	 * Delete all messages for a thread
	 *
	 * @param Thread $thread The thread instance.
	 * @return bool True on success, false on failure.
	 * @since     1.0.0
	 */
	public function delete_all_messages( Thread $thread ) {
		global $wpdb;
		$table_name = $this->get_table();

		// Delete all messages from database
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable.
		$result = $wpdb->delete(
			$table_name,
			array( 'thread_id' => $thread->get_id() ),
			array( '%d' )
		);

		// If deletion was successful, send control event to notify all browser windows/tabs
		if ( false !== $result ) {
			$control_event = new Events\Control_Event(
				$thread->get_user_id(),
				$thread->get_id(),
				Events\Control_Event::ACTION_CLEAR,
				array()
			);
			$control_event->push();
		}

		return false !== $result;
	}

	/**
	 * Delete a specific message by ID
	 *
	 * @param int $message_id The message ID to delete
	 * @return bool True on success, false on failure
	 * @since     1.0.0
	 */
	public function delete_message( $message_id ) {
		global $wpdb;
		$table_name = $this->get_table();

		// Delete the specific message from database
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable.
		$result = $wpdb->delete(
			$table_name,
			array( 'id' => $message_id ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Delete all messages for a thread (silent, no SSE events)
	 *
	 * Used for cascade deletes when a thread or user is deleted.
	 *
	 * @param int $thread_id The thread ID.
	 * @since     1.0.0
	 */
	public function delete_messages_by_thread_id( $thread_id ) {
		global $wpdb;
		$table_name = $this->get_table();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable.
		$wpdb->delete(
			$table_name,
			array( 'thread_id' => $thread_id ),
			array( '%d' )
		);
	}

	/**
	 * Add a complete message to chat log with SSE notification
	 *
	 * This is the primary method for adding messages to chat log.
	 * It stores the message and sends real-time notifications via SSE.
	 *
	 * @param Thread  $thread      The thread instance.
	 * @param Message $message     The message instance to add.
	 * @param bool    $is_complete Whether the message is complete (defaults to true).
	 * @return int|false The inserted message ID or false on failure.
	 * @since     1.0.0
	 */
	public function add_message( Thread $thread, Message $message, $is_complete = true ) {
		global $wpdb;
		$table_name = $this->get_table();

		// Encode metadata as JSON if provided
		$metadata_json = null;
		if ( $message->get_metadata() !== null ) {
			$metadata_json = wp_json_encode( $message->get_metadata() );
		}

		// Insert message directly
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table; uses internal $wpdb methods.
		$result = $wpdb->insert(
			$table_name,
			array(
				'thread_id' => $thread->get_id(),
				'role'      => $message->get_role(),
				'content'   => $message->get_content() ?? '', // Convert null to empty string
				'metadata'  => $metadata_json,
				'timestamp' => $message->get_timestamp(),
			),
			array( '%d', '%s', '%s', '%s', '%d' )
		);

		if ( ! $result ) {
			return false;
		}

		$message_id = $wpdb->insert_id;

		// Set the message ID on the message instance
		$message->set_message_id( $message_id );

		// Create message event
		$message_event = new Events\Message_Event(
			$thread->get_user_id(),
			$thread->get_id(),
			$message,
			$is_complete
		);
		$message_event->push();

		return $message_id;
	}

	/**
	 * Add welcome message to the chat log
	 *
	 * @param Thread $thread The thread instance.
	 * @return int|false The message ID of the welcome message or false on failure.
	 * @since     1.0.0
	 */
	public function add_welcome_message( Thread $thread ) {
		// Check if a chat model is configured
		if ( ! LLM_Manager::get_instance()->is_chat_model_configured() ) {
			// Create a markdown response with a link to the settings page
			$settings_url    = admin_url( 'admin.php?page=agenistrator-settings' );
			$welcome_content = String_Utils::replace_tags(
				__( "**Hello!** 👋 I'm your WordPress assistant. To get started, please [configure a provider and chat model]({settings_url}) in the settings.", 'agenistrator' ),
				array( 'settings_url' => $settings_url )
			);
		} else {
			$welcome_content = __( "**Hello!** 👋 I'm your WordPress assistant, ready to help you with your website management tasks!", 'agenistrator' );
		}

		// Create an Assistant_Message instance
		$welcome_message = new Assistant_Message( $welcome_content );

		// Add to database and create message event
		return $this->add_message( $thread, $welcome_message, true );
	}
}
