<?php
/**
 * Batch database cleaner.
 *
 * @package DatabaseCleanup
 */

namespace DatabaseCleanup\Includes;

use DatabaseCleanup\Module;
use WP_PowerSuite\Core\TransientPurger;

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

class Database_Cleaner {

	public const JOB_TRANSIENT_PREFIX  = 'wpps_dc_job_';
	public const ACTIVE_JOB_TRANSIENT  = 'wpps_dc_active_job_';
	public const RUNTIME_KEYS_OPTION   = 'wpps_dc_runtime_keys';
	public const JOB_TTL               = 2 * HOUR_IN_SECONDS;
	public const STALE_JOB_SECONDS     = 300;
	public const STALE_INIT_SECONDS    = 120;

	/**
	 * @param list<string>         $task_ids
	 * @param array<string, mixed> $settings
	 */
	public function create_job( array $task_ids, array $settings, bool $scheduled = false, bool $allow_caution = false ): string {
		$user_id = get_current_user_id();

		if ( ! $scheduled && $user_id > 0 ) {
			self::release_stale_active_job( $user_id );

			$active = get_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );
			if ( is_string( $active ) && $active !== '' ) {
				if ( get_transient( self::JOB_TRANSIENT_PREFIX . $active ) === false ) {
					delete_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );
				} else {
					return '';
				}
			}
		}

		$allow_caution = $allow_caution && ! $scheduled;
		$valid         = Cleanup_Task_Registry::filter_allowed_task_ids( $task_ids, $settings, $allow_caution );

		if ( $valid === [] ) {
			return '';
		}

		$job_id = wp_generate_uuid4();
		$job    = [
			'job_id'             => $job_id,
			'task_ids'           => $valid,
			'current_task_index' => 0,
			'cursor'             => null,
			'started_at'         => time(),
			'user_id'            => $scheduled ? 0 : $user_id,
			'scheduled'          => $scheduled,
			'allow_caution'      => $allow_caution,
			'results'            => [],
			'totals'             => [
				'deleted' => 0,
				'bytes'   => 0,
			],
		];

		$job_key = self::JOB_TRANSIENT_PREFIX . $job_id;
		set_transient( $job_key, $job, self::JOB_TTL );
		TransientPurger::remember( self::RUNTIME_KEYS_OPTION, $job_key );

		if ( ! $scheduled && $user_id > 0 ) {
			$active_key = self::ACTIVE_JOB_TRANSIENT . $user_id;
			set_transient( $active_key, $job_id, self::JOB_TTL );
			TransientPurger::remember( self::RUNTIME_KEYS_OPTION, $active_key );
		}

		return $job_id;
	}

	public static function is_valid_job_id( string $job_id ): bool {
		return (bool) preg_match(
			'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
			$job_id
		);
	}

	/**
	 * @return array<string, mixed>|null
	 */
	public function get_job( string $job_id ): ?array {
		if ( ! self::is_valid_job_id( $job_id ) ) {
			return null;
		}

		$job = get_transient( self::JOB_TRANSIENT_PREFIX . $job_id );

		return is_array( $job ) ? $job : null;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public function run_step( string $job_id, array $settings, ?int $expected_user_id = null ): array {
		$job = $this->get_job( $job_id );

		if ( ! $job || empty( $job['task_ids'] ) ) {
			if ( $expected_user_id !== null && $expected_user_id > 0 ) {
				delete_transient( self::ACTIVE_JOB_TRANSIENT . $expected_user_id );
			}

			return [
				'done'  => true,
				'error' => __( 'Cleanup job not found or expired.', 'wp-powersuite' ),
				'job'   => null,
			];
		}

		if ( empty( $job['scheduled'] ) && $expected_user_id !== null && $expected_user_id > 0 ) {
			if ( (int) ( $job['user_id'] ?? 0 ) !== $expected_user_id ) {
				return [
					'done'  => true,
					'error' => __( 'You are not allowed to run this cleanup job.', 'wp-powersuite' ),
					'job'   => null,
				];
			}
		}

		$allow_caution = ! empty( $job['scheduled'] ) ? false : ! empty( $job['allow_caution'] );
		$batch_size    = max( 200, min( 500, absint( $settings['batch_size'] ?? 300 ) ) );
		$index      = (int) ( $job['current_task_index'] ?? 0 );
		$task_ids   = (array) $job['task_ids'];

		if ( ! isset( $task_ids[ $index ] ) ) {
			$finished = $this->finish_job( $job, $settings );

			return [
				'done'         => true,
				'job'          => $finished,
				'current_task' => '',
				'deleted_step' => 0,
				'bytes_step'   => 0,
			];
		}

		$task_id = (string) $task_ids[ $index ];

		if ( ! Cleanup_Task_Registry::is_allowed_task( $task_id, $settings, $allow_caution ) ) {
			$job['current_task_index'] = $index + 1;
			$job['cursor']             = null;
			set_transient( self::JOB_TRANSIENT_PREFIX . $job_id, $job, self::JOB_TTL );

			return [
				'done'         => false,
				'job'          => $job,
				'current_task' => $task_id,
				'deleted_step' => 0,
				'bytes_step'   => 0,
			];
		}

		$task = Cleanup_Task_Registry::get( $task_id );

		if ( ! $task ) {
			$job['current_task_index'] = $index + 1;
			$job['cursor']             = null;
			set_transient( self::JOB_TRANSIENT_PREFIX . $job_id, $job, self::JOB_TTL );

			return [
				'done'         => false,
				'job'          => $job,
				'current_task' => $task_id,
				'deleted_step' => 0,
				'bytes_step'   => 0,
			];
		}

		try {
			$result  = $task->clean( $settings, $batch_size, $job['cursor'] ?? null );
		} catch ( \Throwable $e ) {
			error_log(
				sprintf(
					'WP PowerSuite Database Cleanup: clean failed for task "%s": %s',
					$task_id,
					$e->getMessage()
				)
			);

			return [
				'done'  => true,
				'error' => __( 'Cleanup failed for the current task. Check the error log and try again.', 'wp-powersuite' ),
				'job'   => $job,
			];
		}

		$deleted = (int) ( $result['deleted'] ?? 0 );
		$bytes   = (int) ( $result['bytes'] ?? 0 );

		if ( ! isset( $job['results'][ $task_id ] ) ) {
			$job['results'][ $task_id ] = [
				'label'   => $task->get_label(),
				'deleted' => 0,
				'bytes'   => 0,
			];
		}

		$job['results'][ $task_id ]['deleted'] += $deleted;
		$job['results'][ $task_id ]['bytes']   += $bytes;
		$job['totals']['deleted']              += $deleted;
		$job['totals']['bytes']                += $bytes;
		$job['updated_at']                      = time();

		if ( ! empty( $result['done'] ) ) {
			$job['current_task_index'] = $index + 1;
			$job['cursor']             = null;
		} else {
			$job['cursor'] = $result['cursor'] ?? null;
		}

		$all_done = (int) $job['current_task_index'] >= count( $task_ids );

		if ( $all_done ) {
			$finished = $this->finish_job( $job, $settings );

			return [
				'done'         => true,
				'job'          => $finished,
				'current_task' => $task_id,
				'deleted_step' => $deleted,
				'bytes_step'   => $bytes,
			];
		}

		set_transient( self::JOB_TRANSIENT_PREFIX . $job_id, $job, self::JOB_TTL );

		return [
			'done'         => false,
			'job'          => $job,
			'current_task' => $task_id,
			'deleted_step' => $deleted,
			'bytes_step'   => $bytes,
		];
	}

	/**
	 * @param array<string, mixed> $job
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	private function finish_job( array $job, array $settings ): array {
		$job_id  = (string) ( $job['job_id'] ?? '' );
		$user_id = (int) ( $job['user_id'] ?? 0 );
		$duration = max( 0, time() - (int) ( $job['started_at'] ?? time() ) );

		if ( $job_id !== '' ) {
			delete_transient( self::JOB_TRANSIENT_PREFIX . $job_id );
		}

		if ( $user_id > 0 ) {
			delete_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );
		}

		if ( ! empty( $job['scheduled'] ) ) {
			Scheduler::clear_scheduled_job_pointer();
		}

		$entry = [
			'timestamp' => time(),
			'user_id'   => $user_id,
			'duration'  => $duration,
			'tasks'     => array_values( (array) ( $job['results'] ?? [] ) ),
			'totals'    => [
				'deleted' => (int) ( $job['totals']['deleted'] ?? 0 ),
				'bytes'   => (int) ( $job['totals']['bytes'] ?? 0 ),
			],
			'scheduled' => ! empty( $job['scheduled'] ),
		];

		Cleanup_Logger::append( $entry );

		// Invalidate the settings-hashed scan transient, then fold the cleanup
		// results into the persisted scan so a page reload shows the cleaned
		// counts immediately without forcing a full rescan.
		Database_Scanner::clear_cache( $settings );
		Database_Scanner::apply_cleanup_to_last_scan( (array) ( $job['results'] ?? [] ) );

		update_option(
			'wpps_dc_last_cleanup',
			[
				'timestamp' => $entry['timestamp'],
				'deleted'   => $entry['totals']['deleted'],
				'bytes'     => $entry['totals']['bytes'],
				'duration'  => $duration,
			],
			false
		);

		if ( ! empty( $job['scheduled'] ) && ! empty( $settings['email_report'] ) && $entry['totals']['deleted'] > 0 ) {
			self::send_report_email( $entry, $settings );
		}

		$job['finished_at'] = time();
		$job['duration']    = $duration;

		return $job;
	}

	/**
	 * Send a plain-text summary of a completed scheduled cleanup run.
	 *
	 * @param array<string, mixed> $entry
	 * @param array<string, mixed> $settings
	 */
	private static function send_report_email( array $entry, array $settings ): void {
		$recipient = sanitize_email( (string) ( $settings['report_email'] ?? '' ) );

		if ( $recipient === '' || ! is_email( $recipient ) ) {
			$recipient = (string) get_option( 'admin_email' );
		}

		if ( $recipient === '' || ! is_email( $recipient ) ) {
			return;
		}

		$site_name = wp_specialchars_decode( (string) get_option( 'blogname' ), ENT_QUOTES );
		$when      = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $entry['timestamp'] );

		/* translators: %s: site name */
		$subject = sprintf( __( '[%s] Scheduled database cleanup report', 'wp-powersuite' ), $site_name );

		$lines   = [];
		$lines[] = sprintf(
			/* translators: %s: date/time */
			__( 'Scheduled database cleanup completed on %s.', 'wp-powersuite' ),
			$when
		);
		$lines[] = '';
		$lines[] = sprintf(
			/* translators: 1: number of rows removed, 2: space freed */
			__( 'Total: %1$s rows removed, %2$s freed.', 'wp-powersuite' ),
			number_format_i18n( (int) $entry['totals']['deleted'] ),
			Module::formatBytes( (int) $entry['totals']['bytes'] )
		);
		$lines[] = '';
		$lines[] = __( 'Breakdown:', 'wp-powersuite' );

		foreach ( (array) ( $entry['tasks'] ?? [] ) as $task ) {
			if ( ! is_array( $task ) ) {
				continue;
			}

			$deleted = (int) ( $task['deleted'] ?? 0 );
			if ( $deleted <= 0 ) {
				continue;
			}

			$lines[] = sprintf(
				'- %1$s: %2$s rows (%3$s)',
				(string) ( $task['label'] ?? ( $task['id'] ?? '' ) ),
				number_format_i18n( $deleted ),
				Module::formatBytes( (int) ( $task['bytes'] ?? 0 ) )
			);
		}

		wp_mail( $recipient, $subject, implode( "\n", $lines ) );
	}

	/**
	 * Whether a stored job pointer should be treated as abandoned.
	 *
	 * @param array<string, mixed> $job
	 */
	public static function is_job_stale( array $job ): bool {
		$now     = time();
		$started = (int) ( $job['started_at'] ?? 0 );
		$updated = (int) ( $job['updated_at'] ?? $started );

		if ( $started > 0 && ( $now - $started ) > self::JOB_TTL ) {
			return true;
		}

		$index   = (int) ( $job['current_task_index'] ?? 0 );
		$deleted = (int) ( $job['totals']['deleted'] ?? 0 );

		if ( $index === 0 && $deleted === 0 && $started > 0 && ( $now - $started ) > self::STALE_INIT_SECONDS ) {
			return true;
		}

		if ( $updated > 0 && ( $now - $updated ) > self::STALE_JOB_SECONDS ) {
			return true;
		}

		return false;
	}

	public static function abort_job( string $job_id, ?int $user_id = null ): bool {
		if ( ! self::is_valid_job_id( $job_id ) ) {
			return false;
		}

		$job = get_transient( self::JOB_TRANSIENT_PREFIX . $job_id );
		if ( ! is_array( $job ) ) {
			return false;
		}

		if ( $user_id !== null && $user_id > 0 && (int) ( $job['user_id'] ?? 0 ) !== $user_id ) {
			return false;
		}

		delete_transient( self::JOB_TRANSIENT_PREFIX . $job_id );

		$owner = (int) ( $job['user_id'] ?? 0 );
		if ( $owner > 0 ) {
			$active = get_transient( self::ACTIVE_JOB_TRANSIENT . $owner );
			if ( is_string( $active ) && $active === $job_id ) {
				delete_transient( self::ACTIVE_JOB_TRANSIENT . $owner );
			}
		}

		return true;
	}

	public static function release_stale_active_job( int $user_id ): bool {
		if ( $user_id <= 0 ) {
			return false;
		}

		$active = get_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );
		if ( ! is_string( $active ) || $active === '' ) {
			return false;
		}

		$job = get_transient( self::JOB_TRANSIENT_PREFIX . $active );
		if ( ! is_array( $job ) || self::is_job_stale( $job ) ) {
			delete_transient( self::JOB_TRANSIENT_PREFIX . $active );
			delete_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );

			return true;
		}

		return false;
	}

	public static function abort_active_job_for_user( int $user_id ): bool {
		if ( $user_id <= 0 ) {
			return false;
		}

		$active = get_transient( self::ACTIVE_JOB_TRANSIENT . $user_id );
		if ( ! is_string( $active ) || $active === '' ) {
			return false;
		}

		return self::abort_job( $active, $user_id );
	}

	/**
	 * Remove in-progress scan/clean transients (module disable, reset recovery).
	 */
	public static function clear_all_runtime_state(): void {
		TransientPurger::deleteRemembered( self::RUNTIME_KEYS_OPTION );
		TransientPurger::deleteByPrefix( 'wpps_dc_' );

		delete_option( Scheduler::SCHEDULED_JOB_OPTION );
	}
}
