<?php
/**
 * Thread Manager.
 *
 * Manages per-thread state including imported tools. Threads are identified by
 * bigint id (primary key). Each user can have multiple threads (currently one
 * "default" thread per user).
 *
 * @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://agenistrator.com/
 *
 * 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' ) ) {
	exit;
}

/**
 * Thread_Manager class.
 */
class Thread_Manager extends Manager {

	/**
	 * Constructor - initializes the manager
	 * @since     1.0.0
	 */
	protected function __construct() {
		parent::__construct( 'thread_settings' );
	}

	/**
	 * Get default configuration
	 *
	 * @return array Default configuration
	 * @since     1.0.0
	 */
	protected function get_default_config() {
		return array();
	}

	/**
	 * Plugin activation tasks
	 * @since     1.0.0
	 */
	public function activate_plugin() {
		parent::activate_plugin();
		$this->create_database_tables();
	}

	/**
	 * Uninstall plugin - removes all thread data
	 * @since     1.0.0
	 */
	public function uninstall_plugin() {
		parent::uninstall_plugin();
		$this->drop_database_tables();
	}

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

	/**
	 * Create threads database table
	 */
	private function create_database_tables() {
		global $wpdb;
		$charset_collate = $wpdb->get_charset_collate();
		$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 = "CREATE TABLE {$table_name} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			user_id bigint(20) unsigned NOT NULL,
			title varchar(64) NOT NULL DEFAULT 'default',
			imported_tools JSON,
			content_guard varchar(16) DEFAULT NULL,
			loop_state JSON,
			PRIMARY KEY (id),
			UNIQUE KEY user_title (user_id, title)
		) {$charset_collate};";

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

	/**
	 * Drop threads database table
	 */
	private function drop_database_tables() {
		global $wpdb;
		$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 {$table_name}" );
	}

	/**
	 * Get or create a thread for a user (race-safe)
	 *
	 * Uses SELECT first to avoid unnecessary INSERT operations. Falls back to
	 * INSERT ... ON DUPLICATE KEY UPDATE for new threads to handle race conditions.
	 *
	 * @param int    $user_id The user ID.
	 * @param string $title   The thread title (default: 'default').
	 * @return Thread The thread instance.
	 * @since     1.0.0
	 */
	public function get_or_create_thread( $user_id, $title = 'default' ) {
		global $wpdb;
		$table_name = $this->get_table();
		$user_id    = (int) $user_id;

		// Try to get existing thread first (common case)
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; caching not applicable; table name is a hardcoded string literal with a trusted prefix, not user input.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded.
				"SELECT id, user_id, title FROM {$table_name} WHERE user_id = %d AND title = %s",
				$user_id,
				$title
			),
			ARRAY_A
		);

		if ( $row ) {
			return new Thread( (int) $row['id'], (int) $row['user_id'], $row['title'] );
		}

		// Thread doesn't exist, create it with atomic upsert: INSERT if not exists, otherwise set LAST_INSERT_ID to existing id.
		// This may increment AUTO_INCREMENT but that's okay since this will probably be rare.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; caching not applicable; table name is a hardcoded string literal with a trusted prefix, not user input.
		$wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded.
				"INSERT INTO {$table_name} (user_id, title) VALUES (%d, %s) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)",
				$user_id,
				$title
			)
		);

		return new Thread( (int) $wpdb->insert_id, $user_id, $title );
	}

	/**
	 * Get thread by thread ID
	 *
	 * @param int $thread_id The thread ID.
	 * @return Thread|null Thread instance or null if not found.
	 * @since     1.0.0
	 */
	public function get_thread( $thread_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.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, user_id, title FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded
				$thread_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return null;
		}

		return new Thread( (int) $row['id'], (int) $row['user_id'], $row['title'] );
	}

	/**
	 * Register hooks for cascade deletes
	 * @since     1.0.0
	 */
	public function register_hooks() {
		add_action( 'delete_user', array( $this, 'deleted_user' ) );
	}

	/**
	 * Handle user deletion - cascade delete all user's threads
	 *
	 * @param int $user_id The user ID being deleted.
	 * @since     1.0.0
	 */
	public function deleted_user( $user_id ) {
		try {
			$this->delete_threads_by_user( (int) $user_id );
		} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Silently fail - cascade deletion should not block user deletion
		}
	}

	/**
	 * Get all thread IDs for a user
	 *
	 * @param int $user_id The user ID.
	 * @return int[] Array of thread IDs.
	 * @since     1.0.0
	 */
	public function get_thread_ids_by_user( $user_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.
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$table_name} WHERE user_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded
				$user_id
			)
		);

		return array_map( 'intval', $ids );
	}

	/**
	 * Delete all threads for a user and cascade to dependent data
	 *
	 * @param int $user_id The user ID.
	 * @since     1.0.0
	 */
	public function delete_threads_by_user( $user_id ) {
		$thread_ids = $this->get_thread_ids_by_user( $user_id );

		// Cascade delete dependent data first
		foreach ( $thread_ids as $thread_id ) {
			Chat_Log_Manager::get_instance()->delete_messages_by_thread_id( $thread_id );
			Event_Queue::get_instance()->delete_events_by_thread( $thread_id );
		}

		// Delete the threads
		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( 'user_id' => $user_id ),
			array( '%d' )
		);
	}
}
