<?php
/**
 * AnyCoin Database Manager utilities.
 *
 * @package AnyCoin_Payment_Gateway
 */

defined( 'ABSPATH' ) || exit;

/**
 * Zano Database Manager
 *
 * Handles all database operations for the Zano Payment Gateway.
 * Extracted from utilities for better separation of concerns.
 */
class AnyCoin_Database_Manager {

	/**
	 * Lightweight debug logger for DB manager (no error_log in production).
	 * Uses WooCommerce logger when available and WP_DEBUG is true.
	 *
	 * @param string $message Message to log.
	 */
	private static function log_debug( $message ) {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'wc_get_logger' ) ) {
			$logger = wc_get_logger();
			$logger->debug( $message, [ 'source' => 'anycoin-db' ] );
		}
	}

	/**
	 * Get the payments table name.
	 *
	 * @return string Table name with WordPress prefix.
	 */
	public static function get_table_name() {
		global $wpdb;
		return $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
	}

	/**
	 * Build the SQL schema for the payments table.
	 *
	 * @param string $table_name     Table name.
	 * @param string $charset_collate Charset/collation string.
	 * @return string Schema SQL.
	 */
	private static function get_schema_sql( $table_name, $charset_collate ) {
		// No IF NOT EXISTS: dbDelta cannot parse it (it captures 'IF' as the
		// table name) and would never diff/alter an existing table.
		return "CREATE TABLE $table_name (
            id mediumint(9) NOT NULL AUTO_INCREMENT,
            order_id bigint(20) NOT NULL,
            payment_id varchar(64) NOT NULL,
            wallet_address text NOT NULL,
            amount decimal(15,8) NOT NULL,
            status varchar(20) NOT NULL DEFAULT '" . AnyCoin_Constants::STATUS_PENDING . "',
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            confirmations int(11) NOT NULL DEFAULT 0,
            tx_hash varchar(64) DEFAULT NULL,
            received_amount decimal(15,8) DEFAULT NULL,
            asset_id varchar(64) DEFAULT NULL,
            asset_symbol varchar(10) DEFAULT NULL,
            asset_amount decimal(15,8) DEFAULT NULL,
            received_block bigint(20) DEFAULT NULL,
            current_block bigint(20) DEFAULT NULL,
            keeper_block bigint(20) DEFAULT NULL,
            completed_at datetime DEFAULT NULL,
            PRIMARY KEY  (id),
            KEY order_id (order_id),
            KEY payment_id (payment_id),
            KEY status (status),
            KEY asset_id (asset_id),
            UNIQUE KEY tx_hash_pid (tx_hash(40),payment_id(24))
        ) $charset_collate;";
	}

	/**
	 * Create or update plugin database tables.
	 *
	 * @return bool True on success, false on failure.
	 */
	public static function create_tables() {
		global $wpdb;

		$table_name      = self::get_table_name();
		$charset_collate = $wpdb->get_charset_collate();

		$sql = self::get_schema_sql( $table_name, $charset_collate );

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        // phpcs:ignore WordPress.DB.RestrictedFunctions.dbDelta_DBDelta -- WordPress-provided migration helper for custom tables.
		$result = dbDelta( $sql );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time existence check during activation, safe table name.
		$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ) === $table_name;

		if ( ! $table_exists ) {
			self::log_debug( 'Failed to create Zano payments table' );
			return false;
		}

		self::run_table_migrations();

		return true;
	}

	/**
	 * Run table migrations for new columns.
	 *
	 * @return bool True on success.
	 */
	public static function run_table_migrations() {
		global $wpdb;

		$table_name      = self::get_table_name();
		$charset_collate = $wpdb->get_charset_collate();

		$sql = self::get_schema_sql( $table_name, $charset_collate );
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        // phpcs:ignore WordPress.DB.RestrictedFunctions.dbDelta_DBDelta -- WordPress-provided migration helper for custom tables.
		dbDelta( $sql );

		// Add the new composite index before dropping the legacy one, so the
		// table is never left without a unique constraint.
		self::ensure_tx_hash_pid_unique_index();
		self::drop_legacy_tx_hash_unique_index();

		return true;
	}

	/**
	 * Ensure the (tx_hash, payment_id) unique index exists.
	 *
	 * Explicit and idempotent: it also repairs tables where dbDelta failed to
	 * add the index (e.g. while duplicate rows still existed).
	 */
	private static function ensure_tx_hash_pid_unique_index() {
		global $wpdb;

		$table_name = self::get_table_name();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time schema inspection during migration; safe table name.
		$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ) === $table_name;
		if ( ! $table_exists ) {
			return;
		}

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier; index inspection during migration.
		$index = $wpdb->get_results( "SHOW INDEX FROM {$table_name} WHERE Key_name = 'tx_hash_pid'" );
		if ( ! empty( $index ) ) {
			return;
		}

		self::log_debug( 'Adding unique index tx_hash_pid for HF6 multi-payment support' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Controlled schema migration; safe table name.
		$wpdb->query( "ALTER TABLE {$table_name} ADD UNIQUE KEY tx_hash_pid (tx_hash(40),payment_id(24))" );
	}

	/**
	 * Drop the pre-HF6 single-column unique index on tx_hash if present.
	 *
	 * Since Zano HF6, one transaction may pay multiple orders with different
	 * payment IDs, so uniqueness is enforced on (tx_hash, payment_id) instead
	 * (see get_schema_sql). dbDelta adds new indexes but never removes old
	 * ones, hence this explicit drop.
	 */
	private static function drop_legacy_tx_hash_unique_index() {
		global $wpdb;

		$table_name = self::get_table_name();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time schema inspection during migration; safe table name.
		$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ) === $table_name;
		if ( ! $table_exists ) {
			return;
		}

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier; index inspection during migration.
		$legacy_index = $wpdb->get_results( "SHOW INDEX FROM {$table_name} WHERE Key_name = 'tx_hash'" );
		if ( empty( $legacy_index ) ) {
			return;
		}

		self::log_debug( 'Dropping legacy unique index tx_hash for HF6 multi-payment support' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.NotPrepared -- Controlled schema migration; safe table name.
		$wpdb->query( "ALTER TABLE {$table_name} DROP INDEX tx_hash" );
	}

	/**
	 * Check for and resolve duplicate transaction hashes.
	 *
	 * @return bool True on success.
	 */
	public static function resolve_duplicate_transactions() {
		global $wpdb;

		$table_name = self::get_table_name();

		self::handle_duplicate_hashes();
		$charset_collate = $wpdb->get_charset_collate();
		$sql             = self::get_schema_sql( $table_name, $charset_collate );
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        // phpcs:ignore WordPress.DB.RestrictedFunctions.dbDelta_DBDelta -- WordPress-provided migration helper for custom tables.
		dbDelta( $sql );

		// Retry after duplicate cleanup: the ADD UNIQUE can only succeed once
		// same-(tx_hash, payment_id) duplicate rows are resolved.
		self::ensure_tx_hash_pid_unique_index();

		return true;
	}

	/**
	 * Handle duplicate transaction hashes.
	 *
	 * Duplicates are grouped by (tx_hash, payment_id): since Zano HF6 a single
	 * transaction may legitimately pay multiple orders with different payment
	 * IDs, so only same-tx same-PID records are real duplicates.
	 *
	 * @return bool True on success.
	 */
	private static function handle_duplicate_hashes() {
		global $wpdb;

		$table_name = self::get_table_name();

		$duplicates = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is constructed from $wpdb->prefix and known constant.
				'SELECT tx_hash, payment_id, COUNT(*) as count FROM ' . $table_name . '
                 WHERE tx_hash IS NOT NULL
                 GROUP BY tx_hash, payment_id
                 HAVING COUNT(*) > %d',
				1
			)
		);

		if ( empty( $duplicates ) ) {
			return true;
		}

		// Log the duplicate transaction hashes.
		self::log_debug( 'Found duplicated transaction hashes in Zano payments table:' );

		foreach ( $duplicates as $duplicate ) {
			self::log_debug( 'Transaction hash: ' . $duplicate->tx_hash . ' (used ' . $duplicate->count . ' times)' );

			$records = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prepare(
					'SELECT id, order_id, status, created_at FROM ' . $table_name . ' WHERE tx_hash = %s AND payment_id = %s ORDER BY created_at ASC', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier.
					$duplicate->tx_hash,
					$duplicate->payment_id
				)
			);

			$first_record = array_shift( $records );
			self::log_debug( 'Keeping transaction for order: ' . $first_record->order_id );

			foreach ( $records as $record ) {
				self::log_debug( 'Clearing transaction hash from order: ' . $record->order_id );

				$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
					$table_name,
					[
						'tx_hash' => null,
						'status'  => AnyCoin_Constants::STATUS_PENDING,
					],
					[ 'id' => $record->id ]
				);

				$order = wc_get_order( $record->order_id );
				if ( $order ) {
					$order->add_order_note(
						sprintf(
							'Payment reset to pending: Transaction %s was already assigned to order #%s',
							$duplicate->tx_hash,
							$first_record->order_id
						)
					);
				}
			}
		}

		self::clear_status_summary_cache();
		return true;
	}

	/**
	 * Get payment record by order ID.
	 *
	 * @param int $order_id Order ID.
	 * @return array|null Payment record or null if not found.
	 */
	public static function get_payment_by_order_id( $order_id ) {
		global $wpdb;

		$table_name = self::get_table_name();
		return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE order_id = %d', $order_id ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
			ARRAY_A
		);
	}

	/**
	 * Get payment record by payment ID.
	 *
	 * @param string $payment_id Payment ID.
	 * @return array|null Payment record or null if not found.
	 */
	public static function get_payment_by_payment_id( $payment_id ) {
		global $wpdb;

		$table_name = self::get_table_name();
		return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE payment_id = %s', $payment_id ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
			ARRAY_A
		);
	}

	/**
	 * Get payment record by primary ID.
	 *
	 * @param int $payment_id Payment ID.
	 * @return array|null Payment record or null.
	 */
	public static function get_payment_by_id( $payment_id ) {
		global $wpdb;
		$table_name = self::get_table_name();
		return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE id = %d', $payment_id ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
			ARRAY_A
		);
	}

	/**
	 * Insert new payment record.
	 *
	 * @param array $payment_data Payment data.
	 * @return int|false Payment ID on success, false on failure.
	 */
	public static function insert_payment( $payment_data ) {
		global $wpdb;

		$table_name = self::get_table_name();

		$payment_data = array_merge(
			[
				'status'        => AnyCoin_Constants::STATUS_PENDING,
				'created_at'    => current_time( 'mysql' ),
				'confirmations' => 0,
			],
			$payment_data
		);

		$result = $wpdb->insert( $table_name, $payment_data ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery

		if ( $result ) {
			self::clear_status_summary_cache();
			return $wpdb->insert_id;
		}

		return false;
	}

	/**
	 * Update payment record.
	 *
	 * @param int   $payment_id  Payment ID.
	 * @param array $update_data Data to update.
	 * @return bool True on success, false on failure.
	 */
	public static function update_payment( $payment_id, $update_data ) {
		global $wpdb;

		$table_name = self::get_table_name();

		$update_data['updated_at'] = current_time( 'mysql' );

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$table_name,
			$update_data,
			[ 'id' => $payment_id ]
		);

		if ( $result !== false ) {
			self::clear_status_summary_cache();
			return true;
		}

		return false;
	}

	/**
	 * Mark expired payments as failed instead of deleting them.
	 *
	 * @param int|null $expiry_time Expiry time in seconds (null for default).
	 * @return int Number of payments marked as failed.
	 */
	public static function delete_expired_payments( $expiry_time = null ) {
		global $wpdb;

		if ( $expiry_time === null ) {
			$expiry_time = AnyCoin_Constants::DEFAULT_PAYMENT_TIMEOUT;
		}

		$table_name  = self::get_table_name();
		$cutoff_time = gmdate( 'Y-m-d H:i:s', time() - $expiry_time );

		$expired_payments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier.
				'SELECT * FROM ' . $table_name . ' 
             WHERE status = %s 
             AND tx_hash IS NULL 
             AND created_at < %s',
				AnyCoin_Constants::STATUS_PENDING,
				$cutoff_time
			),
			ARRAY_A
		);

		if ( empty( $expired_payments ) ) {
			return 0;
		}

		$result = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier.
				'UPDATE ' . $table_name . ' 
             SET status = %s, updated_at = %s
             WHERE status = %s 
             AND tx_hash IS NULL 
             AND created_at < %s',
				AnyCoin_Constants::STATUS_FAILED,
				current_time( 'mysql' ),
				AnyCoin_Constants::STATUS_PENDING,
				$cutoff_time
			)
		);

		foreach ( $expired_payments as $payment ) {
			$order = wc_get_order( $payment['order_id'] );
			if ( $order && in_array( $order->get_status(), [ 'pending', 'on-hold' ], true ) ) {
				$order->update_status( 'failed', 'Payment expired after 20 minutes without transaction' );
				$order->add_order_note( 'Payment automatically marked as failed - no transaction detected within timeout period' );
				$order->save();

				self::log_debug( "AnyCoin: Marked order #{$payment['order_id']} as failed due to payment expiration" );
			}
		}

		self::log_debug( 'AnyCoin: Marked ' . count( $expired_payments ) . ' expired payments as failed' );

		self::clear_status_summary_cache();
		return ( $result ) ? $result : 0;
	}

	/**
	 * Get all payment statuses summary.
	 *
	 * @return array Status counts.
	 */
	public static function get_payment_status_summary() {
		global $wpdb;

		$table_name = self::get_table_name();
		$cache_key  = 'anycoin_status_summary';
		$cached     = wp_cache_get( $cache_key, 'anycoin' );
		if ( false !== $cached ) {
			return $cached;
		}

		$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			'SELECT status, COUNT(*) as count FROM ' . $table_name . ' GROUP BY status', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
			ARRAY_A
		);

		$summary = [];
		foreach ( $results as $result ) {
			$summary[ $result['status'] ] = intval( $result['count'] );
		}

		wp_cache_set( $cache_key, $summary, 'anycoin' );
		return $summary;
	}

	/**
	 * Clear cached summaries.
	 */
	private static function clear_status_summary_cache() {
		wp_cache_delete( 'anycoin_status_summary', 'anycoin' );
	}

	/**
	 * Check if a transaction hash is already used by a different payment record.
	 *
	 * When $payment_id is given, the check is scoped to the same tx+payment ID
	 * combination: since Zano HF6 one transaction may legitimately pay multiple
	 * orders with different payment IDs.
	 *
	 * @param string      $tx_hash            Transaction hash.
	 * @param int         $exclude_payment_id Payment record ID to exclude.
	 * @param string|null $payment_id         Payment ID (hex) to scope the check to.
	 * @return bool True if used elsewhere.
	 */
	public static function is_tx_hash_used_elsewhere( $tx_hash, $exclude_payment_id, $payment_id = null ) {
		global $wpdb;
		$table_name = self::get_table_name();

		if ( null !== $payment_id && '' !== $payment_id ) {
			$count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prepare(
					'SELECT COUNT(*) FROM ' . $table_name . ' WHERE tx_hash = %s AND payment_id = %s AND id != %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
					$tx_hash,
					$payment_id,
					$exclude_payment_id
				)
			);
			return intval( $count ) > 0;
		}

		$count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				'SELECT COUNT(*) FROM ' . $table_name . ' WHERE tx_hash = %s AND id != %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
				$tx_hash,
				$exclude_payment_id
			)
		);
		return intval( $count ) > 0;
	}

	/**
	 * Atomically claim a transaction by setting tx_hash only if currently unclaimed.
	 *
	 * @param int   $payment_id  Payment ID.
	 * @param array $update_data Data to set (should include tx_hash and related fields).
	 * @return bool True if the row was updated (claimed).
	 */
	public static function claim_transaction_if_unclaimed( $payment_id, array $update_data ) {
		global $wpdb;
		$table_name = self::get_table_name();
		$result     = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$table_name,
			$update_data,
			[
				'id'      => $payment_id,
				'tx_hash' => null,
			]
		);
		if ( $result !== false && $result > 0 ) {
			self::clear_status_summary_cache();
			return true;
		}
		return false;
	}

	/**
	 * Count all transactions.
	 *
	 * @return int Total transactions.
	 */
	public static function count_transactions() {
		$summary = self::get_payment_status_summary();
		return array_sum( array_map( 'intval', $summary ) );
	}

	/**
	 * Get transactions with pagination.
	 *
	 * @param int $per_page Items per page.
	 * @param int $offset   Offset.
	 * @return array List of transactions.
	 */
	public static function get_transactions_paginated( $per_page, $offset ) {
		global $wpdb;
		$table_name = self::get_table_name();
		return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				'SELECT * FROM ' . $table_name . ' ORDER BY created_at DESC LIMIT %d OFFSET %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
				$per_page,
				$offset
			),
			ARRAY_A
		);
	}

	/**
	 * Get duplicate transaction hashes (used by admin UI to warn).
	 *
	 * @return array Array of tx_hash strings.
	 */
	public static function get_duplicate_tx_hashes() {
		global $wpdb;
		$table_name = self::get_table_name();
		$rows       = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			'SELECT tx_hash FROM ' . $table_name . ' WHERE tx_hash IS NOT NULL GROUP BY tx_hash, payment_id HAVING COUNT(*) > 1', // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
			ARRAY_A
		);
		return array_map(
			function ( $row ) {
				return $row['tx_hash'];
			},
			( $rows ) ? $rows : []
		);
	}

	/**
	 * Count expired pending payments older than expiry_time without tx_hash.
	 *
	 * @param int|null $expiry_time Seconds; null uses default.
	 * @return int Number of expired payments.
	 */
	public static function count_expired_payments( $expiry_time = null ) {
		global $wpdb;
		if ( $expiry_time === null ) {
			$expiry_time = AnyCoin_Constants::DEFAULT_PAYMENT_TIMEOUT;
		}
		$table_name  = self::get_table_name();
		$cutoff_time = gmdate( 'Y-m-d H:i:s', time() - $expiry_time );
		$count       = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				'SELECT COUNT(*) FROM ' . $table_name . " WHERE status = %s AND (tx_hash IS NULL OR tx_hash = '') AND created_at < %s", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Safe table identifier
				AnyCoin_Constants::STATUS_PENDING,
				$cutoff_time
			)
		);
		return intval( $count );
	}
}
