<?php
/**
 * Plugin Name: Media Cleanup WP-CLI Command
 */

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return;
}

class CMBird_Media_Cleanup_Command {

	/**
	 * Cleanup WooCommerce product images.
	 *
	 * # # OPTIONS.
	 *
	 * [--dry-run]
	 * : Show what would be deleted without deleting anything.
	 *
	 * [--broken]
	 * : Remove attachments whose files no longer exist.
	 *
	 * [--duplicates]
	 * : Remove duplicate attachments based on filename suffix.
	 *
	 * [--products-only]
	 * : Limit cleanup to WooCommerce product & variation images.
	 *
	 * [--suffix-regex=<regex>]
	 * : Regex to detect duplicate suffix (no extension).
	 * ---
	 * default: -(\d{2})$
	 * ---
	 *
	 * [--batch=<number>]
	 * : Number of attachments to process per batch.
	 * ---
	 * default: 300
	 * ---
	 *
	 * # # EXAMPLES.
	 *
	 * wp media cleanup --dry-run --duplicates --products-only
	 * wp media cleanup --duplicates --suffix-regex='-(\d+)$'
	 *
	 * @when after_wp_load
	 */
	public function cleanup( $args, $assoc_args ) {

		if ( ! class_exists( 'WooCommerce' ) ) {
			WP_CLI::error( 'WooCommerce is not active.' );
		}

		$dry_run       = isset( $assoc_args['dry-run'] );
		$broken        = isset( $assoc_args['broken'] );
		$duplicates    = isset( $assoc_args['duplicates'] );
		$products_only = isset( $assoc_args['products-only'] );
		$batch         = intval( $assoc_args['batch'] ?? 300 );
		$suffix_regex  = $assoc_args['suffix-regex'] ?? '-(\d{2})$';

		if ( ! $broken && ! $duplicates ) {
			WP_CLI::error( 'You must specify --broken and/or --duplicates.' );
		}

		WP_CLI::log( 'Starting WooCommerce media cleanup...' );

		$attachment_ids = $products_only
			? $this->get_product_image_ids()
			: $this->get_all_image_ids();

		$upload_dir = wp_get_upload_dir();
		$seen       = array();
		$deleted    = array(
			'broken'    => 0,
			'duplicate' => 0,
		);

		foreach ( array_chunk( $attachment_ids, $batch ) as $chunk ) {

			$meta_map = $this->get_attachment_meta_map( $chunk );

			foreach ( $chunk as $attachment_id ) {

				$meta        = $meta_map[ $attachment_id ] ?? array();
				$file_meta   = $meta['file'] ?? '';
				$relative    = is_string( $file_meta ) ? $file_meta : '';
				$file        = $relative ? trailingslashit( $upload_dir['basedir'] ) . $relative : '';
				$file_exists = $file ? file_exists( $file ) : false;

				/**
				 * Broken images
				 */
				if ( $broken && ( ! $file || ! $file_exists ) ) {
					$this->delete_attachment( $attachment_id, 'BROKEN', $dry_run );
					$deleted['broken']++;
					continue;
				}

				/**
				 * Duplicate images (filename-based)
				 * Uses the stored file name or post slug instead of hitting the filesystem again.
				 */
				if ( $duplicates ) {

					$source_name = $relative ?: ( $meta['post_name'] ?? '' );
					$info        = pathinfo( $source_name );
					$basename    = strtolower( $info['filename'] ?? '' );
					$extension   = strtolower( $info['extension'] ?? '' );

					if ( $basename && preg_match( '/' . $suffix_regex . '/i', $basename ) ) {

						$base_key = preg_replace( '/' . $suffix_regex . '/i', '', $basename );
						$base_key = $extension ? $base_key . '.' . $extension : $base_key;

						if ( isset( $seen[ $base_key ] ) ) {
							$this->delete_attachment(
								$attachment_id,
								'DUPLICATE (suffix match)',
								$dry_run
							);
							$deleted['duplicate']++;
							continue;
						}

						$seen[ $base_key ] = $attachment_id;
					}
				}
			}

			WP_CLI::log( 'Processed batch of ' . count( $chunk ) );
		}

		WP_CLI::success(
			sprintf(
				'Cleanup completed. Broken removed: %d, duplicates removed: %d.',
				$deleted['broken'],
				$deleted['duplicate']
			)
		);
	}

	/**
	 * Get all WooCommerce product-related image attachment IDs
	 */
	private function get_product_image_ids() {

		global $wpdb;

		$ids = array();

		// Featured images.
		$ids = array_merge(
			$ids,
			$wpdb->get_col(
				"
            SELECT meta_value FROM {$wpdb->postmeta}
            WHERE meta_key = '_thumbnail_id'
        "
			)
		);

		// Product galleries.
		$gallery_ids = $wpdb->get_col(
			"
            SELECT meta_value FROM {$wpdb->postmeta}
            WHERE meta_key = '_product_image_gallery'
        "
		);

		foreach ( $gallery_ids as $gallery ) {
			$ids = array_merge( $ids, array_filter( explode( ',', $gallery ) ) );
		}

		// Variation images.
		$ids = array_merge(
			$ids,
			$wpdb->get_col(
				"
            SELECT meta_value FROM {$wpdb->postmeta}
            WHERE meta_key = '_variation_thumbnail_id'
        "
			)
		);

		return array_unique( array_map( 'intval', $ids ) );
	}

	/**
	 * Get all image attachment IDs (fallback)
	 */
	private function get_all_image_ids() {

		return get_posts(
			array(
				'post_type'      => 'attachment',
				'post_mime_type' => 'image',
				'posts_per_page' => -1,
				'fields'         => 'ids',
			)
		);
	}

	/**
	 * Prefetch attachment slug and file path to reduce per-ID queries.
	 */
	private function get_attachment_meta_map( $attachment_ids ) {

		if ( empty( $attachment_ids ) ) {
			return array();
		}

		global $wpdb;

		$placeholders = implode( ',', array_fill( 0, count( $attachment_ids ), '%d' ) );

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT p.ID, p.post_name, pm.meta_value AS file
				FROM {$wpdb->posts} p
				LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_wp_attached_file'
				WHERE p.ID IN ($placeholders)",
				$attachment_ids
			),
			ARRAY_A
		);

		$map = array();

		foreach ( $rows as $row ) {
			$map[ (int) $row['ID'] ] = array(
				'post_name' => $row['post_name'],
				'file'      => $row['file'],
			);
		}

		return $map;
	}

	private function delete_attachment( $attachment_id, $reason, $dry_run ) {

		if ( $dry_run ) {
			WP_CLI::log( "[DRY-RUN] {$reason} → Attachment ID {$attachment_id}" );
			return;
		}

		wp_delete_attachment( $attachment_id, true );
		WP_CLI::log( "[DELETED] {$reason} → Attachment ID {$attachment_id}" );
	}
}

WP_CLI::add_command( 'media cleanup', 'CMBird_Media_Cleanup_Command' );
