<?php
defined( 'ABSPATH' ) || exit;

class BW_WebP_Queue {

	const ACTION_BATCH = 'bw_webp_process_batch';
	const ACTION_ONE   = 'bw_webp_process_one';
	const GROUP        = 'bw-webp';

	const SUPPORTED_EXT = array( 'jpg', 'jpeg', 'png', 'gif' );
	const BATCH_SIZE    = 50;

	private BW_WebP_Manifest $manifest;
	private BW_WebP_Settings $settings;

	public function __construct( BW_WebP_Manifest $manifest, BW_WebP_Settings $settings ) {
		$this->manifest = $manifest;
		$this->settings = $settings;
	}

	public function register_hooks(): void {
		add_action( self::ACTION_BATCH, array( $this, 'process_batch' ), 10, 1 );
		add_action( self::ACTION_ONE,   array( $this, 'process_one' ),   10, 1 );

		// Auto-convert new uploads.
		add_filter( 'wp_generate_attachment_metadata', array( $this, 'on_attachment_metadata' ), 10, 2 );
	}

	/**
	 * Walk the uploads directory and upsert manifest rows for every supported image
	 * whose source mtime has changed (or that's never been seen).
	 *
	 * @return int  Number of pending rows after the scan.
	 */
	public function scan(): int {
		$basedir = bw_webp_uploads_basedir();
		if ( '' === $basedir || ! is_dir( $basedir ) ) {
			return 0;
		}

		$it = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $basedir, FilesystemIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::LEAVES_ONLY
		);

		foreach ( $it as $file ) {
			/** @var SplFileInfo $file */
			if ( ! $file->isFile() ) {
				continue;
			}
			$ext = strtolower( $file->getExtension() );
			if ( ! in_array( $ext, self::SUPPORTED_EXT, true ) ) {
				continue;
			}

			$src  = $file->getPathname();
			$dest = $src . '.webp';

			/**
			 * Filter: bw_webp_skip_path
			 * Return true to skip a source path.
			 */
			if ( apply_filters( 'bw_webp_skip_path', false, $src ) ) {
				continue;
			}

			$this->manifest->upsert( $src, $file->getMTime(), $file->getSize(), $dest );
		}

		$counts = $this->manifest->counts();
		return (int) $counts['pending'];
	}

	public function enqueue_bulk( ?int $workers = null ): int {
		$cfg     = $this->settings->get();
		$workers = $workers ?? (int) apply_filters( 'bw_webp_workers', (int) $cfg['workers'] );
		$workers = max( 1, min( 8, $workers ) );

		for ( $i = 0; $i < $workers; $i++ ) {
			$this->schedule_async( self::ACTION_BATCH, array( $i ) );
		}

		return $workers;
	}

	public function process_batch( int $worker_id ): void {
		$converter = $this->build_converter();
		if ( ! $converter ) {
			return;
		}

		$cfg     = $this->settings->get();
		$quality = (int) apply_filters( 'bw_webp_quality', (int) $cfg['quality'] );
		$token   = sprintf( 'w%d-%d-%s', $worker_id, time(), wp_generate_password( 6, false ) );

		$rows = $this->manifest->claim_batch( self::BATCH_SIZE, $token );
		if ( empty( $rows ) ) {
			return;
		}

		foreach ( $rows as $row ) {
			$id   = (int) $row['id'];
			$src  = (string) $row['src_path'];
			$dest = (string) $row['dest_path'];

			do_action( 'bw_webp_before_convert', $src, $dest );

			try {
				if ( ! is_readable( $src ) ) {
					throw new RuntimeException( 'source missing' );
				}
				$converter->convert( $src, $dest, $quality );
				$this->manifest->mark_done( $id, $converter->name() );
				do_action( 'bw_webp_after_convert', $src, $dest, $converter->name() );
			} catch ( Throwable $e ) {
				$this->manifest->mark_failed( $id, $e->getMessage() );
				do_action( 'bw_webp_convert_failed', $src, $e->getMessage() );
			}
		}

		// Re-enqueue self if more pending work remains.
		$counts = $this->manifest->counts();
		if ( $counts['pending'] > 0 ) {
			$this->schedule_async( self::ACTION_BATCH, array( $worker_id ) );
		} else {
			do_action( 'bw_webp_bulk_complete', $counts );
		}
	}

	public function on_attachment_metadata( $metadata, $attachment_id ) {
		if ( ! is_array( $metadata ) ) {
			return $metadata;
		}
		$this->schedule_async( self::ACTION_ONE, array( (int) $attachment_id ) );
		return $metadata;
	}

	public function process_one( int $attachment_id ): void {
		$file = get_attached_file( $attachment_id );
		if ( ! $file || ! is_readable( $file ) ) {
			return;
		}

		$cfg   = $this->settings->get();
		$paths = array( $file );

		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( is_array( $meta ) && empty( $cfg['skip_thumbnails'] ) && ! empty( $meta['sizes'] ) ) {
			$dir = dirname( $file );
			foreach ( (array) $meta['sizes'] as $size ) {
				if ( ! empty( $size['file'] ) ) {
					$paths[] = $dir . '/' . $size['file'];
				}
			}
		}

		foreach ( $paths as $src ) {
			if ( ! is_readable( $src ) ) {
				continue;
			}
			$this->manifest->upsert( $src, (int) filemtime( $src ), (int) filesize( $src ), $src . '.webp' );
		}

		$this->enqueue_bulk( 1 );
	}

	private function build_converter(): ?BW_WebP_Converter {
		$cfg = $this->settings->get();
		try {
			return BW_WebP_Converter_Factory::detect( (string) $cfg['converter'] );
		} catch ( Throwable $e ) {
			return null;
		}
	}

	/**
	 * Schedule a single async action. Prefers Action Scheduler when available;
	 * falls back to a one-shot WP-Cron event.
	 */
	private function schedule_async( string $hook, array $args ): void {
		if ( function_exists( 'as_enqueue_async_action' ) ) {
			as_enqueue_async_action( $hook, $args, self::GROUP );
			return;
		}
		wp_schedule_single_event( time() + 1, $hook, $args );
	}
}
