<?php
/**
 * BaseCloud Boost — WP-CLI Commands
 *
 * Provides command-line management of the cache and optimization features
 * for developers, DevOps engineers, and CI/CD pipelines.
 *
 * Usage:
 *   wp bcboost cache clear           — Purge all cached pages
 *   wp bcboost cache warm             — Run the cache preloader (non-blocking)
 *   wp bcboost cache status           — Show cache stats (pages cached, size, hit rate)
 *   wp bcboost cache clear-url <url>  — Purge cache for a specific URL
 *   wp bcboost compress --all         — Generate WebP/AVIF copies of the Media Library
 *   wp bcboost verify-conversions     — Validate existing WebP/AVIF siblings (repair with --delete-broken/--regenerate)
 *   wp bcboost db cleanup             — Run all database cleanup tasks
 *   wp bcboost settings export        — Export all settings as JSON
 *   wp bcboost settings import <file> — Import settings from a JSON file
 *   wp bcboost critical-css list      — Show stored critical CSS (templates + URL overrides)
 *   wp bcboost critical-css set <template> --file=<path> — Store critical CSS for a template
 *   wp bcboost critical-css delete <template>            — Delete a template's critical CSS
 *
 * @package BaseCloud\Boost\CLI
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\CLI;

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

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

use BaseCloud\Boost\Core\Cache_Purge;
use BaseCloud\Boost\Core\Cache_Preloader;
use BaseCloud\Boost\Core\Page_Cache;
use BaseCloud\Boost\Core\Metrics_Collector;
use BaseCloud\Boost\Database\DB_Optimizer;
use BaseCloud\Boost\Admin\Settings_API;
use BaseCloud\Boost\Optimizer\Image_Compressor;
use BaseCloud\Boost\Optimizer\Critical_CSS;

/**
 * Manage BaseCloud Boost cache, settings, and optimization.
 *
 * ## EXAMPLES
 *
 *     wp bcboost cache clear
 *     wp bcboost cache status
 *     wp bcboost db cleanup
 */
class BCBOOST_CLI extends \WP_CLI_Command {

	// ─── Cache ────────────────────────────────────────────────────────────

	/**
	 * Clear all cached pages.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost cache clear
	 *
	 * @subcommand cache clear
	 */
	public function cache_clear(): void {
		( new Cache_Purge() )->purge_all();
		\WP_CLI::success( 'BaseCloud Boost cache cleared.' );
	}

	/**
	 * Clear the cache for a specific URL.
	 *
	 * ## OPTIONS
	 *
	 * <url>
	 * : The full URL to purge.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost cache clear-url https://example.com/my-post/
	 *
	 * @subcommand cache clear-url
	 * @param string[] $args Positional args.
	 */
	public function cache_clear_url( array $args ): void {
		if ( empty( $args[0] ) ) {
			\WP_CLI::error( 'Please provide a URL.' );
		}
		( new Cache_Purge() )->purge_url( $args[0] );
		\WP_CLI::success( 'Cache cleared for: ' . $args[0] );
	}

	/**
	 * Start the cache preloader (warm all pages).
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost cache warm
	 *
	 * @subcommand cache warm
	 */
	public function cache_warm(): void {
		$preloader = new Cache_Preloader();
		$preloader->start_preload();
		\WP_CLI::success( 'Cache preloader started. It will run in the background via WP-Cron.' );
	}

	/**
	 * Show cache statistics.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost cache status
	 *
	 * @subcommand cache status
	 */
	public function cache_status(): void {
		$page_cache = new Page_Cache();
		$metrics    = new Metrics_Collector();
		$stats      = $page_cache->get_cache_stats();
		$perf       = $metrics->get_all();

		\WP_CLI\Utils\format_items( 'table', [
			[ 'Metric' => 'Pages Cached',   'Value' => number_format( $stats['count'] ) ],
			[ 'Metric' => 'Cache Size',     'Value' => size_format( $stats['size'] ) ],
			[ 'Metric' => 'Cache Hits',     'Value' => number_format( (int) $perf['hits'] ) ],
			[ 'Metric' => 'Cache Misses',   'Value' => number_format( (int) $perf['misses'] ) ],
			[ 'Metric' => 'Hit Rate',       'Value' => $metrics->get_hit_rate() . '%' ],
			[ 'Metric' => 'Total Saved',    'Value' => size_format( $metrics->get_total_saved_bytes() ) ],
		], [ 'Metric', 'Value' ] );
	}

	// ─── Images ───────────────────────────────────────────────────────────

	/**
	 * Generate WebP (and AVIF where supported) copies of Media Library images.
	 *
	 * Non-destructive: originals are never touched — modern-format siblings are
	 * written next to each file (full size + every thumbnail). Safe to re-run:
	 * already-converted images are skipped. Uses the same cursor-based pipeline
	 * as the admin Bulk Compress tool, without browser batch limits — ideal for
	 * large libraries on shared hosting.
	 *
	 * ## OPTIONS
	 *
	 * [<id>...]
	 * : One or more attachment IDs to compress. Omit when using --all.
	 *
	 * [--all]
	 * : Process every JPEG/PNG in the Media Library.
	 *
	 * [--batch=<n>]
	 * : How many attachments to fetch per database round-trip. Default 10.
	 *
	 * [--dry-run]
	 * : Only report what would be converted; write nothing.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost compress --all
	 *     wp bcboost compress --all --batch=25
	 *     wp bcboost compress --all --dry-run
	 *     wp bcboost compress 123 456
	 *
	 * @subcommand compress
	 * @param string[] $args       Positional args (attachment IDs).
	 * @param array    $assoc_args Associative args.
	 */
	public function compress( array $args, array $assoc_args ): void {
		if ( ! Image_Compressor::webp_supported() && ! Image_Compressor::avif_supported() ) {
			\WP_CLI::error( Image_Compressor::unsupported_reason( 'webp' ) );
		}

		$compressor = new Image_Compressor();
		$dry        = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
		$all        = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false );
		$batch      = max( 1, (int) ( $assoc_args['batch'] ?? 10 ) );
		$ids        = array_values( array_filter( array_map( 'absint', $args ) ) );

		if ( ! $all && empty( $ids ) ) {
			\WP_CLI::error( 'Pass one or more attachment IDs, or use --all for the whole library.' );
		}

		\WP_CLI::log( sprintf(
			'Engines — WebP: %s, AVIF: %s%s',
			Image_Compressor::engine_label( 'webp' ),
			Image_Compressor::engine_label( 'avif' ),
			$dry ? ' (dry run — nothing will be written)' : ''
		) );

		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'image' );
		}

		$totals       = [ 'images' => 0, 'converted' => 0, 'skipped' => 0, 'failed' => 0, 'bytes' => 0 ];
		$total_images = $all ? $compressor->count_source_images() : count( $ids );

		$handle_one = function ( int $id ) use ( $compressor, $dry, &$totals, $total_images ): void {
			$totals['images']++;
			$name   = basename( (string) ( get_attached_file( $id ) ?: '' ) );
			$name   = '' !== $name ? $name : ( 'attachment #' . $id );
			$prefix = sprintf( '[%d/%d] #%d %s', $totals['images'], max( $total_images, $totals['images'] ), $id, $name );

			if ( $dry ) {
				$pending = $compressor->pending_conversions( $id );
				if ( empty( $pending ) ) {
					$totals['skipped']++;
					\WP_CLI::log( $prefix . ' — up to date, nothing to do' );
					return;
				}
				$totals['converted'] += count( $pending );
				$list = array_map( function ( array $p ): string {
					return basename( (string) $p['file'] ) . ' -> .' . $p['format'];
				}, $pending );
				\WP_CLI::log( $prefix . ' — would convert ' . count( $pending ) . ' file(s): ' . implode( ', ', $list ) );
				return;
			}

			$res = $compressor->compress_attachment( $id );

			$totals['converted'] += (int) $res['generated'];
			$totals['bytes']     += (int) $res['bytes_saved'];
			$totals['failed']    += count( $res['failures'] );
			if ( empty( $res['failures'] ) && 0 === (int) $res['generated'] ) {
				$totals['skipped']++;
			}

			$line = sprintf(
				'%s — %d converted, %d skipped, %s saved',
				$prefix,
				(int) $res['generated'],
				(int) $res['skipped'],
				size_format( (int) $res['bytes_saved'] ) ?: '0 B'
			);
			if ( ! empty( $res['failures'] ) ) {
				$line .= ', ' . count( $res['failures'] ) . ' FAILED';
			}
			\WP_CLI::log( $line );

			foreach ( $res['failures'] as $failure ) {
				\WP_CLI::warning( '  ' . (string) ( $failure['file'] ?? '?' ) . ' — ' . (string) ( $failure['reason'] ?? 'unknown error' ) );
			}
		};

		if ( $all ) {
			$last_id = 0;
			while ( true ) {
				$batch_ids = $compressor->get_image_ids_after( $last_id, $batch );
				if ( empty( $batch_ids ) ) {
					break;
				}
				foreach ( $batch_ids as $id ) {
					$handle_one( $id );
					$last_id = $id;
				}
			}
		} else {
			foreach ( $ids as $id ) {
				$handle_one( $id );
			}
		}

		$summary = sprintf(
			'%s: %d image(s) processed — %d file(s) %s, %d image(s) already up to date, %d failure(s), %s saved.',
			$dry ? 'Dry run' : 'Done',
			$totals['images'],
			$totals['converted'],
			$dry ? 'would be converted' : 'converted',
			$totals['skipped'],
			$totals['failed'],
			size_format( $totals['bytes'] ) ?: '0 B'
		);

		if ( $totals['failed'] > 0 ) {
			\WP_CLI::warning( $summary );
		} else {
			\WP_CLI::success( $summary );
		}
	}

	/**
	 * Verify existing WebP/AVIF conversions actually decode; repair broken ones.
	 *
	 * Field context (bfap.co.za): before conversions were written atomically, a
	 * worker killed mid-write could leave a TRUNCATED .webp/.avif whose header
	 * still reports valid dimensions — so it passed the dimension check and was
	 * served as a broken/blank image. A defective encoder can likewise emit a
	 * complete file that no browser can render. This command runs the same
	 * layered validation as generation now does (structural container checks +
	 * a real decode with an independent engine where available) over every
	 * existing sibling of every JPEG/PNG attachment (full size + thumbnails).
	 *
	 * Never touched: originals, client uploads that merely share a basename
	 * (dimensions differ), animated WebP, and files that cannot be judged on
	 * this host (no decoder) unless they are provably truncated.
	 *
	 * ## OPTIONS
	 *
	 * [--delete-broken]
	 * : Delete siblings that fail validation. Safe: the untouched original is
	 * served wherever a sibling is missing.
	 *
	 * [--regenerate]
	 * : Delete AND regenerate broken siblings through the atomic, validated
	 * pipeline (implies deletion of the broken file first).
	 *
	 * [--batch=<n>]
	 * : Attachments to fetch per database round-trip. Default 50.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost verify-conversions
	 *     wp bcboost verify-conversions --delete-broken
	 *     wp bcboost verify-conversions --delete-broken --regenerate
	 *
	 * @subcommand verify-conversions
	 * @param string[] $args       Positional args.
	 * @param array    $assoc_args Associative args.
	 */
	public function verify_conversions( array $args, array $assoc_args ): void {
		$compressor = new Image_Compressor();
		$delete     = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'delete-broken', false );
		$regen      = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'regenerate', false );
		$batch      = max( 1, (int) ( $assoc_args['batch'] ?? 50 ) );

		if ( ! Image_Compressor::can_validate( 'webp' ) && ! Image_Compressor::can_validate( 'avif' ) ) {
			\WP_CLI::warning( 'No WebP/AVIF decoder (GD or Imagick) on this host — only structural truncation checks can run; undecodable-but-complete files cannot be detected here.' );
		}

		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'image' );
		}

		$totals = [
			'attachments' => 0,
			'checked'     => 0,
			'ok'          => 0,
			'foreign'     => 0,
			'animated'    => 0,
			'unverif'     => 0,
			'broken'      => 0,
			'deleted'     => 0,
			'regenerated' => 0,
			'tmp_removed' => 0,
		];

		$last_id = 0;
		while ( true ) {
			$ids = $compressor->get_image_ids_after( $last_id, $batch );
			if ( empty( $ids ) ) {
				break;
			}
			foreach ( $ids as $id ) {
				$last_id = $id;
				$totals['attachments']++;

				$res = $compressor->verify_attachment( $id, $delete, $regen );

				$totals['checked']     += $res['checked'];
				$totals['ok']          += $res['ok'];
				$totals['foreign']     += $res['foreign'];
				$totals['animated']    += $res['animated'];
				$totals['unverif']     += $res['unverifiable'];
				$totals['broken']      += count( $res['broken'] );
				$totals['deleted']     += $res['deleted'];
				$totals['regenerated'] += $res['regenerated'];
				$totals['tmp_removed'] += $res['tmp_removed'];

				foreach ( $res['broken'] as $name ) {
					\WP_CLI::warning( sprintf(
						'#%d %s — BROKEN (fails decode/structure)%s',
						$id,
						$name,
						( $delete || $regen ) ? ' — deleted' : ''
					) );
				}
				foreach ( $res['regen_failed'] as $note ) {
					\WP_CLI::warning( '#' . $id . ' ' . $note );
				}
			}
		}

		\WP_CLI\Utils\format_items( 'table', [
			[ 'Metric' => 'Attachments scanned',        'Value' => number_format( $totals['attachments'] ) ],
			[ 'Metric' => 'Siblings checked',           'Value' => number_format( $totals['checked'] ) ],
			[ 'Metric' => 'Valid',                      'Value' => number_format( $totals['ok'] ) ],
			[ 'Metric' => 'Broken',                     'Value' => number_format( $totals['broken'] ) ],
			[ 'Metric' => 'Deleted',                    'Value' => number_format( $totals['deleted'] ) ],
			[ 'Metric' => 'Regenerated',                'Value' => number_format( $totals['regenerated'] ) ],
			[ 'Metric' => 'Foreign uploads (skipped)',  'Value' => number_format( $totals['foreign'] ) ],
			[ 'Metric' => 'Animated WebP (skipped)',    'Value' => number_format( $totals['animated'] ) ],
			[ 'Metric' => 'Unverifiable (skipped)',     'Value' => number_format( $totals['unverif'] ) ],
			[ 'Metric' => 'Stale .tmp files removed',   'Value' => number_format( $totals['tmp_removed'] ) ],
		], [ 'Metric', 'Value' ] );

		if ( $totals['broken'] > 0 && ! $delete && ! $regen ) {
			\WP_CLI::warning( sprintf( '%d broken file(s) found. Re-run with --delete-broken (serve originals) or --delete-broken --regenerate (rebuild them).', $totals['broken'] ) );
		} elseif ( $totals['broken'] > 0 ) {
			\WP_CLI::success( sprintf( '%d broken file(s) handled (%d deleted, %d regenerated). Purge the page cache/CDN so cached HTML stops referencing repaired URLs from before.', $totals['broken'], $totals['deleted'], $totals['regenerated'] ) );
		} else {
			\WP_CLI::success( 'All existing WebP/AVIF conversions validate cleanly.' );
		}
	}

	// ─── Database ─────────────────────────────────────────────────────────

	/**
	 * Run database cleanup tasks.
	 *
	 * ## OPTIONS
	 *
	 * [--task=<task>]
	 * : Specific task to run: revisions|auto_drafts|trash|spam|transients|postmeta|optimize
	 * If omitted, all tasks are run.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost db cleanup
	 *     wp bcboost db cleanup --task=transients
	 *
	 * @subcommand db cleanup
	 * @param string[] $args       Positional args.
	 * @param array    $assoc_args Associative args.
	 */
	public function db_cleanup( array $args, array $assoc_args ): void {
		$db   = new DB_Optimizer();
		$task = $assoc_args['task'] ?? 'all';

		switch ( $task ) {
			case 'revisions':
				$n = $db->clean_post_revisions();
				\WP_CLI::success( "Deleted {$n} post revisions." );
				break;
			case 'auto_drafts':
				$n = $db->clean_auto_drafts();
				\WP_CLI::success( "Deleted {$n} auto drafts." );
				break;
			case 'trash':
				$n = $db->clean_trashed_posts();
				\WP_CLI::success( "Deleted {$n} trashed posts." );
				break;
			case 'spam':
				$n = $db->clean_spam_comments();
				\WP_CLI::success( "Deleted {$n} spam comments." );
				break;
			case 'transients':
				$n = $db->clean_expired_transients();
				\WP_CLI::success( "Deleted {$n} expired transients." );
				break;
			case 'postmeta':
				$n = $db->clean_orphaned_postmeta();
				\WP_CLI::success( "Deleted {$n} orphaned post meta rows." );
				break;
			case 'optimize':
				$t = $db->optimize_tables();
				\WP_CLI::success( "Optimized {$t} database tables." );
				break;
			default:
				$result = $db->run_all_cleanup();
				\WP_CLI::success( 'All cleanup tasks completed: ' . wp_json_encode( $result ) );
		}
	}

	// ─── Critical CSS ─────────────────────────────────────────────────────

	/**
	 * Show stored critical CSS: one row per template bucket and per-URL override,
	 * plus the async-CSS gate status. This is the fleet workflow's source of truth
	 * after generating CSS with tools/generate-critical-css.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost critical-css list
	 *
	 * @subcommand critical-css list
	 */
	public function critical_css_list(): void {
		$store  = Critical_CSS::get_store();
		$labels = Critical_CSS::template_labels();
		$rows   = [];

		foreach ( Critical_CSS::TEMPLATES as $template ) {
			$entry  = $store['templates'][ $template ] ?? null;
			$has    = is_array( $entry ) && ! empty( $entry['css'] );
			$rows[] = [
				'Target'  => $template,
				'Kind'    => 'template',
				'Label'   => $labels[ $template ] ?? $template,
				'Size'    => $has ? size_format( strlen( (string) $entry['css'] ) ) : '—',
				'Updated' => $has ? gmdate( 'Y-m-d H:i', (int) ( $entry['updated'] ?? 0 ) ) : '—',
				'Source'  => $has ? (string) ( $entry['source'] ?? 'manual' ) : '—',
				'Stale?'  => ( $has && Critical_CSS::is_entry_stale( $entry ) ) ? 'POSSIBLY' : '',
			];
		}
		foreach ( $store['urls'] as $path => $entry ) {
			$rows[] = [
				'Target'  => (string) $path,
				'Kind'    => 'url',
				'Label'   => 'per-URL override',
				'Size'    => size_format( strlen( (string) ( $entry['css'] ?? '' ) ) ),
				'Updated' => gmdate( 'Y-m-d H:i', (int) ( $entry['updated'] ?? 0 ) ),
				'Source'  => (string) ( $entry['source'] ?? 'manual' ),
				'Stale?'  => Critical_CSS::is_entry_stale( (array) $entry ) ? 'POSSIBLY' : '',
			];
		}

		\WP_CLI\Utils\format_items( 'table', $rows, [ 'Target', 'Kind', 'Label', 'Size', 'Updated', 'Source', 'Stale?' ] );

		$global = trim( (string) BCBOOST_get_option( 'critical_css', '' ) );
		\WP_CLI::log( 'Site-wide fallback (critical_css option): ' . ( '' !== $global ? size_format( strlen( $global ) ) : 'empty' ) );

		$stale = Critical_CSS::get_stale();
		if ( null !== $stale ) {
			\WP_CLI::warning( sprintf( 'Stale flag set: %s (%s). Entries older than that time need regenerating.', $stale['reason'], gmdate( 'Y-m-d H:i', $stale['time'] ) ) );
		}

		$lazy    = (bool) BCBOOST_get_option( 'lazy_css', false );
		$enabled = (bool) BCBOOST_get_option( 'critical_css_enabled', false );
		$any     = Critical_CSS::any_css_configured();
		if ( $lazy && $enabled && $any ) {
			\WP_CLI::success( 'Async CSS gate: OPEN on pages covered by the CSS above (uncovered pages stay render-blocking).' );
		} else {
			$why = ! $lazy ? 'lazy_css is off' : ( ! $enabled ? 'critical_css_enabled is off' : 'no critical CSS stored anywhere' );
			\WP_CLI::log( 'Async CSS gate: CLOSED (' . $why . ') — all CSS is render-blocking.' );
		}
	}

	/**
	 * Store critical CSS for a template (or a per-URL override).
	 *
	 * The content is sanitized (markup stripped, @import removed) and size-capped.
	 * On success the page cache is purged so the new inline CSS reaches visitors
	 * immediately. Note: async CSS additionally requires `lazy_css` and
	 * `critical_css_enabled` to be on — this command only fills the store.
	 *
	 * ## OPTIONS
	 *
	 * <template>
	 * : front_page | home | single_post | single_page | product | archive | default,
	 * or the literal `url` together with --url=<url> for a per-URL override.
	 *
	 * --file=<path>
	 * : Path to the CSS file to store (from tools/generate-critical-css).
	 *
	 * [--url=<url>]
	 * : Target URL/path — required when <template> is `url`.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost critical-css set front_page --file=front-page-critical.css
	 *     wp bcboost critical-css set url --url=/careers/ --file=careers-critical.css
	 *
	 * @subcommand critical-css set
	 * @param string[] $args       Positional args.
	 * @param array    $assoc_args Associative args.
	 */
	public function critical_css_set( array $args, array $assoc_args ): void {
		$target = (string) ( $args[0] ?? '' );
		$file   = (string) ( $assoc_args['file'] ?? '' );

		if ( '' === $file || ! file_exists( $file ) ) {
			\WP_CLI::error( 'CSS file not found: ' . $file );
		}
		$css = (string) file_get_contents( $file );
		if ( '' === trim( $css ) ) {
			\WP_CLI::error( 'CSS file is empty: ' . $file );
		}

		if ( 'url' === $target ) {
			$url = (string) ( $assoc_args['url'] ?? '' );
			if ( '' === $url ) {
				\WP_CLI::error( 'Per-URL override needs --url=<url>.' );
			}
			$result = Critical_CSS::set_url_css( $url, $css, 'cli' );
			$label  = Critical_CSS::normalize_path( $url );
		} else {
			$result = Critical_CSS::set_template_css( $target, $css, 'cli' );
			$label  = $target;
		}

		if ( is_wp_error( $result ) ) {
			\WP_CLI::error( $result->get_error_message() );
		}

		( new Cache_Purge() )->purge_all();
		\WP_CLI::success( sprintf( 'Critical CSS stored for %s (%s after sanitization). Page cache purged.', $label, size_format( strlen( Critical_CSS::sanitize_css( $css ) ) ) ) );
	}

	/**
	 * Delete stored critical CSS for a template or per-URL override.
	 *
	 * ## OPTIONS
	 *
	 * <template>
	 * : Template key, or the literal `url` together with --url=<url>.
	 *
	 * [--url=<url>]
	 * : Target URL/path — required when <template> is `url`.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost critical-css delete front_page
	 *     wp bcboost critical-css delete url --url=/careers/
	 *
	 * @subcommand critical-css delete
	 * @param string[] $args       Positional args.
	 * @param array    $assoc_args Associative args.
	 */
	public function critical_css_delete( array $args, array $assoc_args ): void {
		$target = (string) ( $args[0] ?? '' );

		if ( 'url' === $target ) {
			$url = (string) ( $assoc_args['url'] ?? '' );
			if ( '' === $url ) {
				\WP_CLI::error( 'Per-URL delete needs --url=<url>.' );
			}
			$deleted = Critical_CSS::delete_url_css( $url );
			$label   = Critical_CSS::normalize_path( $url );
		} else {
			if ( ! in_array( $target, Critical_CSS::TEMPLATES, true ) ) {
				\WP_CLI::error( 'Unknown template "' . $target . '". Valid: ' . implode( ', ', Critical_CSS::TEMPLATES ) . ', url' );
			}
			$deleted = Critical_CSS::delete_template_css( $target );
			$label   = $target;
		}

		if ( ! $deleted ) {
			\WP_CLI::warning( 'Nothing stored for ' . $label . ' — nothing deleted.' );
			return;
		}

		( new Cache_Purge() )->purge_all();
		\WP_CLI::success( 'Critical CSS deleted for ' . $label . '. Page cache purged. Async CSS closes automatically on pages this covered.' );
	}

	// ─── Settings ─────────────────────────────────────────────────────────

	/**
	 * Export plugin settings as JSON.
	 *
	 * ## OPTIONS
	 *
	 * [--file=<file>]
	 * : Write to a file instead of stdout.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost settings export
	 *     wp bcboost settings export --file=bcboost-settings.json
	 *
	 * @subcommand settings export
	 * @param string[] $args       Positional.
	 * @param array    $assoc_args Associative.
	 */
	public function settings_export( array $args, array $assoc_args ): void {
		$options = (array) get_option( BCBOOST_OPTIONS_KEY, [] );
		$json    = (string) wp_json_encode( $options, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );

		if ( ! empty( $assoc_args['file'] ) ) {
			file_put_contents( $assoc_args['file'], $json );
			\WP_CLI::success( 'Settings exported to ' . $assoc_args['file'] );
		} else {
			\WP_CLI::line( $json );
		}
	}

	/**
	 * Import plugin settings from a JSON file.
	 *
	 * ## OPTIONS
	 *
	 * <file>
	 * : Path to the JSON settings file.
	 *
	 * ## EXAMPLES
	 *
	 *     wp bcboost settings import bcboost-settings.json
	 *
	 * @subcommand settings import
	 * @param string[] $args Positional args.
	 */
	public function settings_import( array $args ): void {
		if ( empty( $args[0] ) || ! file_exists( $args[0] ) ) {
			\WP_CLI::error( 'Settings file not found: ' . ( $args[0] ?? '' ) );
		}

		$json    = file_get_contents( $args[0] );
		$options = json_decode( (string) $json, true );

		if ( ! is_array( $options ) ) {
			\WP_CLI::error( 'Invalid JSON in settings file.' );
		}

		$defaults  = Settings_API::get_defaults();
		$sanitized = [];
		foreach ( $defaults as $key => $default ) {
			if ( array_key_exists( $key, $options ) ) {
				$sanitized[ $key ] = Settings_API::sanitize_option( $key, $options[ $key ] );
			} else {
				$sanitized[ $key ] = $default;
			}
		}

		Settings_API::persist( $sanitized );

		\WP_CLI::success( 'Settings imported from ' . $args[0] );
	}
}

// Register the top-level commands: historic `bcb` plus the documented `bcboost`.
\WP_CLI::add_command( 'bcb', BCBOOST_CLI::class );
\WP_CLI::add_command( 'bcboost', BCBOOST_CLI::class );
