<?php
/**
 * Database bloat scanner.
 *
 * @package DatabaseCleanup
 */

namespace DatabaseCleanup\Includes;

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

class Database_Scanner {

	public const TRANSIENT_PREFIX   = 'wpps_dc_scan_';
	public const OVERVIEW_TRANSIENT = 'wpps_dc_overview_metrics';
	public const SCAN_JOB_PREFIX    = 'wpps_dc_scan_job_';
	public const CACHE_TTL          = 600;
	public const OVERVIEW_TTL       = 900;
	public const SCAN_JOB_TTL       = 900;
	public const SIZE_HISTORY_OPTION = 'wpps_dc_size_history';
	public const SIZE_HISTORY_MAX    = 60;

	/**
	 * @param array<string, mixed> $settings
	 * @param bool                 $force
	 * @return array<string, mixed>
	 */
	public function scan( array $settings, bool $force = false ): array {
		$key = self::cache_key( $settings );

		if ( ! $force ) {
			$cached = get_transient( $key );
			if ( is_array( $cached ) && ! empty( $cached['tasks'] ) ) {
				return $cached;
			}
		}

		$tasks        = Cleanup_Task_Registry::all();
		$results      = [];
		$total_count  = 0;
		$total_bytes  = 0;
		$issues_count = 0;

		foreach ( $tasks as $task_id => $task ) {
			if ( ! Cleanup_Task_Registry::is_scannable_task( $task_id, $settings ) ) {
				continue;
			}

			$scan  = $task->scan( $settings );
			$count = (int) ( $scan['count'] ?? 0 );
			$bytes = (int) ( $scan['estimated_bytes'] ?? 0 );

			$results[ $task_id ] = [
				'id'              => $task_id,
				'label'           => $task->get_label(),
				'description'     => $task->get_description(),
				'category'        => Cleanup_Task_Registry::get_category( $task_id ),
				'count'           => $count,
				'estimated_bytes' => $bytes,
				'safe'            => $task->is_safe(),
				'safety_level'    => Cleanup_Task_Registry::safety_level( $task_id ),
				'details'         => is_array( $scan['details'] ?? null ) ? $scan['details'] : [],
			];

			$total_count += $count;
			$total_bytes += $bytes;

			if ( $count > 0 ) {
				++$issues_count;
			}
		}

		return $this->finalize_scan_payload( $settings, $results, $total_count, $total_bytes, $issues_count );
	}

	/**
	 * @param array<string, mixed> $settings
	 * @param bool                 $force
	 * @return array{job_id:string,cached:bool,total_tasks:int}|array<string, mixed>
	 */
	public function create_scan_job( array $settings, bool $force = false ): array {
		if ( ! $force ) {
			$cached = get_transient( self::cache_key( $settings ) );
			if ( is_array( $cached ) && ! empty( $cached['tasks'] ) ) {
				return [
					'cached'   => true,
					'results'  => $cached,
				];
			}
		}

		$task_ids = [];

		foreach ( Cleanup_Task_Registry::all() as $task_id => $task ) {
			unset( $task );
			if ( Cleanup_Task_Registry::is_scannable_task( $task_id, $settings ) ) {
				$task_ids[] = $task_id;
			}
		}

		$job_id = wp_generate_uuid4();
		$job    = [
			'job_id'        => $job_id,
			'task_ids'      => $task_ids,
			'index'         => 0,
			'results'       => [],
			'total_count'   => 0,
			'total_bytes'   => 0,
			'issues_count'  => 0,
			'started_at'    => time(),
			'user_id'       => get_current_user_id(),
			'settings_hash' => md5( wp_json_encode( $settings ) ?: '' ),
		];

		set_transient( self::SCAN_JOB_PREFIX . $job_id, $job, self::SCAN_JOB_TTL );

		return [
			'cached'      => false,
			'job_id'      => $job_id,
			'total_tasks' => count( $task_ids ),
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public function run_scan_step( string $job_id, array $settings, ?int $expected_user_id = null ): array {
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 90 );
		}

		if ( ! Database_Cleaner::is_valid_job_id( $job_id ) ) {
			return [
				'done'  => true,
				'error' => __( 'Scan job not found or expired.', 'wp-powersuite' ),
			];
		}

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

		if ( ! is_array( $job ) || empty( $job['task_ids'] ) ) {
			return [
				'done'  => true,
				'error' => __( 'Scan job not found or expired.', 'wp-powersuite' ),
			];
		}

		if ( $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 scan job.', 'wp-powersuite' ),
				];
			}
		}

		$index    = (int) ( $job['index'] ?? 0 );
		$task_ids = (array) $job['task_ids'];

		if ( ! isset( $task_ids[ $index ] ) ) {
			$payload = $this->finalize_scan_payload(
				$settings,
				(array) ( $job['results'] ?? [] ),
				(int) ( $job['total_count'] ?? 0 ),
				(int) ( $job['total_bytes'] ?? 0 ),
				(int) ( $job['issues_count'] ?? 0 )
			);

			delete_transient( self::SCAN_JOB_PREFIX . $job_id );

			return [
				'done'    => true,
				'results' => $payload,
				'progress'=> 100,
			];
		}

		$task_id = (string) $task_ids[ $index ];
		$task    = Cleanup_Task_Registry::get( $task_id );

		if ( $task && Cleanup_Task_Registry::is_scannable_task( $task_id, $settings ) ) {
			try {
				$scan  = $task->scan( $settings );
				$count = (int) ( $scan['count'] ?? 0 );
				$bytes = (int) ( $scan['estimated_bytes'] ?? 0 );

				$job['results'][ $task_id ] = [
					'id'              => $task_id,
					'label'           => $task->get_label(),
					'description'     => $task->get_description(),
					'category'        => Cleanup_Task_Registry::get_category( $task_id ),
					'count'           => $count,
					'estimated_bytes' => $bytes,
					'safe'            => $task->is_safe(),
					'safety_level'    => Cleanup_Task_Registry::safety_level( $task_id ),
					'details'         => is_array( $scan['details'] ?? null ) ? $scan['details'] : [],
				];

				$job['total_count']  = (int) ( $job['total_count'] ?? 0 ) + $count;
				$job['total_bytes']  = (int) ( $job['total_bytes'] ?? 0 ) + $bytes;
				$job['issues_count'] = (int) ( $job['issues_count'] ?? 0 ) + ( $count > 0 ? 1 : 0 );
			} catch ( \Throwable $e ) {
				error_log(
					sprintf(
						'WP PowerSuite Database Cleanup: scan failed for task "%s": %s',
						$task_id,
						$e->getMessage()
					)
				);

				return [
					'done'  => true,
					'error' => sprintf(
						/* translators: %s: cleanup task label */
						__( 'Scan failed while checking "%s". Please try again.', 'wp-powersuite' ),
						$task ? $task->get_label() : $task_id
					),
				];
			}
		}

		$job['index'] = $index + 1;
		set_transient( self::SCAN_JOB_PREFIX . $job_id, $job, self::SCAN_JOB_TTL );

		$total_tasks   = max( 1, count( $task_ids ) );
		$progress      = min( 100, (int) round( ( $job['index'] / $total_tasks ) * 100 ) );
		$current_label = $task ? $task->get_label() : $task_id;

		if ( (int) $job['index'] >= count( $task_ids ) ) {
			$payload = $this->finalize_scan_payload(
				$settings,
				(array) ( $job['results'] ?? [] ),
				(int) ( $job['total_count'] ?? 0 ),
				(int) ( $job['total_bytes'] ?? 0 ),
				(int) ( $job['issues_count'] ?? 0 )
			);

			delete_transient( self::SCAN_JOB_PREFIX . $job_id );

			return [
				'done'          => true,
				'results'       => $payload,
				'progress'      => 100,
				'current'       => $task_id,
				'current_label' => $current_label,
			];
		}

		return [
			'done'             => false,
			'progress'         => $progress,
			'current'          => $task_id,
			'current_label'    => $current_label,
			'index'            => (int) $job['index'],
			'total'            => count( $task_ids ),
			'discovered_count' => (int) ( $job['total_count'] ?? 0 ),
			'discovered_bytes' => (int) ( $job['total_bytes'] ?? 0 ),
		];
	}

	/**
	 * @param array<string, mixed> $results
	 * @return array<string, mixed>
	 */
	private function finalize_scan_payload( array $settings, array $results, int $total_count, int $total_bytes, int $issues_count ): array {
		$overview = self::get_overview_metrics( false );

		if ( empty( $overview['cached_at'] ) || ( time() - (int) $overview['cached_at'] ) > self::OVERVIEW_TTL ) {
			try {
				$overview = self::get_overview_metrics( true );
			} catch ( \Throwable $e ) {
				error_log( 'WP PowerSuite Database Cleanup: overview metrics failed: ' . $e->getMessage() );
			}
		}

		$payload = [
			'scanned_at'     => time(),
			'database_bytes' => (int) ( $overview['database_bytes'] ?? 0 ),
			'total_count'    => $total_count,
			'total_bytes'    => $total_bytes,
			'issues_count'   => $issues_count,
			'tasks'          => $results,
			'overview'       => $overview,
		];

		set_transient( self::cache_key( $settings ), $payload, self::CACHE_TTL );
		update_option( 'wpps_dc_last_scan', $payload, false );

		self::record_size_history( (int) ( $overview['database_bytes'] ?? 0 ), $total_bytes, $total_count );

		return $payload;
	}

	/**
	 * Record a database size/bloat data point for the growth trend (one per day).
	 */
	public static function record_size_history( int $db_bytes, int $removable_bytes, int $items ): void {
		$history = get_option( self::SIZE_HISTORY_OPTION, [] );

		if ( ! is_array( $history ) ) {
			$history = [];
		}

		$point = [
			't'  => time(),
			'db' => max( 0, $db_bytes ),
			'rm' => max( 0, $removable_bytes ),
			'it' => max( 0, $items ),
		];

		$today = gmdate( 'Y-m-d', $point['t'] );
		$last  = end( $history );

		if ( is_array( $last ) && gmdate( 'Y-m-d', (int) ( $last['t'] ?? 0 ) ) === $today ) {
			$history[ array_key_last( $history ) ] = $point;
		} else {
			$history[] = $point;
		}

		if ( count( $history ) > self::SIZE_HISTORY_MAX ) {
			$history = array_slice( $history, -self::SIZE_HISTORY_MAX );
		}

		update_option( self::SIZE_HISTORY_OPTION, array_values( $history ), false );
	}

	/**
	 * @return list<array{t:int,db:int,rm:int,it:int}>
	 */
	public static function get_size_history(): array {
		$history = get_option( self::SIZE_HISTORY_OPTION, [] );

		if ( ! is_array( $history ) ) {
			return [];
		}

		return array_values( array_filter( $history, 'is_array' ) );
	}

	/**
	 * Lightweight dashboard metrics (cached).
	 *
	 * @return array<string, mixed>
	 */
	public static function get_overview_metrics( bool $refresh = false ): array {
		if ( ! $refresh ) {
			$cached = get_transient( self::OVERVIEW_TRANSIENT );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		global $wpdb;

		$revisions_total = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'"
		);

		// Removable revisions respect the "keep last X per post" setting so this
		// figure matches the Post revisions cleanup task instead of the raw total.
		$keep = max( 0, absint( \DatabaseCleanup\Module::getStoredSettings()['revision_keep_count'] ?? 5 ) );

		if ( $keep === 0 ) {
			$revisions_removable = $revisions_total;
		} else {
			$parents = (int) $wpdb->get_var(
				"SELECT COUNT(DISTINCT post_parent) FROM {$wpdb->posts} WHERE post_type = 'revision' AND post_parent > 0"
			);
			$kept_est            = min( $revisions_total, $parents * $keep );
			$revisions_removable = max( 0, $revisions_total - $kept_est );
		}

		$autoload_bytes = (int) $wpdb->get_var(
			"SELECT COALESCE(SUM(LENGTH(option_value)), 0) FROM {$wpdb->options} WHERE autoload = 'yes'"
		);

		$spam_comments = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
		);

		$metrics = [
			'database_bytes'    => self::get_database_size_bytes( true ),
			'revisions'         => $revisions_removable,
			'revisions_total'   => $revisions_total,
			'autoload_bytes'    => $autoload_bytes,
			'autoload_warn'     => $autoload_bytes > 1048576,
			'revisions_warn'    => $revisions_total > 500,
			'spam_comments'     => $spam_comments,
			'cached_at'         => time(),
		];

		set_transient( self::OVERVIEW_TRANSIENT, $metrics, self::OVERVIEW_TTL );

		return $metrics;
	}

	public static function clear_cache( array $settings ): void {
		delete_transient( self::cache_key( $settings ) );
		delete_transient( self::OVERVIEW_TRANSIENT );
	}

	/**
	 * Persist cleanup results into the stored scan so a page reload reflects the
	 * cleaned state without forcing a full rescan.
	 *
	 * @param array<string, array{deleted?:int,bytes?:int}> $job_results
	 */
	public static function apply_cleanup_to_last_scan( array $job_results ): void {
		if ( $job_results === [] ) {
			return;
		}

		$last = get_option( 'wpps_dc_last_scan', null );

		if ( ! is_array( $last ) || empty( $last['tasks'] ) || ! is_array( $last['tasks'] ) ) {
			return;
		}

		foreach ( $job_results as $task_id => $entry ) {
			if ( ! isset( $last['tasks'][ $task_id ] ) || ! is_array( $last['tasks'][ $task_id ] ) ) {
				continue;
			}

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

			$current_count = (int) ( $last['tasks'][ $task_id ]['count'] ?? 0 );
			$next_count    = max( 0, $current_count - $deleted );

			$last['tasks'][ $task_id ]['count']   = $next_count;
			$last['tasks'][ $task_id ]['scanned'] = true;

			if ( $next_count === 0 ) {
				$last['tasks'][ $task_id ]['estimated_bytes'] = 0;
			} else {
				$current_bytes = (int) ( $last['tasks'][ $task_id ]['estimated_bytes'] ?? 0 );
				$last['tasks'][ $task_id ]['estimated_bytes'] = max( 0, $current_bytes - $bytes );
			}

			if ( $deleted > 0 && $next_count === 0 ) {
				$last['tasks'][ $task_id ]['cleaned']    = true;
				$last['tasks'][ $task_id ]['cleaned_at'] = time();
			}
		}

		$total_count = 0;
		$total_bytes = 0;
		$issues      = 0;

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

			$count        = (int) ( $task['count'] ?? 0 );
			$total_count += $count;
			$total_bytes += (int) ( $task['estimated_bytes'] ?? 0 );

			if ( $count > 0 ) {
				$issues++;
			}
		}

		$last['total_count']  = $total_count;
		$last['total_bytes']  = $total_bytes;
		$last['issues_count'] = $issues;

		try {
			$overview               = self::get_overview_metrics( true );
			$last['overview']       = $overview;
			$last['database_bytes'] = (int) ( $overview['database_bytes'] ?? ( $last['database_bytes'] ?? 0 ) );
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: overview refresh after cleanup failed: ' . $e->getMessage() );
		}

		update_option( 'wpps_dc_last_scan', $last, false );

		self::record_size_history( (int) ( $last['database_bytes'] ?? 0 ), $total_count > 0 ? $total_bytes : 0, $total_count );
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	public static function cache_key( array $settings ): string {
		return self::TRANSIENT_PREFIX . md5( wp_json_encode( $settings ) ?: '' );
	}

	public static function get_database_size_bytes( bool $allow_cache = false ): int {
		if ( $allow_cache ) {
			$overview = get_transient( self::OVERVIEW_TRANSIENT );
			if ( is_array( $overview ) && isset( $overview['database_bytes'] ) ) {
				return (int) $overview['database_bytes'];
			}
		}

		global $wpdb;

		$db_name = defined( 'DB_NAME' ) ? DB_NAME : '';

		if ( $db_name === '' ) {
			return 0;
		}

		try {
			$size = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COALESCE(SUM(data_length + index_length), 0)
					FROM information_schema.TABLES
					WHERE table_schema = %s',
					$db_name
				)
			);
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: database size query failed: ' . $e->getMessage() );
			return 0;
		}

		return max( 0, (int) $size );
	}

	/**
	 * @return array<string, mixed>|null
	 */
	public static function get_last_scan(): ?array {
		$last = get_option( 'wpps_dc_last_scan', null );

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