<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Queue;

use SecurWP\Modules\Scanner\Repository\CloudJobRepository;
use SecurWP\Modules\Scanner\Repository\FileIndexRepository;
use SecurWP\Modules\Scanner\Repository\FileStateRepository;
use SecurWP\Modules\Scanner\Repository\HashCacheRepository;

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

/**
 * Background watchdog for scanner liveness and historical retention.
 *
 * Active scan recovery is owned by the durable task runner under the scan-row
 * lease. The watchdog only nudges worker liveness and ages out historical data.
 */
class WatchdogService {

	/**
	 * File state repository.
	 *
	 * @var FileStateRepository
	 */
	private static $file_repository;

	/**
	 * File index repository.
	 *
	 * @var FileIndexRepository
	 */
	private static $file_index_repo;

	/**
	 * Signed hash cache repository.
	 *
	 * @var HashCacheRepository
	 */
	private static $hash_cache_repo;

	/**
	 * Cloud job repository.
	 *
	 * @var CloudJobRepository
	 */
	private static $job_repository;

	/**
	 * Escape a plugin-owned table identifier for direct interpolation.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . str_replace( '`', '``', $table ) . '`';
	}

	/**
	 * Initialize watchdog scheduling.
	 *
	 * @return void
	 */
	public static function init(): void {
		if ( function_exists( '\as_has_scheduled_action' ) && function_exists( '\as_schedule_recurring_action' ) ) {
			if ( ! \as_has_scheduled_action( 'securwp_scanner_watchdog' ) ) {
				\as_schedule_recurring_action(
					time(),
					300,
					'securwp_scanner_watchdog',
					[],
					'securwp_scanner'
				);
			}
		} else {
			add_filter(
				'cron_schedules',
				static function( $schedules ) {
					if ( ! isset( $schedules['securwp_five_minutes'] ) ) {
						$schedules['securwp_five_minutes'] = [
							'interval' => 300,
							'display'  => 'Every 5 Minutes (SecurWP)',
						];
					}

					return $schedules;
				}
			);

			if ( ! wp_next_scheduled( 'securwp_scanner_watchdog' ) ) {
				wp_schedule_event( time(), 'securwp_five_minutes', 'securwp_scanner_watchdog' );
			}
		}

		add_action( 'securwp_scanner_watchdog', [ __CLASS__, 'run' ] );
	}

	/**
	 * Run the watchdog.
	 *
	 * @return void
	 */
	public static function run(): void {
		do_action( 'securwp_watchdog_start' );

		$stats = [
			'worker_restarts' => self::ensure_worker_liveness(),
		];

		$last_cleanup = (int) get_option( 'securwp_last_scan_cleanup', 0 );
		if ( ( time() - $last_cleanup ) >= DAY_IN_SECONDS ) {
			$total_cleaned = 0;
			$batches       = 0;

			do {
				$cleaned = self::cleanup_old_scans( 7 );
				$total_cleaned += $cleaned;
				$batches++;
				if ( $batches >= 10 ) {
					break;
				}
			} while ( $cleaned > 0 );

			if ( $total_cleaned > 0 ) {
				securwp_log( sprintf( 'SecurWP: Cleaned up %d old scan(s) from runtime tables', $total_cleaned ) );
			}

			update_option( 'securwp_last_scan_cleanup', time(), false );
		}

		do_action( 'securwp_watchdog_complete', $stats );
	}

	/**
	 * Lazily load repository dependencies.
	 *
	 * @return void
	 */
	private static function init_dependencies(): void {
		if ( self::$file_repository instanceof FileStateRepository ) {
			return;
		}

		self::$file_repository = new FileStateRepository();
		self::$file_index_repo = new FileIndexRepository();
		self::$hash_cache_repo = new HashCacheRepository();
		self::$job_repository  = new CloudJobRepository();
	}

	/**
	 * Nudge the durable task queue when the worker has gone quiet.
	 *
	 * @return int Number of restarts triggered.
	 */
	private static function ensure_worker_liveness(): int {
		if ( ! ScanQueue::is_active() ) {
			return 0;
		}

		$queue = ScanQueue::get();
		$scan_id = is_array( $queue ) ? (string) ( $queue['id'] ?? '' ) : '';
		if ( '' === $scan_id ) {
			return 0;
		}

		if ( ! class_exists( TaskScheduler::class ) ) {
			require_once __DIR__ . '/TaskScheduler.php';
		}

		return TaskScheduler::ensure_progress( $scan_id );
	}

	/**
	 * Remove historical scan execution state.
	 *
	 * @param int $days_old Number of days to retain.
	 * @return int Number of scans cleaned.
	 */
	public static function cleanup_old_scans( int $days_old = 30 ): int {
		self::init_dependencies();

		global $wpdb;

		$cutoff_time = time() - ( $days_old * DAY_IN_SECONDS );
		$scan_queue_table = $wpdb->prefix . 'securwp_scan_queue';
		$scan_queue_identifier = self::table_identifier( $scan_queue_table );

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$old_scans = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT job_id FROM {$scan_queue_identifier}
				WHERE status IN ('complete', 'completed', 'failed', 'stopped')
				AND updated_at < %d
				LIMIT 100",
				$cutoff_time
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$file_states_table = self::table_identifier( $wpdb->prefix . 'securwp_file_states' );
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$orphaned_scans = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT fs.scan_id
				FROM {$file_states_table} fs
				LEFT JOIN {$scan_queue_identifier} sq ON fs.scan_id = sq.job_id
				WHERE sq.job_id IS NULL
				AND fs.updated_at < %d
				LIMIT 100",
				$cutoff_time
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$targets = array_values( array_unique( array_merge( $old_scans ?: [], $orphaned_scans ?: [] ) ) );
		if ( empty( $targets ) ) {
			return 0;
		}

		$cleaned = 0;

		foreach ( $targets as $scan_id ) {
			self::$file_repository->delete_by_scan_id( $scan_id );
			self::$job_repository->delete_by_scan_id( $scan_id );

			if ( ! class_exists( '\SecurWP\Modules\Scanner\Repository\TaskQueueRepository' ) ) {
				require_once dirname( __DIR__ ) . '/Repository/TaskQueueRepository.php';
			}
			if ( class_exists( '\SecurWP\Modules\Scanner\Repository\TaskQueueRepository' ) ) {
				( new \SecurWP\Modules\Scanner\Repository\TaskQueueRepository() )->delete_by_scan_id( $scan_id );
			}

			if ( self::$file_index_repo instanceof FileIndexRepository ) {
				self::$file_index_repo->delete_by_scan_id( $scan_id );
			}

			if ( ! class_exists( '\SecurWP\Modules\Scanner\Service\ResourcePressure' ) ) {
				require_once dirname( __DIR__ ) . '/Service/ResourcePressure.php';
			}
			if ( class_exists( '\SecurWP\Modules\Scanner\Service\ResourcePressure' ) ) {
				\SecurWP\Modules\Scanner\Service\ResourcePressure::clear_state( $scan_id );
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Deletes a plugin-owned queue row during historical scan cleanup.
			$wpdb->delete(
				$scan_queue_table,
				[ 'job_id' => $scan_id ],
				[ '%s' ]
			);

			$cleaned++;
		}

		$expired = self::$hash_cache_repo->cleanup_expired();
		if ( $cleaned > 0 || $expired > 0 ) {
			securwp_log(
				sprintf(
					'SecurWP Cleanup: Deleted %d old scans, removed %d expired cache entries',
					$cleaned,
					$expired
				)
			);
		}

		do_action( 'securwp_old_scans_cleaned', $cleaned, $days_old );

		return $cleaned;
	}

	/**
	 * Force cleanup of historical scan state.
	 *
	 * @param int $days_old Number of days to retain.
	 * @return array<string,int>
	 */
	public static function force_cleanup( int $days_old = 7 ): array {
		self::init_dependencies();

		$total_cleaned = 0;
		$batches       = 0;

		do {
			$cleaned        = self::cleanup_old_scans( $days_old );
			$total_cleaned += $cleaned;
			$batches++;
			if ( $batches >= 50 ) {
				break;
			}
		} while ( $cleaned > 0 );

		delete_option( 'securwp_last_scan_cleanup' );

		return [
			'scans_cleaned' => $total_cleaned,
			'batches'       => $batches,
		];
	}
}
