<?php
/**
 * Expired transients cleanup task.
 *
 * @package DatabaseCleanup
 */

namespace DatabaseCleanup\Includes\Tasks;

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

class Task_Expired_Transients extends Abstract_Cleanup_Task {

	public function get_id(): string {
		return 'expired_transients';
	}

	public function get_label(): string {
		return __( 'Expired transients', 'wp-powersuite' );
	}

	public function get_description(): string {
		return __( 'Remove expired _transient_* and _site_transient_* rows from the options table. Active transients are never deleted.', 'wp-powersuite' );
	}

	public function is_safe(): bool {
		return true;
	}

	public function scan( array $settings ): array {
		global $wpdb;

		unset( $settings );

		$now            = time();
		$transient_like = $wpdb->esc_like( '_transient_' ) . '%';
		$timeout_like   = $wpdb->esc_like( '_transient_timeout_' ) . '%';
		$site_like      = $wpdb->esc_like( '_site_transient_' ) . '%';
		$site_timeout   = $wpdb->esc_like( '_site_transient_timeout_' ) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$count = (int) $wpdb->get_var(
			$this->prepare_with_plugin_option_exclusion(
				"SELECT COUNT(*) FROM {$wpdb->options} o
				INNER JOIN {$wpdb->options} t ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
				WHERE o.option_name LIKE %s
				AND o.option_name NOT LIKE %s
				AND CAST(t.option_value AS UNSIGNED) > 0
				AND CAST(t.option_value AS UNSIGNED) < %d",
				[ $transient_like, $timeout_like, $now ],
				'o.option_name'
			)
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$site_count = (int) $wpdb->get_var(
			$this->prepare_with_plugin_option_exclusion(
				"SELECT COUNT(*) FROM {$wpdb->options} o
				INNER JOIN {$wpdb->options} t ON t.option_name = CONCAT('_site_transient_timeout_', SUBSTRING(o.option_name, 17))
				WHERE o.option_name LIKE %s
				AND o.option_name NOT LIKE %s
				AND CAST(t.option_value AS UNSIGNED) > 0
				AND CAST(t.option_value AS UNSIGNED) < %d",
				[ $site_like, $site_timeout, $now ],
				'o.option_name'
			)
		);

		$total = $count + $site_count;

		$avg_bytes = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT AVG(LENGTH(option_value)) FROM {$wpdb->options}
				WHERE option_name LIKE %s OR option_name LIKE %s",
				$transient_like,
				$site_like
			)
		);

		return [
			'count'           => $total,
			'estimated_bytes' => $this->estimate_row_bytes( $total, max( 256, $avg_bytes * 2 ) ),
			'details'         => [
				'regular' => $count,
				'site'    => $site_count,
			],
		];
	}

	public function clean( array $settings, int $batch_size, mixed $cursor = null ): array {
		global $wpdb;

		unset( $settings );

		$phase          = is_array( $cursor ) ? (string) ( $cursor['phase'] ?? 'regular' ) : 'regular';
		$offset         = is_array( $cursor ) ? absint( $cursor['offset'] ?? 0 ) : 0;
		$now            = time();
		$transient_like = $wpdb->esc_like( '_transient_' ) . '%';
		$timeout_like   = $wpdb->esc_like( '_transient_timeout_' ) . '%';
		$site_like      = $wpdb->esc_like( '_site_transient_' ) . '%';
		$site_timeout   = $wpdb->esc_like( '_site_transient_timeout_' ) . '%';
		$deleted        = 0;
		$bytes          = 0;

		if ( $phase === 'regular' ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
			$rows = $wpdb->get_results(
				$this->prepare_with_plugin_option_exclusion(
					"SELECT o.option_name, o.option_value
					FROM {$wpdb->options} o
					INNER JOIN {$wpdb->options} t ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
					WHERE o.option_name LIKE %s
					AND o.option_name NOT LIKE %s
					AND CAST(t.option_value AS UNSIGNED) > 0
					AND CAST(t.option_value AS UNSIGNED) < %d
					ORDER BY o.option_id ASC
					LIMIT %d OFFSET %d",
					[ $transient_like, $timeout_like, $now, $batch_size, $offset ],
					'o.option_name'
				),
				ARRAY_A
			);

			foreach ( (array) $rows as $row ) {
				$name = (string) ( $row['option_name'] ?? '' );
				if ( $name === '' || str_starts_with( $name, '_transient_timeout_' ) ) {
					continue;
				}

				$transient = substr( $name, 11 );
				if ( $transient === false || $transient === '' ) {
					continue;
				}

				if ( str_starts_with( $transient, 'wpps_' ) || str_starts_with( $transient, 'wp_powersuite_' ) ) {
					continue;
				}

				$bytes += strlen( (string) ( $row['option_value'] ?? '' ) ) + 64;

				if ( delete_transient( $transient ) ) {
					++$deleted;
				}
			}

			if ( count( (array) $rows ) < $batch_size ) {
				return [
					'deleted' => $deleted,
					'done'    => false,
					'cursor'  => [ 'phase' => 'site', 'offset' => 0 ],
					'bytes'   => $bytes,
				];
			}

			return [
				'deleted' => $deleted,
				'done'    => false,
				'cursor'  => [ 'phase' => 'regular', 'offset' => $offset + $batch_size ],
				'bytes'   => $bytes,
			];
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$rows = $wpdb->get_results(
			$this->prepare_with_plugin_option_exclusion(
				"SELECT o.option_name, o.option_value
				FROM {$wpdb->options} o
				INNER JOIN {$wpdb->options} t ON t.option_name = CONCAT('_site_transient_timeout_', SUBSTRING(o.option_name, 17))
				WHERE o.option_name LIKE %s
				AND o.option_name NOT LIKE %s
				AND CAST(t.option_value AS UNSIGNED) > 0
				AND CAST(t.option_value AS UNSIGNED) < %d
				ORDER BY o.option_id ASC
				LIMIT %d OFFSET %d",
				[ $site_like, $site_timeout, $now, $batch_size, $offset ],
				'o.option_name'
			),
			ARRAY_A
		);

		foreach ( (array) $rows as $row ) {
			$name = (string) ( $row['option_name'] ?? '' );
			if ( $name === '' || str_starts_with( $name, '_site_transient_timeout_' ) ) {
				continue;
			}

			$transient = substr( $name, 16 );
			if ( $transient === false || $transient === '' ) {
				continue;
			}

			if ( str_starts_with( $transient, 'wpps_' ) || str_starts_with( $transient, 'wp_powersuite_' ) ) {
				continue;
			}

			$bytes += strlen( (string) ( $row['option_value'] ?? '' ) ) + 64;

			if ( delete_site_transient( $transient ) ) {
				++$deleted;
			}
		}

		$done = count( (array) $rows ) < $batch_size;

		return [
			'deleted' => $deleted,
			'done'    => $done,
			'cursor'  => $done ? null : [ 'phase' => 'site', 'offset' => $offset + $batch_size ],
			'bytes'   => $bytes,
		];
	}
}
