<?php
namespace R2_Offload\Migration;

use R2_Offload\DB;
use R2_Offload\Logger;
use R2_Offload\Media\Path_Resolver;
use R2_Offload\Settings;

defined( 'ABSPATH' ) || exit;

final class Scanner {

	public const STATE_OPTION = 'r2_offload_scan_state';

	private Settings $settings;
	private Logger $logger;

	public function __construct( Settings $settings, Logger $logger ) {
		$this->settings = $settings;
		$this->logger   = $logger;
	}

	public function reset_state(): void {
		delete_option( self::STATE_OPTION );
	}

	public function get_state(): array {
		return wp_parse_args(
			(array) get_option( self::STATE_OPTION, [] ),
			[
				'last_id'        => 0,
				'last_path'      => '',
				'phase'          => 'attachments', // attachments|paths
				'scanned'        => 0,
				'queued'         => 0,
				'skipped'        => 0,
				'bytes_total'    => 0,
				'finished'       => 0,
				'started_at'     => '',
				'dry_run'        => 0,
				'paths_queued'   => 0,
				'paths_scanned'  => 0,
			]
		);
	}

	private function save_state( array $state ): void {
		update_option( self::STATE_OPTION, $state, false );
	}

	public function start( bool $dry_run = false ): void {
		$this->reset_state();
		$this->save_state(
			array_merge(
				$this->get_state(),
				[
					'started_at' => current_time( 'mysql' ),
					'dry_run'    => $dry_run ? 1 : 0,
					'phase'      => 'attachments',
				]
			)
		);
	}

	/**
	 * Scan a chunk of attachments (then extra upload dirs). Returns state snapshot.
	 */
	public function scan_chunk( int $batch_size = 200 ): array {
		$state = $this->get_state();
		if ( $state['finished'] ) {
			return $state;
		}

		if ( ( $state['phase'] ?? 'attachments' ) === 'paths' ) {
			return $this->scan_paths_chunk( $batch_size, $state );
		}

		return $this->scan_attachments_chunk( $batch_size, $state );
	}

	private function scan_attachments_chunk( int $batch_size, array $state ): array {
		global $wpdb;

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts}
				 WHERE post_type = 'attachment' AND ID > %d
				 ORDER BY ID ASC LIMIT %d",
				(int) $state['last_id'],
				$batch_size
			)
		);

		if ( empty( $rows ) ) {
			$state['phase']     = 'paths';
			$state['last_path'] = '';
			$this->save_state( $state );
			$this->logger->info( 'Attachment scan complete; scanning extra upload dirs.', 'scan', $state );
			return $this->scan_paths_chunk( $batch_size, $state );
		}

		$dry    = (bool) $state['dry_run'];
		$bucket = (string) $this->settings->get( 'bucket' );
		$prefix = $this->settings->effective_prefix();

		foreach ( $rows as $row ) {
			$attachment_id    = (int) $row->ID;
			$state['last_id'] = $attachment_id;
			$state['scanned']++;

			$paths = Path_Resolver::attachment_files( $attachment_id );
			if ( empty( $paths ) ) {
				$state['skipped']++;
				continue;
			}

			$size = 0;
			foreach ( $paths as $p ) {
				$size += (int) @filesize( $p );
			}
			$state['bytes_total'] += $size;

			$existing = DB::get_item( $attachment_id );
			if ( $existing && $existing->status === 'offloaded' ) {
				if ( ! $this->needs_sidecar_refresh( $existing, $paths ) ) {
					$state['skipped']++;
					continue;
				}
			}

			if ( ! $dry ) {
				DB::upsert_item(
					$attachment_id,
					[
						'relative_path' => '',
						'status'        => 'pending',
						'provider'      => $this->settings->provider(),
						'bucket'        => $bucket,
						'object_prefix' => $prefix,
						'bytes_total'   => $size,
						'updated_at'    => current_time( 'mysql' ),
					]
				);
				$state['queued']++;
			} else {
				$state['queued']++;
			}
		}

		$this->save_state( $state );
		return $state;
	}

	private function scan_paths_chunk( int $batch_size, array $state ): array {
		$dirs = $this->settings->extra_upload_dirs();
		if ( empty( $dirs ) ) {
			$state['finished'] = 1;
			$this->save_state( $state );
			$this->logger->info( 'Scan complete (no extra dirs).', 'scan', $state );
			return $state;
		}

		$uploads = wp_get_upload_dir();
		$basedir = trailingslashit( $uploads['basedir'] );
		$dry     = (bool) $state['dry_run'];
		$bucket  = (string) $this->settings->get( 'bucket' );
		$prefix  = $this->settings->effective_prefix();
		$after   = (string) ( $state['last_path'] ?? '' );
		$seen    = ( $after === '' );
		$queued  = 0;

		foreach ( $dirs as $dir ) {
			$root = $basedir . $dir;
			if ( ! is_dir( $root ) ) {
				continue;
			}

			try {
				$iterator = new \RecursiveIteratorIterator(
					new \RecursiveDirectoryIterator( $root, \FilesystemIterator::SKIP_DOTS )
				);
			} catch ( \Throwable $e ) {
				$this->logger->warn( 'Cannot scan dir ' . $dir . ': ' . $e->getMessage(), 'scan' );
				continue;
			}

			/** @var \SplFileInfo $file */
			foreach ( $iterator as $file ) {
				if ( ! $file->isFile() ) {
					continue;
				}
				if ( Path_Resolver::should_skip_file( $file->getBasename() ) ) {
					continue;
				}

				$full = $file->getPathname();
				$rel  = ltrim( str_replace( '\\', '/', substr( $full, strlen( $basedir ) ) ), '/' );

				if ( ! $seen ) {
					if ( $rel === $after ) {
						$seen = true;
					}
					continue;
				}

				$state['last_path']     = $rel;
				$state['scanned']++;
				$state['paths_scanned'] = (int) $state['paths_scanned'] + 1;

				$size = (int) @filesize( $full );
				$state['bytes_total'] += $size;

				$existing = DB::get_item_by_path( $rel );
				if ( $existing && $existing->status === 'offloaded' ) {
					$state['skipped']++;
					$queued++;
					if ( $queued >= $batch_size ) {
						$this->save_state( $state );
						return $state;
					}
					continue;
				}

				if ( ! $dry ) {
					$id = DB::path_to_id( $rel );
					DB::upsert_item(
						$id,
						[
							'relative_path' => $rel,
							'status'        => 'pending',
							'provider'      => $this->settings->provider(),
							'bucket'        => $bucket,
							'object_prefix' => $prefix,
							'bytes_total'   => $size,
							'updated_at'    => current_time( 'mysql' ),
						]
					);
				}

				$state['queued']++;
				$state['paths_queued'] = (int) $state['paths_queued'] + 1;
				$queued++;

				if ( $queued >= $batch_size ) {
					$this->save_state( $state );
					return $state;
				}
			}
		}

		$state['finished'] = 1;
		$this->save_state( $state );
		$this->logger->info( 'Scan complete (attachments + extra dirs).', 'scan', $state );
		return $state;
	}

	/**
	 * Re-queue offloaded attachments when local webp/avif sidecars exist but were
	 * never recorded/uploaded (legacy offloads before the files map).
	 *
	 * @param string[] $paths
	 */
	private function needs_sidecar_refresh( object $item, array $paths ): bool {
		$local_sidecars = [];
		foreach ( $paths as $p ) {
			if ( preg_match( '/\.(webp|avif)$/i', $p ) ) {
				$local_sidecars[] = $p;
			}
		}
		if ( empty( $local_sidecars ) ) {
			return false;
		}

		global $wpdb;
		$table     = DB::files_table();
		$uploads   = wp_get_upload_dir();
		$base      = trailingslashit( $uploads['basedir'] );
		$recorded  = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT relative_path FROM {$table} WHERE offload_item_id = %d",
				(int) $item->id
			)
		);
		$recorded = array_map(
			static fn( $r ) => ltrim( str_replace( '\\', '/', (string) $r ), '/' ),
			(array) $recorded
		);

		foreach ( $local_sidecars as $abs ) {
			$rel = ltrim( str_replace( $base, '', $abs ), '/' );
			if ( ! in_array( $rel, $recorded, true ) ) {
				return true;
			}
		}
		return false;
	}
}
