<?php
/**
 * `wp blaminhor image-sizes ...` — Image Sizes module commands.
 *
 * @package BlaminhorEssentials
 */

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

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

/**
 * Disable WordPress core thumbnail sizes and manage custom sizes
 * (add/remove) from the command line.
 *
 * Bulk regeneration of existing thumbnails is intentionally NOT exposed
 * here — it requires per-attachment processing best handled by the
 * existing `wp media regenerate` core command (or the module's admin UI).
 */
class Blaminhor_Essentials_CLI_Image_Sizes extends Blaminhor_Essentials_CLI_Module {

	/**
	 * @var string
	 */
	protected $module_id = 'image-sizes';

	/**
	 * List every registered image size with its status.
	 *
	 * Combines WordPress built-in sizes (thumbnail, medium, large…), other
	 * sizes registered by the theme / other plugins, and the custom sizes
	 * declared in this module. The `status` column shows whether the size
	 * is currently disabled by this module.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render format (table, json, csv, yaml, count, ids). Default: table.
	 *
	 * [--fields=<fields>]
	 * : Columns to display. Available: name, width, height, crop, source, status.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes list
	 *     $ wp blaminhor image-sizes list --format=json
	 *
	 * @subcommand list
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function list_( $args, $assoc_args ) {
		$module        = $this->get_module_instance();
		$disabled      = (array) $module->get_setting( 'disabled_sizes', array() );
		$custom        = (array) $module->get_setting( 'custom_sizes', array() );
		$custom_names  = wp_list_pluck( $custom, 'name' );

		// Built-in sizes (thumbnail/medium/medium_large/large).
		global $_wp_additional_image_sizes;
		$additional = is_array( $_wp_additional_image_sizes ) ? $_wp_additional_image_sizes : array();

		$rows = array();

		// Core sizes from the Settings → Media page.
		foreach ( array( 'thumbnail', 'medium', 'medium_large', 'large' ) as $size ) {
			$rows[] = array(
				'name'   => $size,
				'width'  => (string) (int) get_option( $size . '_size_w', 0 ),
				'height' => (string) (int) get_option( $size . '_size_h', 0 ),
				'crop'   => get_option( $size . '_crop' ) ? 'true' : 'false',
				'source' => 'core',
				'status' => in_array( $size, $disabled, true ) ? 'disabled' : 'enabled',
			);
		}

		// Sizes registered via add_image_size() (theme / other plugins / our custom).
		foreach ( $additional as $name => $data ) {
			$source = in_array( $name, $custom_names, true ) ? 'custom' : 'theme/plugin';
			$rows[] = array(
				'name'   => (string) $name,
				'width'  => (string) (int) ( isset( $data['width'] ) ? $data['width'] : 0 ),
				'height' => (string) (int) ( isset( $data['height'] ) ? $data['height'] : 0 ),
				'crop'   => ! empty( $data['crop'] ) ? 'true' : 'false',
				'source' => $source,
				'status' => in_array( $name, $disabled, true ) ? 'disabled' : 'enabled',
			);
		}

		// Custom sizes declared in module settings but maybe not registered yet (cron not fired).
		foreach ( $custom as $cs ) {
			$name = isset( $cs['name'] ) ? (string) $cs['name'] : '';
			if ( '' === $name ) {
				continue;
			}
			$already = false;
			foreach ( $rows as $r ) {
				if ( $r['name'] === $name ) {
					$already = true;
					break;
				}
			}
			if ( $already ) {
				continue;
			}
			$rows[] = array(
				'name'   => $name,
				'width'  => (string) (int) ( isset( $cs['width'] ) ? $cs['width'] : 0 ),
				'height' => (string) (int) ( isset( $cs['height'] ) ? $cs['height'] : 0 ),
				'crop'   => ! empty( $cs['crop'] ) ? 'true' : 'false',
				'source' => 'custom',
				'status' => in_array( $name, $disabled, true ) ? 'disabled' : 'enabled',
			);
		}

		$this->format_for_output( $rows, $assoc_args, array( 'name', 'width', 'height', 'source', 'status' ) );
	}

	/**
	 * Disable a registered image size (no new thumbnails generated for it).
	 *
	 * Disabling marks the size for cleanup — existing thumbnail files are
	 * NOT deleted until you run the cleanup action from the admin UI (or
	 * `wp media regenerate`). The module re-applies the disabled list on
	 * the next image upload.
	 *
	 * ## OPTIONS
	 *
	 * <size>
	 * : Size slug (e.g. thumbnail, medium, large, my-custom-size). See `list`.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes disable medium_large
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<size>].
	 * @param array $assoc_args Associative args (unused).
	 */
	public function disable( $args, $assoc_args ) {
		$this->ensure_capabilities();
		if ( empty( $args[0] ) ) {
			WP_CLI::error( __( 'Missing size name. Usage: wp blaminhor image-sizes disable <size>', 'blaminhor-essentials' ) );
		}

		$module = $this->get_module_instance();
		$size   = sanitize_key( (string) $args[0] );

		$disabled = (array) $module->get_setting( 'disabled_sizes', array() );
		if ( in_array( $size, $disabled, true ) ) {
			WP_CLI::warning( sprintf(
				/* translators: %s: size name */
				__( 'Size "%s" is already disabled.', 'blaminhor-essentials' ),
				$size
			) );
			return;
		}

		// Trigger the toggle_size verb in process_settings(). The module flips
		// the state, so we pass it when currently enabled to switch it off.
		$result = $module->apply_settings( array( 'toggle_size' => $size ) );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		blaminhor_essentials_log( sprintf( 'Image size "%s" disabled via WP-CLI.', $size ), 'info', 'cli', array( 'event' => 'imgsizes_disable' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: size name */
			__( 'Size "%s" disabled.', 'blaminhor-essentials' ),
			$size
		) );
	}

	/**
	 * Re-enable a previously disabled image size.
	 *
	 * ## OPTIONS
	 *
	 * <size>
	 * : Size slug previously passed to `disable`.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes enable medium_large
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<size>].
	 * @param array $assoc_args Associative args (unused).
	 */
	public function enable( $args, $assoc_args ) {
		$this->ensure_capabilities();
		if ( empty( $args[0] ) ) {
			WP_CLI::error( __( 'Missing size name. Usage: wp blaminhor image-sizes enable <size>', 'blaminhor-essentials' ) );
		}

		$module = $this->get_module_instance();
		$size   = sanitize_key( (string) $args[0] );

		$disabled = (array) $module->get_setting( 'disabled_sizes', array() );
		if ( ! in_array( $size, $disabled, true ) ) {
			WP_CLI::warning( sprintf(
				/* translators: %s: size name */
				__( 'Size "%s" is already enabled.', 'blaminhor-essentials' ),
				$size
			) );
			return;
		}

		$result = $module->apply_settings( array( 'toggle_size' => $size ) );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		blaminhor_essentials_log( sprintf( 'Image size "%s" re-enabled via WP-CLI.', $size ), 'info', 'cli', array( 'event' => 'imgsizes_enable' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: size name */
			__( 'Size "%s" enabled.', 'blaminhor-essentials' ),
			$size
		) );
	}

	/**
	 * Register a new custom image size.
	 *
	 * The size is added immediately. Existing media items will not have a
	 * thumbnail in this size until they're regenerated (run `wp media
	 * regenerate` afterwards if needed).
	 *
	 * ## OPTIONS
	 *
	 * --name=<name>
	 * : Slug for the new size (sanitised as a WordPress key).
	 *
	 * --width=<px>
	 * : Target width in pixels (use 0 for proportional).
	 *
	 * --height=<px>
	 * : Target height in pixels (use 0 for proportional).
	 *
	 * [--crop]
	 * : Hard-crop the image to the exact dimensions.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes add-custom --name=hero --width=1920 --height=600 --crop
	 *
	 * @subcommand add-custom
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function add_custom( $args, $assoc_args ) {
		$this->ensure_capabilities();

		foreach ( array( 'name', 'width', 'height' ) as $required ) {
			if ( ! isset( $assoc_args[ $required ] ) || '' === (string) $assoc_args[ $required ] ) {
				WP_CLI::error( sprintf(
					/* translators: %s: flag name */
					__( 'Missing required --%s.', 'blaminhor-essentials' ),
					$required
				) );
			}
		}

		$module = $this->get_module_instance();
		$data   = array(
			'add_custom_size' => '1',
			'new_size_name'   => (string) $assoc_args['name'],
			'new_size_width'  => (string) (int) $assoc_args['width'],
			'new_size_height' => (string) (int) $assoc_args['height'],
		);
		if ( isset( $assoc_args['crop'] ) ) {
			$data['new_size_crop'] = '1';
		}

		$result = $module->apply_settings( $data );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		blaminhor_essentials_log( sprintf( 'Custom image size "%s" added via WP-CLI.', $data['new_size_name'] ), 'info', 'cli', array( 'event' => 'imgsizes_add_custom' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: size name */
			__( 'Custom size "%s" added.', 'blaminhor-essentials' ),
			$data['new_size_name']
		) );
	}

	/**
	 * Remove a custom image size declared by this module.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Slug of the custom size to remove (see `list` with --format=ids).
	 *
	 * [--yes]
	 * : Skip the interactive confirmation prompt.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes remove-custom hero --yes
	 *
	 * @subcommand remove-custom
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<name>].
	 * @param array $assoc_args Associative args.
	 */
	public function remove_custom( $args, $assoc_args ) {
		$this->ensure_capabilities();
		if ( empty( $args[0] ) ) {
			WP_CLI::error( __( 'Missing size name. Usage: wp blaminhor image-sizes remove-custom <name>', 'blaminhor-essentials' ) );
		}
		$name = sanitize_key( (string) $args[0] );

		WP_CLI::confirm(
			sprintf(
				/* translators: %s: size name */
				__( 'Remove custom image size "%s"? Existing thumbnails for that size are NOT deleted.', 'blaminhor-essentials' ),
				$name
			),
			$assoc_args
		);

		$module = $this->get_module_instance();
		$result = $module->apply_settings( array( 'remove_custom_size' => $name ) );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		blaminhor_essentials_log( sprintf( 'Custom image size "%s" removed via WP-CLI.', $name ), 'info', 'cli', array( 'event' => 'imgsizes_remove_custom' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: size name */
			__( 'Custom size "%s" removed.', 'blaminhor-essentials' ),
			$name
		) );
	}

	/**
	 * Register several custom image sizes from a CSV file in one go.
	 *
	 * The file must contain a header row with columns: name,width,height,crop
	 * (crop accepts true/false/1/0). Rows with an invalid name are skipped.
	 *
	 * ## OPTIONS
	 *
	 * --file=<path>
	 * : Absolute path to the CSV file.
	 *
	 * [--dry-run]
	 * : Parse and validate the CSV without writing anything.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes bulk-add --file=/srv/sizes.csv
	 *     $ wp blaminhor image-sizes bulk-add --file=/srv/sizes.csv --dry-run
	 *
	 * @subcommand bulk-add
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function bulk_add( $args, $assoc_args ) {
		$this->ensure_capabilities();

		if ( empty( $assoc_args['file'] ) ) {
			WP_CLI::error( __( 'Missing --file=<path>.', 'blaminhor-essentials' ) );
		}
		$file = (string) $assoc_args['file'];
		if ( ! is_readable( $file ) ) {
			WP_CLI::error( sprintf(
				/* translators: %s: file path */
				__( 'Cannot read CSV file: %s', 'blaminhor-essentials' ),
				$file
			) );
		}

		$handle = fopen( $file, 'r' );
		if ( false === $handle ) {
			WP_CLI::error( __( 'Failed to open CSV for reading.', 'blaminhor-essentials' ) );
		}

		$header = fgetcsv( $handle );
		if ( ! is_array( $header ) ) {
			fclose( $handle );
			WP_CLI::error( __( 'CSV header missing.', 'blaminhor-essentials' ) );
		}
		$header = array_map( 'strtolower', array_map( 'trim', $header ) );
		$col    = array_flip( $header );
		foreach ( array( 'name', 'width', 'height' ) as $required ) {
			if ( ! isset( $col[ $required ] ) ) {
				fclose( $handle );
				WP_CLI::error( sprintf(
					/* translators: %s: column name */
					__( 'CSV is missing required column: %s', 'blaminhor-essentials' ),
					$required
				) );
			}
		}

		$dry_run = isset( $assoc_args['dry-run'] );
		$module  = $this->get_module_instance();
		$added   = 0;
		$skipped = 0;
		$row_n   = 1;

		while ( ( $row = fgetcsv( $handle ) ) !== false ) {
			$row_n++;
			$name   = isset( $row[ $col['name'] ] ) ? sanitize_key( (string) $row[ $col['name'] ] ) : '';
			$width  = isset( $row[ $col['width'] ] ) ? (int) $row[ $col['width'] ] : 0;
			$height = isset( $row[ $col['height'] ] ) ? (int) $row[ $col['height'] ] : 0;
			$crop   = isset( $col['crop'], $row[ $col['crop'] ] ) ? $this->parse_bool( $row[ $col['crop'] ] ) : false;

			if ( '' === $name || ( 0 === $width && 0 === $height ) ) {
				WP_CLI::warning( sprintf(
					/* translators: %d: row number */
					__( 'Row %d skipped (invalid name or zero dimensions).', 'blaminhor-essentials' ),
					$row_n
				) );
				$skipped++;
				continue;
			}

			if ( $dry_run ) {
				WP_CLI::log( sprintf( '[dry-run] would add: %s %dx%d crop=%s', $name, $width, $height, $crop ? 'true' : 'false' ) );
				$added++;
				continue;
			}

			$data = array(
				'add_custom_size' => '1',
				'new_size_name'   => $name,
				'new_size_width'  => (string) $width,
				'new_size_height' => (string) $height,
			);
			if ( $crop ) {
				$data['new_size_crop'] = '1';
			}
			$result = $module->apply_settings( $data );
			if ( is_wp_error( $result ) ) {
				WP_CLI::warning( sprintf(
					/* translators: 1: row number, 2: error message */
					__( 'Row %1$d failed: %2$s', 'blaminhor-essentials' ),
					$row_n,
					$result->get_error_message()
				) );
				$skipped++;
				continue;
			}
			$added++;
		}
		fclose( $handle );

		blaminhor_essentials_log( sprintf( 'Image sizes bulk-add via WP-CLI: %d added, %d skipped.', $added, $skipped ), 'info', 'cli', array( 'event' => 'imgsizes_bulk_add', 'added' => $added, 'skipped' => $skipped ) );
		WP_CLI::success( sprintf(
			/* translators: 1: added count, 2: skipped count */
			__( '%1$d size(s) added, %2$d row(s) skipped.', 'blaminhor-essentials' ),
			$added,
			$skipped
		) );
	}

	/**
	 * Regenerate ONLY the thumbnails that are currently missing from each
	 * attachment. Existing files are never overwritten.
	 *
	 * For every attachment of MIME type image/*, iterates over the registered
	 * sizes; if an entry is absent from the attachment's metadata, calls
	 * image_make_intermediate_size() to create it and stores the result.
	 * Existing thumbnails are left untouched.
	 *
	 * ## OPTIONS
	 *
	 * [--ids=<csv>]
	 * : Limit to the given attachment IDs (comma-separated). Defaults to all attachments.
	 *
	 * [--limit=<n>]
	 * : Process at most N attachments (useful to throttle long runs). 0 = no cap. Default: 0.
	 *
	 * [--dry-run]
	 * : Report what would be generated without writing any file.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes regenerate-thumbnails --dry-run
	 *     $ wp blaminhor image-sizes regenerate-thumbnails --ids=42,77
	 *     $ wp blaminhor image-sizes regenerate-thumbnails --limit=50
	 *
	 * @subcommand regenerate-thumbnails
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function regenerate_thumbnails( $args, $assoc_args ) {
		$this->ensure_capabilities();

		if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}
		if ( ! function_exists( 'image_make_intermediate_size' ) ) {
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}

		$query_args = array(
			'post_type'      => 'attachment',
			'post_status'    => 'inherit',
			'post_mime_type' => 'image',
			'fields'         => 'ids',
			'posts_per_page' => -1,
			'no_found_rows'  => true,
		);
		if ( ! empty( $assoc_args['ids'] ) ) {
			$ids = array_filter( array_map( 'intval', explode( ',', (string) $assoc_args['ids'] ) ) );
			if ( empty( $ids ) ) {
				WP_CLI::error( __( 'No valid attachment IDs in --ids.', 'blaminhor-essentials' ) );
			}
			$query_args['post__in'] = $ids;
		}
		$attachments = get_posts( $query_args );

		$limit   = isset( $assoc_args['limit'] ) ? (int) $assoc_args['limit'] : 0;
		$dry_run = isset( $assoc_args['dry-run'] );

		if ( empty( $attachments ) ) {
			WP_CLI::warning( __( 'No image attachments found.', 'blaminhor-essentials' ) );
			return;
		}

		$processed       = 0;
		$created_total   = 0;
		$skipped_present = 0;

		// Resolve every registered size (core + theme/plugin/custom).
		global $_wp_additional_image_sizes;
		$sizes = array();
		foreach ( array( 'thumbnail', 'medium', 'medium_large', 'large' ) as $core_size ) {
			$sizes[ $core_size ] = array(
				'width'  => (int) get_option( $core_size . '_size_w', 0 ),
				'height' => (int) get_option( $core_size . '_size_h', 0 ),
				'crop'   => (bool) get_option( $core_size . '_crop', false ),
			);
		}
		if ( is_array( $_wp_additional_image_sizes ) ) {
			foreach ( $_wp_additional_image_sizes as $name => $data ) {
				$sizes[ $name ] = array(
					'width'  => isset( $data['width'] ) ? (int) $data['width'] : 0,
					'height' => isset( $data['height'] ) ? (int) $data['height'] : 0,
					'crop'   => ! empty( $data['crop'] ),
				);
			}
		}

		foreach ( $attachments as $attachment_id ) {
			if ( $limit > 0 && $processed >= $limit ) {
				break;
			}
			$processed++;

			$file = get_attached_file( $attachment_id );
			if ( ! $file || ! file_exists( $file ) ) {
				WP_CLI::warning( sprintf(
					/* translators: %d: attachment ID */
					__( 'Skipping #%d (source file missing on disk).', 'blaminhor-essentials' ),
					(int) $attachment_id
				) );
				continue;
			}

			$meta = wp_get_attachment_metadata( $attachment_id );
			if ( ! is_array( $meta ) ) {
				$meta = array();
			}
			if ( ! isset( $meta['sizes'] ) || ! is_array( $meta['sizes'] ) ) {
				$meta['sizes'] = array();
			}

			$dirty = false;
			foreach ( $sizes as $size_name => $dims ) {
				if ( isset( $meta['sizes'][ $size_name ] ) ) {
					$skipped_present++;
					continue;
				}
				if ( $dims['width'] <= 0 && $dims['height'] <= 0 ) {
					continue;
				}
				if ( $dry_run ) {
					WP_CLI::log( sprintf( '[dry-run] #%d: would create size "%s" (%dx%d)', (int) $attachment_id, $size_name, $dims['width'], $dims['height'] ) );
					$created_total++;
					continue;
				}
				$resized = image_make_intermediate_size( $file, $dims['width'], $dims['height'], $dims['crop'] );
				if ( $resized ) {
					$meta['sizes'][ $size_name ] = $resized;
					$dirty                        = true;
					$created_total++;
				}
			}
			if ( $dirty && ! $dry_run ) {
				wp_update_attachment_metadata( $attachment_id, $meta );
			}
		}

		blaminhor_essentials_log( sprintf( 'Image sizes regenerate-missing via WP-CLI: %d created, %d already present.', $created_total, $skipped_present ), 'info', 'cli', array( 'event' => 'imgsizes_regenerate_missing' ) );
		WP_CLI::success( sprintf(
			/* translators: 1: created count, 2: attachments processed, 3: already-present count */
			__( '%1$d thumbnail(s) created across %2$d attachment(s); %3$d size(s) already present and left untouched.', 'blaminhor-essentials' ),
			$created_total,
			$processed,
			$skipped_present
		) );
	}

	/**
	 * Dump Image Sizes settings as a JSON envelope on STDOUT.
	 *
	 * Includes disabled core sizes and any custom sizes declared by the module.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor image-sizes export > image-sizes.json
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args (unused).
	 */
	public function export( $args, $assoc_args ) {
		$module  = $this->get_module_instance();
		$payload = array(
			'module'         => 'image-sizes',
			'plugin_version' => defined( 'BLAMINHOR_ESSENTIALS_VERSION' ) ? BLAMINHOR_ESSENTIALS_VERSION : '',
			'exported_at'    => gmdate( 'c' ),
			'settings'       => $module->get_effective_settings(),
		);
		WP_CLI::line( wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
	}
}
