<?php

namespace f12_cf7_captcha\core\timer;

use f12_cf7_captcha\core\wpdb;
use Forge12\Shared\LoggerInterface;
use RuntimeException;

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

/**
 * Class Captcha Timer
 * Generate the custom captcha as an image
 *
 * @package forge12\contactform7
 */
class CaptchaTimer {

	/**
	 * The unique ID
	 *
	 * @var int
	 */
	private $id = 0;
	/**
	 * The identifier used in the contact form
	 *
	 * @var string
	 */
	private $hash = '';
	/**
	 * The value - stores the time in milliseconds
	 *
	 * @var float|string
	 */
	private $value = '';
	/**
	 * The datetime whenever the captcha code has been created
	 *
	 * @var string
	 */
	private $createtime = '';

	/**
	 * @var string
	 */
	private $updatetime = '';
	private LoggerInterface $logger;

	/**
	 * Create a new Captcha Object
	 *
	 * @param $object
	 */
	public function __construct(LoggerInterface $logger, $params = array())
	{
		$this->logger = $logger;

		$this->get_logger()->info('Constructor started.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		$this->set_params($params);

		$this->get_logger()->debug('Initialization with parameters completed.', [
			'params_count' => count($params),
		]);
	}

	private function get_logger(): LoggerInterface {
		return $this->logger;
	}

	/**
	 * Set the parameters for the object.
	 *
	 * @param array $params An associative array of parameter values.
	 *                      The keys of the array should correspond to the property names of the object.
	 *                      The values of the array should be the new values for the corresponding properties.
	 *
	 * @return void
	 */
	private function set_params(array $params)
	{
		$this->get_logger()->info('Attempting to apply parameters to class properties.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'given_params_count' => count($params),
		]);

		foreach ($params as $key => $value) {
			if (isset($this->{$key})) {
				$this->{$key} = $value;
				$this->get_logger()->debug("Parameter '{$key}' was successfully set.", [
					'value' => $value,
				]);
			} else {
				$this->get_logger()->warning("Parameter '{$key}' does not exist as a class property. It will be skipped.");
			}
		}

		$this->get_logger()->info('Parameter initialization completed.');
	}

	/**
	 * Retrieves the table name for the CF7 Captcha Timer plugin.
	 *
	 * This method returns the name of the table that is used by the CF7 Captcha Timer plugin for storing data.
	 *
	 * @return string The table name for the CF7 Captcha Timer plugin.
	 *
	 * @throws RuntimeException When the global $wpdb is not defined.
	 */
	public function get_table_name(): string
	{
		global $wpdb;

		$this->get_logger()->info('Attempting to retrieve the timer database table name.');

		if (null === $wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

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

		$this->get_logger()->debug('Table name successfully retrieved.', [
			'table_name' => $table_name,
		]);

		return $table_name;
	}

	/**
	 * Creates a table in the database.
	 *
	 * This method uses the WordPress function dbDelta() to create a table with the specified structure.
	 * The table name is generated by calling the get_table_name() method.
	 *
	 * @return void
	 */
	public function create_table(): void
	{
		$this->get_logger()->info('Starting database table creation.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		// Load the WordPress upgrade functionality.
		require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
		$this->get_logger()->debug('WordPress upgrade functionality has been loaded.');

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Table creation aborted.', [
				'error' => $e->getMessage(),
			]);
			return;
		}

		global $wpdb;

		$sql = sprintf( "CREATE TABLE %s (
                id int(11) NOT NULL auto_increment, 
                hash varchar(255) NOT NULL, 
                value varchar(255) NOT NULL,
                createtime varchar(255) DEFAULT '',
                PRIMARY KEY  (id),
                KEY hash (hash)
            )", $table_name );

		$this->get_logger()->debug('SQL statement for table creation prepared.', [
			'sql' => $sql,
		]);

		// Execute the dbDelta function.
		dbDelta($sql);

		// There is no "table_exists" method in the class.
		// A success check cannot be logged.
		$this->get_logger()->info('Table creation completed.');
	}

	/**
	 * Delete the specified table from the database
	 *
	 * @throws RuntimeException if WPDB is not defined
	 */
	public function delete_table(): void
	{
		$this->get_logger()->info('Starting the process to delete the database table and cron job.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		global $wpdb;

		if (null === $wpdb) {
			$error_message = 'WPDB is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Deletion process aborted.', [
				'error' => $e->getMessage(),
			]);
			return;
		}

		// Delete the database table
		$sql = sprintf("DROP TABLE IF EXISTS %s", $table_name);
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query($sql);
		$this->get_logger()->info('Database table has been deleted (if it existed).', ['table_name' => $table_name]);

		// Delete the scheduled cron job
		wp_clear_scheduled_hook('dailyCaptchaTimerClear');
		$this->get_logger()->info('Scheduled cron job "dailyCaptchaTimerClear" has been deleted.');

		$this->get_logger()->info('Table and cron job deletion process completed successfully.');
	}


	/**
	 * Get the ID of the object
	 *
	 * @return int The ID of the object
	 */
	public function get_id(): int
	{
		$this->get_logger()->debug('Retrieving the object ID.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'id' => $this->id,
		]);

		return $this->id;
	}

	/**
	 * Set the id of the object
	 *
	 * @param int $id The id to be set
	 *
	 * @return void
	 */
	public function set_id(int $id): void
	{
		$this->get_logger()->info('Setting the object ID.', [
			'class'  => __CLASS__,
			'method' => __METHOD__,
			'old_id' => $this->id,
			'new_id' => $id,
		]);

		$this->id = $id;

		$this->get_logger()->debug('ID successfully set to ' . $id . '.');
	}


	/**
	 * Retrieves the hash value generated for this object.
	 * If the hash value is not yet generated, it calls the generateHash method to generate one and stores it.
	 *
	 * @return string The hash value.
	 */
	public function get_hash(string $user_ip_address = ''): string
	{
		$this->get_logger()->info('Attempting to retrieve the hash value.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'current_hash_state' => empty($this->hash) ? 'empty' : 'present',
		]);

		// Generate a new hash if the current object does not have one
		if (empty($this->hash)) {
			$this->get_logger()->debug('Current hash value is empty. Generating a new one.');
			$this->hash = $this->generate_hash($user_ip_address);
			$this->get_logger()->info('New hash value has been generated.');
		} else {
			$this->get_logger()->debug('Hash value already exists. Returning the existing value.');
		}

		return $this->hash;
	}

	/**
	 * Generates a hash string using the user's IP address concatenated with the current time.
	 *
	 * @param string $user_ip_address The IP address of the user.
	 *
	 * @return string The generated hash string.
	 */
	private function generate_hash(string $user_ip_address): string
	{
		$this->get_logger()->info('Generating a new unique hash value.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		// Generate a unique, hard-to-guess string
		$data_to_hash = microtime(true) . $user_ip_address . uniqid('', true);

		// Create the hash using password_hash to strengthen security
		$hash = password_hash($data_to_hash, PASSWORD_DEFAULT);

		$this->get_logger()->debug('Hash generation completed.', [
			'generated_hash_length' => strlen($hash),
		]);

		return $hash;
	}

	/**
	 * Checks if the hash is valid.
	 *
	 * @return bool Returns true if the hash is valid, false otherwise.
	 */
	private function is_valid_hash(): bool
	{
		$is_valid = !empty($this->hash);

		$this->get_logger()->debug('Checking the validity of the hash value.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'hash_state' => $is_valid ? 'valid' : 'invalid',
		]);

		return $is_valid;
	}


	/**
	 * Retrieves the value of the property.
	 *
	 * @TODO Check if that ever returns anything else than a float. - should be the time in ms for timer validation
	 *
	 * @return string|float The value of the property as a string.
	 */
	public function get_value() {
		return $this->value;
	}

	/**
	 * Sets the value of the property.
	 *
	 * @TODO Check if that ever returns anything else than a float. - should be the time in ms for timer validation
	 *
	 * @param $value The value to be set.
	 *
	 * @return void
	 */
	public function set_value($value)
	{
		$this->get_logger()->info('Setting the object value.', [
			'class'  => __CLASS__,
			'method' => __METHOD__,
			'old_value' => $this->value,
			'new_value' => $value,
		]);

		$this->value = $value;

		$this->get_logger()->debug('Value successfully set.');
	}

	/**
	 * Returns the create time of the object.
	 *
	 * If the create time is not set, it will be generated using the current
	 * date and time.
	 *
	 * @return string The create time in the format 'Y-m-d H:i:s'
	 */
	public function get_create_time(): string
	{
		$this->get_logger()->info('Attempting to retrieve the creation time.');

		// Check if the creation time is already set
		if (empty($this->createtime)) {
			$this->get_logger()->debug('Creation time is empty. Generating a new timestamp.');

			try {
				// Create a new DateTime object with the current time
				$dt = new \DateTime();
				$this->createtime = $dt->format('Y-m-d H:i:s');
				$this->get_logger()->info('New timestamp successfully generated.', ['createtime' => $this->createtime]);
			} catch (\Exception $e) {
				$this->get_logger()->error('Error creating the DateTime object.', ['error' => esc_html($e->getMessage())]);
				// Optional: Return a default timestamp on error
				return date('Y-m-d H:i:s');
			}
		} else {
			$this->get_logger()->debug('Creation time already exists. Returning the existing value.');
		}

		return $this->createtime;
	}

	/**
	 * Returns the create time of the object.
	 *
	 * If the create time is not set, it will be generated using the current
	 * date and time.
	 *
	 * @return string The create time in the format 'Y-m-d H:i:s'
	 */
	public function get_update_time(): string
	{
		$this->get_logger()->info('Attempting to retrieve the update time.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		// Check if the update time is already set.
		if (empty($this->updatetime)) {
			$this->get_logger()->debug('Update time is empty. Generating a new timestamp.');

			try {
				// Create a new DateTime object with the current time.
				$dt = new \DateTime();
				$this->updatetime = $dt->format('Y-m-d H:i:s');
				$this->get_logger()->info('New timestamp successfully generated.', ['updatetime' => $this->updatetime]);
			} catch (\Exception $e) {
				$this->get_logger()->error('Error creating the DateTime object.', ['error' => esc_html($e->getMessage())]);
				// Optional: Return a default timestamp on error.
				return date('Y-m-d H:i:s');
			}
		} else {
			$this->get_logger()->debug('Update time already exists. Returning the existing value.');
		}

		return $this->updatetime;
	}

	/**
	 * Sets the creation time of the object.
	 *
	 * This function sets the creation time of the object to the current date and time in the format 'Y-m-d H:i:s'.
	 * It updates the 'updatetime' property with the current date and time.
	 *
	 * @return void
	 */
	private function set_create_time(): void
	{
		$this->get_logger()->info('Setting the creation time for the object.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		try {
			$dt = new \DateTime();
			$this->createtime = $dt->format('Y-m-d H:i:s');
			$this->get_logger()->debug('Creation time successfully set.', ['createtime' => $this->createtime]);
		} catch (\Exception $e) {
			$this->get_logger()->error('Error creating the DateTime object. Creation time could not be set.', [
				'error' => $e->getMessage(),
			]);
		}
	}

	/**
	 * Retrieves a CaptchaTimer object by its ID.
	 *
	 * @param int $id The ID of the CaptchaTimer to retrieve.
	 *
	 * @return CaptchaTimer|null The retrieved CaptchaTimer object, or null if not found.
	 * @throws RuntimeException If WPDB global variable is not defined.
	 */
	public function get_by_id(int $id): ?CaptchaTimer
	{
		$this->get_logger()->info('Attempting to retrieve a captcha timer by ID.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'id' => $id,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Query aborted.', [
				'error' => $e->getMessage(),
			]);
			return null;
		}

		// Use a secure method to prevent SQL injection.
		// The WordPress prepare function is ideal for this.
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $id);
		$this->get_logger()->debug('SQL query prepared.', ['sql' => $sql]);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results($sql, ARRAY_A);

		if (empty($results)) {
			$this->get_logger()->info('No captcha timer entry found for the given ID.', ['id' => $id]);
			return null;
		}

		// Create a CaptchaTimer object from the first result
		$timer = new CaptchaTimer($this->get_logger(), $results[0]);

		$this->get_logger()->info('Captcha timer entry successfully retrieved by ID.', [
			'id' => $timer->get_id(),
			'hash' => $timer->get_hash(),
		]);

		return $timer;
	}

	/**
	 * Retrieves the CaptchaTimer object from the database based on the given hash.
	 *
	 * @param string $hash The hash value associated with the CaptchaTimer object.
	 *
	 * @return CaptchaTimer|null The retrieved CaptchaTimer object if found, or null if not found.
	 *
	 * @throws RuntimeException When the global $wpdb is not defined.
	 */
	public function get_by_hash(string $hash): ?CaptchaTimer
	{
		$this->get_logger()->info('Attempting to retrieve a captcha timer by hash.', [
			'class'  => __CLASS__,
			'method' => __METHOD__,
			'hash'   => $hash,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Query aborted.', [
				'error' => $e->getMessage(),
			]);
			return null;
		}

		// Use $wpdb->prepare() for SQL injection prevention.
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare("SELECT * FROM {$table_name} WHERE hash = %s", $hash);
		$this->get_logger()->debug('SQL query prepared.', ['sql' => $sql]);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results($sql, ARRAY_A);

		if (empty($results)) {
			$this->get_logger()->info('No captcha timer entry found for the given hash.', ['hash' => $hash]);
			return null;
		}

		// Since we know the array is not empty, we can use the first entry.
		$timer = new CaptchaTimer($this->get_logger(), $results[0]);
		$this->get_logger()->info('Captcha timer entry successfully retrieved by hash.', [
			'id' => $timer->get_id(),
			'hash' => $timer->get_hash(),
		]);

		return $timer;
	}

	/**
	 * Deletes the object by its hash.
	 *
	 * @return bool Returns `true` if the deletion is successful, `false` otherwise.
	 */
	public function delete(): bool
	{
		$this->get_logger()->info('Attempting to delete the current timer entry.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'hash' => $this->hash,
		]);

		if (empty($this->hash)) {
			$this->get_logger()->warning('No hash value available for deletion. Operation aborted.');
			return false;
		}

		$is_deleted = $this->delete_by_hash($this->hash);

		if ($is_deleted) {
			$this->get_logger()->info('Timer entry successfully deleted.');
			// Optional: Set the internal hash to null to prevent multiple deletions.
			$this->hash = null;
		} else {
			$this->get_logger()->error('Error deleting the timer entry.');
		}

		return $is_deleted;
	}


	/**
	 * Deletes a record from the database table based on the given hash.
	 *
	 * @param string $hash The hash of the record to be deleted.
	 *
	 * @return bool True if the record was successfully deleted, false otherwise.
	 * @throws RuntimeException If WPDB global variable is not defined.
	 */
	public function delete_by_hash(string $hash): bool
	{
		$this->get_logger()->info('Starting the deletion process based on the hash value.', [
			'class'  => __CLASS__,
			'method' => __METHOD__,
			'hash'   => $hash,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Deletion process aborted.', [
				'error' => $e->getMessage(),
			]);
			return false;
		}

		// Use $wpdb->prepare() for SQL injection prevention.
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare("DELETE FROM {$table_name} WHERE hash = %s", $hash);
		$this->get_logger()->debug('SQL delete query prepared.', ['sql' => $sql]);

		// Execute the query. query() returns the number of affected rows or false on error.
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$result = $wpdb->query($sql);

		if ($result === false) {
			$this->get_logger()->error('Error executing the delete query.', [
				'db_error' => $wpdb->last_error,
			]);
			return false;
		}

		$this->get_logger()->info('Deletion process completed successfully.', [
			'rows_deleted' => $result,
			'hash' => $hash,
		]);

		// Return the result of the query. True if one or more rows were deleted.
		return (bool)$result;
	}


	/**
	 * Checks if the current instance of the class is an update operation.
	 *
	 * @return bool Returns true if the instance is an update operation, otherwise false.
	 */
	private function is_update(): bool
	{
		$is_update = ($this->id != 0);

		$this->get_logger()->debug('Checking whether the object should be updated or newly created.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'current_id' => $this->id,
			'is_update' => $is_update,
		]);

		return $is_update;
	}

	/**
	 * Reset the table in the database.
	 *
	 * This method deletes all rows from the table specified by the get_table_name() method.
	 * It uses the global $wpdb object for executing the SQL query.
	 *
	 * @return int The number of rows affected by the delete operation.
	 * @throws RuntimeException If $wpdb is not defined.
	 *
	 */
	public function reset_table(): int
	{
		$this->get_logger()->info('Starting the process to clear the entire table.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Reset aborted.', [
				'error' => $e->getMessage(),
			]);
			return 0;
		}

		// Use sprintf to prepare the query since no user input is being processed here.
		$sql = sprintf('TRUNCATE TABLE %s', $table_name);
		// TRUNCATE is generally faster and more efficient than DELETE without a WHERE clause.
		// However, there is no return of the number of deleted rows.

		// Alternatively, to get the number of deleted rows:
		$sql = sprintf('DELETE FROM %s', $table_name);

		$this->get_logger()->debug('SQL query prepared for table reset.', ['sql' => $sql]);

		// Execute the query and get the number of affected rows.
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$rows_deleted = $wpdb->query($sql);

		if ($rows_deleted === false) {
			$this->get_logger()->error('Error executing the table reset query.', [
				'db_error' => $wpdb->last_error,
			]);
			return 0;
		}

		$this->get_logger()->info('Table successfully reset.', [
			'rows_deleted' => $rows_deleted,
		]);

		return $rows_deleted;
	}

	/**
	 * Deletes records from the database that are older than the specified create time.
	 *
	 * @param string $create_time The create time to compare against.
	 *
	 * @return int Returns the number of affected rows after the delete operation.
	 *
	 * @throws RuntimeException When WPDB is not defined.
	 */
	public function delete_older_than(string $create_time): int
	{
		$this->get_logger()->info('Starting the deletion process for entries older than a specified timestamp.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'create_time' => $create_time,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Deletion process aborted.', [
				'error' => $e->getMessage(),
			]);
			return 0;
		}

		// Use $wpdb->prepare() for SQL injection prevention.
		// The timestamp should be treated as a string (%s).
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare("DELETE FROM {$table_name} WHERE createtime < %s", $create_time);
		$this->get_logger()->debug('SQL delete query prepared.', ['sql' => $sql]);

		// Execute the query and get the number of affected rows.
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$rows_deleted = $wpdb->query($sql);

		if ($rows_deleted === false) {
			$this->get_logger()->error('Error executing the delete query.', [
				'db_error' => $wpdb->last_error,
			]);
			return 0;
		}

		$this->get_logger()->info('Old timer entries successfully deleted.', [
			'rows_deleted' => $rows_deleted,
		]);

		return (int)$rows_deleted;
	}

	/**
	 * Retrieves the number of entries in the specified table.
	 *
	 * @return int The number of entries in the table.
	 * @throws RuntimeException If WPDB is not defined.
	 */
	public function get_count(): int
	{
		$this->get_logger()->info('Attempting to retrieve the total number of entries in the table.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Query aborted.', [
				'error' => $e->getMessage(),
			]);
			return 0;
		}

		// Use sprintf for the query since no user input is being used here.
		$sql = sprintf('SELECT count(*) AS entries FROM %s', $table_name);
		$this->get_logger()->debug('SQL query prepared.', ['sql' => $sql]);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results($sql);

		if (is_array($results) && isset($results[0])) {
			$count = (int)$results[0]->entries;
			$this->get_logger()->info("Number of entries in the table: {$count}.");
			return $count;
		}

		$this->get_logger()->warning('Database query result was invalid or empty. Returning 0.');

		return 0;
	}

	/**
	 * Save the current object to the database.
	 *
	 * If the object's ID is not set, a new row will be inserted into the database.
	 * If the object's ID is set, the corresponding row in the database will be updated.
	 *
	 * @return int|bool The number of rows affected on successful update, or the ID of the inserted row on
	 *                  successful insert. False on failure.
	 * @throws RuntimeException If wpdb is not defined.
	 *
	 * @global wpdb $wpdb WordPress database object.
	 *
	 */
	public function save()
	{
		$this->get_logger()->info('Starting the save process for the timer entry.', [
			'class' => __CLASS__,
			'method' => __METHOD__,
			'action' => $this->is_update() ? 'update' : 'insert',
		]);

		global $wpdb;

		if (!$wpdb) {
			$error_message = 'The global variable $wpdb is not defined.';
			$this->get_logger()->error($error_message);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new RuntimeException( $error_message );
		}

		try {
			$table_name = $this->get_table_name();
		} catch (RuntimeException $e) {
			$this->get_logger()->error('Error retrieving table name. Save process aborted.', [
				'error' => $e->getMessage(),
			]);
			throw $e;
		}

		$data = [
			'hash'       => $this->get_hash(),
			'value'      => $this->get_value(),
			'createtime' => $this->get_create_time(),
		];

		$result = false;
		if ($this->is_update()) {
			$where = ['id' => $this->get_id()];
			$this->get_logger()->debug('Updating existing entry.', ['data' => $data, 'where' => $where]);
			$result = $wpdb->update($table_name, $data, $where);
		} else {
			$this->get_logger()->debug('Adding new entry.', ['data' => $data]);
			$result = $wpdb->insert($table_name, $data);
			if ($result !== false) {
				$this->id = $wpdb->insert_id;
				$this->get_logger()->info('New entry successfully added. ID: ' . $this->id);
			}
		}

		if ($result === false) {
			$this->get_logger()->warning('Insert failed, attempting to recreate table', [
				'db_error' => $wpdb->last_error,
				'class'    => __CLASS__,
				'method'   => __METHOD__,
			]);

			$this->create_table();

			if ($this->is_update()) {
				$result = $wpdb->update($table_name, $data, ['id' => $this->get_id()]);
			} else {
				$result = $wpdb->insert($table_name, $data);
				if ($result !== false) {
					$this->id = $wpdb->insert_id;
				}
			}

			if ($result === false) {
				$this->get_logger()->error('Insert failed again after table creation', [
					'db_error' => $wpdb->last_error,
					'class'    => __CLASS__,
					'method'   => __METHOD__,
				]);
				throw new RuntimeException('Database error occurred.');
			}
		}

		$this->get_logger()->info('Save process completed successfully.', [
			'result' => $result,
		]);

		return $result;
	}
}