<?php
namespace DownloadMediaLibrary;

use WP_PowerSuite\Core\AbstractModule;

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

class Module extends AbstractModule {

	private const MODULE_ID              = 'download-media-library';
	private const MODULE_VERSION         = '1.0.0';
	private const NONCE_ACTION           = 'wpps_download_media_library';
	private const DOWNLOADS_SUBDIR       = 'powersuite-downloads';
	private const ZIP_EXPIRY_HOURS       = 24;
	private const FOLDER_TAXONOMY          = 'wpps_media_folder';
	private const RATE_LIMIT_SECONDS     = 60;
	private const STATS_CACHE_KEY        = 'wpps_dml_stats_cache';
	private const STATS_CACHE_TTL        = 300;
	private const MAX_ZIP_FILES          = 5000;
	private const MAX_ZIP_BYTES          = 2147483648;
	private const MAX_FOLDER_ATTACHMENTS = 2000;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Download Media Library', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Package your media library into downloadable ZIP files—the entire library, specific months, or folders (when Media Categories is enabled). ZIPs auto-expire after 24 hours.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'ajax', 'cron' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		add_action( 'wpps_dml_cleanup_expired_zips', [ $this, 'cleanupExpiredZips' ] );

		if ( wp_doing_ajax() ) {
			add_action( 'wp_ajax_wpps_dml_get_stats', [ $this, 'ajaxGetStats' ] );
			add_action( 'wp_ajax_wpps_dml_create_zip', [ $this, 'ajaxCreateZip' ] );
			add_action( 'wp_ajax_wpps_dml_delete_zip', [ $this, 'ajaxDeleteZip' ] );
			add_action( 'wp_ajax_wpps_dml_download_zip', [ $this, 'ajaxDownloadZip' ] );
		}

		if ( ! wp_doing_ajax() && ! wp_next_scheduled( 'wpps_dml_cleanup_expired_zips' ) ) {
			wp_schedule_event( time(), 'hourly', 'wpps_dml_cleanup_expired_zips' );
		}
	}

	private function verifyRequest(): void {
		if ( ! $this->isEnabled() ) {
			wp_send_json_error( [ 'message' => __( 'Module is disabled.', 'wp-powersuite' ) ] );
		}
		if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'wp-powersuite' ) ] );
		}
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
	}

	public function getDownloadsDir(): string {
		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			return '';
		}
		return trailingslashit( $upload_dir['basedir'] ) . self::DOWNLOADS_SUBDIR;
	}

	private function ensureDownloadsDir(): bool {
		$dir = $this->getDownloadsDir();
		if ( empty( $dir ) ) {
			return false;
		}
		if ( ! wp_mkdir_p( $dir ) ) {
			return false;
		}
		$index_file = $dir . 'index.php';
		if ( ! file_exists( $index_file ) ) {
			file_put_contents( $index_file, '<?php // Silence is golden.' );
		}
		$htaccess_file = trailingslashit( $dir ) . '.htaccess';
		if ( ! file_exists( $htaccess_file ) ) {
			$htaccess  = "# Block direct access - downloads must go through admin-ajax.php\n";
			$htaccess .= "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n";
			$htaccess .= "<IfModule !mod_authz_core.c>\n\tOrder Deny,Allow\n\tDeny from all\n</IfModule>\n";
			file_put_contents( $htaccess_file, $htaccess );
		}
		return true;
	}

	public function ajaxGetStats(): void {
		$this->verifyRequest();

		$cached = get_transient( self::STATS_CACHE_KEY );
		if ( is_array( $cached ) ) {
			wp_send_json_success( $cached );
		}

		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			wp_send_json_error( [ 'message' => __( 'Upload directory error.', 'wp-powersuite' ) ] );
		}

		$base       = trailingslashit( $upload_dir['basedir'] );
		$total_size = 0;
		$months     = [];
		$folders    = [];

		if ( is_dir( $base ) ) {
			$years = array_filter( glob( $base . '[0-9][0-9][0-9][0-9]', GLOB_ONLYDIR ), 'is_dir' );
			foreach ( $years as $year_dir ) {
				$year       = basename( $year_dir );
				$month_dirs = array_filter( glob( $year_dir . DIRECTORY_SEPARATOR . '[0-9][0-9]', GLOB_ONLYDIR ), 'is_dir' );
				foreach ( $month_dirs as $month_dir ) {
					$month = basename( $month_dir );
					$key   = $year . '-' . $month;
					$count = 0;
					$size  = 0;
					$iterator = new \RecursiveIteratorIterator(
						new \RecursiveDirectoryIterator( $month_dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
						\RecursiveIteratorIterator::LEAVES_ONLY
					);
					foreach ( $iterator as $file ) {
						if ( $file->isFile() ) {
							++$count;
							$size += $file->getSize();
						}
					}
					$months[ $key ] = [
						'year'  => $year,
						'month' => $month,
						'count' => $count,
						'size'  => $size,
					];
					$total_size += $size;
				}
			}
		}

		$root_count = 0;
		$root_size  = 0;
		if ( is_dir( $base ) ) {
			$items = @scandir( $base );
			if ( is_array( $items ) ) {
				foreach ( $items as $item ) {
					if ( $item === '.' || $item === '..' || $item === self::DOWNLOADS_SUBDIR ) {
						continue;
					}
					$full = $base . $item;
					if ( is_file( $full ) ) {
						++$root_count;
						$root_size += (int) @filesize( $full );
					} elseif ( is_dir( $full ) && ! preg_match( '/^\d{4}$/', $item ) ) {
						$iterator = new \RecursiveIteratorIterator(
							new \RecursiveDirectoryIterator( $full, \RecursiveDirectoryIterator::SKIP_DOTS ),
							\RecursiveIteratorIterator::LEAVES_ONLY
						);
						foreach ( $iterator as $file ) {
							if ( $file->isFile() ) {
								++$root_count;
								$root_size += $file->getSize();
							}
						}
					}
				}
			}
		}
		$total_size += $root_size;

		if ( $this->isContentFoldersEnabled() && taxonomy_exists( self::FOLDER_TAXONOMY ) ) {
			$terms = get_terms( [
				'taxonomy'   => self::FOLDER_TAXONOMY,
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
				'fields'     => 'all',
			] );
			if ( ! is_wp_error( $terms ) ) {
				foreach ( $terms as $term ) {
					$sizes     = $this->getFolderSizes( (int) $term->term_id );
					$folders[] = [
						'term_id'              => (int) $term->term_id,
						'name'                 => $term->name,
						'count'                => (int) $term->count,
						'size'                 => $sizes['size'],
						'size_with_children'   => $sizes['size_with_children'],
					];
				}
			}
		}

		$months_list = array_values( $months );
		usort(
			$months_list,
			static function ( array $a, array $b ): int {
				$key_a = ( $a['year'] ?? '' ) . ( $a['month'] ?? '' );
				$key_b = ( $b['year'] ?? '' ) . ( $b['month'] ?? '' );
				return strcmp( $key_b, $key_a );
			}
		);

		$stats = [
			'total_size'       => (int) $total_size,
			'total_count'      => array_sum( array_column( $months_list, 'count' ) ) + $root_count,
			'months'           => $months_list,
			'folders'          => $folders,
			'zips'             => $this->getExistingZips(),
			'zip_expiry_hours' => self::ZIP_EXPIRY_HOURS,
		];

		set_transient( self::STATS_CACHE_KEY, $stats, self::STATS_CACHE_TTL );
		wp_send_json_success( $stats );
	}

	/**
	 * @param array<int> $term_ids
	 * @return array<int>
	 */
	private function getFolderAttachmentIds( array $term_ids ): array {
		$term_ids = array_values( array_filter( array_map( 'intval', $term_ids ) ) );
		if ( empty( $term_ids ) ) {
			return [];
		}
		$posts = get_posts( [
			'post_type'              => 'attachment',
			'post_status'            => 'inherit',
			'posts_per_page'         => self::MAX_FOLDER_ATTACHMENTS,
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'tax_query'              => [
				[
					'taxonomy' => self::FOLDER_TAXONOMY,
					'field'    => 'term_id',
					'terms'    => $term_ids,
				],
			],
		] );
		return is_array( $posts ) ? array_map( 'intval', $posts ) : [];
	}

	/**
	 * @return array{size: int, size_with_children: int}
	 */
	private function getFolderSizes( int $term_id ): array {
		$direct_ids = $this->getFolderAttachmentIds( [ $term_id ] );
		$direct     = $this->sumAttachmentSizes( $direct_ids );

		$children = get_terms( [
			'taxonomy'   => self::FOLDER_TAXONOMY,
			'child_of'   => $term_id,
			'hide_empty' => false,
			'fields'     => 'ids',
		] );
		if ( is_wp_error( $children ) || empty( $children ) ) {
			return [
				'size'               => $direct,
				'size_with_children' => $direct,
			];
		}

		$child_ids = $this->getFolderAttachmentIds( array_map( 'intval', $children ) );
		$all_ids   = array_values( array_unique( array_merge( $direct_ids, $child_ids ) ) );

		return [
			'size'               => $direct,
			'size_with_children' => $this->sumAttachmentSizes( $all_ids ),
		];
	}

	/**
	 * @param array<int> $attachment_ids
	 */
	private function sumAttachmentSizes( array $attachment_ids ): int {
		$size = 0;
		foreach ( $attachment_ids as $attachment_id ) {
			$file = get_attached_file( $attachment_id );
			if ( $file && file_exists( $file ) ) {
				$size += (int) filesize( $file );
			}
		}
		return $size;
	}

	private function isContentFoldersEnabled(): bool {
		return \WP_PowerSuite\Core\OptionStoreModuleSettingsRepository::isModuleEnabled( 'content-folders' );
	}

	/**
	 * @return array<array{filename: string, size: int, created: int, expiry_ts: int}>
	 */
	private function getExistingZips(): array {
		$dir = $this->getDownloadsDir();
		if ( ! is_dir( $dir ) ) {
			return [];
		}
		$files = glob( trailingslashit( $dir ) . '*.zip' );
		if ( ! is_array( $files ) ) {
			return [];
		}
		$expiry         = time() - ( self::ZIP_EXPIRY_HOURS * HOUR_IN_SECONDS );
		$expiry_seconds = self::ZIP_EXPIRY_HOURS * HOUR_IN_SECONDS;
		$zips           = [];
		foreach ( $files as $path ) {
			$mtime = filemtime( $path );
			if ( $mtime === false || $mtime < $expiry ) {
				continue;
			}
			$zips[] = [
				'filename'  => basename( $path ),
				'size'      => (int) filesize( $path ),
				'created'   => $mtime,
				'expiry_ts' => $mtime + $expiry_seconds,
			];
		}
		usort( $zips, static fn( array $a, array $b ): int => $b['created'] <=> $a['created'] );
		return $zips;
	}

	public function ajaxCreateZip(): void {
		$this->verifyRequest();

		$user_id  = get_current_user_id();
		$rate_key = 'wpps_dml_rate_' . $user_id;
		if ( get_transient( $rate_key ) ) {
			wp_send_json_error( [
				'message' => sprintf(
					/* translators: %d: seconds */
					__( 'Please wait %d seconds before creating another ZIP.', 'wp-powersuite' ),
					self::RATE_LIMIT_SECONDS
				),
			] );
		}

		$type               = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : '';
		$month              = isset( $_POST['month'] ) ? sanitize_text_field( wp_unslash( $_POST['month'] ) ) : '';
		$months_raw         = isset( $_POST['months'] ) ? wp_unslash( $_POST['months'] ) : [];
		$months             = is_array( $months_raw )
			? array_map( 'sanitize_text_field', $months_raw )
			: array_filter( array_map( 'trim', explode( ',', (string) $months_raw ) ) );
		$folder_id          = isset( $_POST['folder_id'] ) ? (int) $_POST['folder_id'] : 0;
		$include_subfolders = ! empty( $_POST['include_subfolders'] );

		if ( ! in_array( $type, [ 'full', 'month', 'months', 'folder' ], true ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid download type.', 'wp-powersuite' ) ] );
		}

		if ( $type === 'month' && ! $this->isValidMonthKey( $month ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid month.', 'wp-powersuite' ) ] );
		}

		if ( $type === 'months' ) {
			$months = array_values( array_filter( $months, [ $this, 'isValidMonthKey' ] ) );
			if ( empty( $months ) ) {
				wp_send_json_error( [ 'message' => __( 'Month(s) required.', 'wp-powersuite' ) ] );
			}
		}

		if ( $type === 'folder' && $folder_id <= 0 ) {
			wp_send_json_error( [ 'message' => __( 'Folder required.', 'wp-powersuite' ) ] );
		}

		if ( ! $this->ensureDownloadsDir() ) {
			wp_send_json_error( [ 'message' => __( 'Could not create downloads directory.', 'wp-powersuite' ) ] );
		}

		if ( ! class_exists( 'ZipArchive' ) ) {
			wp_send_json_error( [ 'message' => __( 'ZipArchive is not available on this server.', 'wp-powersuite' ) ] );
		}

		@set_time_limit( 300 );
		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'admin' );
		}

		$files = $this->getFilesForZip( $type, $month, $months, $folder_id, $include_subfolders );
		if ( empty( $files ) ) {
			wp_send_json_error( [ 'message' => __( 'No files to include in ZIP.', 'wp-powersuite' ) ] );
		}

		$upload_dir = wp_upload_dir();
		$base_dir   = trailingslashit( $upload_dir['basedir'] );

		if ( count( $files ) > self::MAX_ZIP_FILES ) {
			wp_send_json_error( [ 'message' => __( 'Too many files for a single ZIP. Please download by month or folder instead.', 'wp-powersuite' ) ] );
		}

		$total_bytes = 0;
		foreach ( $files as $rel_path ) {
			$full_path = $base_dir . $rel_path;
			if ( is_file( $full_path ) ) {
				$total_bytes += (int) @filesize( $full_path );
				if ( $total_bytes > self::MAX_ZIP_BYTES ) {
					wp_send_json_error( [ 'message' => __( 'Selection is too large for a single ZIP. Please download by month or folder instead.', 'wp-powersuite' ) ] );
				}
			}
		}

		$suffix = $type;
		if ( $type === 'month' ) {
			$suffix .= '-' . str_replace( '-', '', $month );
		} elseif ( $type === 'months' ) {
			$suffix .= '-' . implode( '-', array_map( static fn( string $m ): string => str_replace( '-', '', $m ), $months ) );
		} elseif ( $type === 'folder' ) {
			$term = get_term( $folder_id, self::FOLDER_TAXONOMY );
			$suffix .= '-' . ( $term && ! is_wp_error( $term ) ? sanitize_file_name( $term->name ) : (string) $folder_id );
			if ( $include_subfolders ) {
				$suffix .= '-with-children';
			}
		}

		$zip_name = 'media-' . $suffix . '-' . gmdate( 'Y-m-d-His' ) . '.zip';
		$zip_path = trailingslashit( $this->getDownloadsDir() ) . $zip_name;

		$zip = new \ZipArchive();
		if ( $zip->open( $zip_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
			wp_send_json_error( [ 'message' => __( 'Could not create ZIP file.', 'wp-powersuite' ) ] );
		}

		$added = 0;
		foreach ( $files as $rel_path ) {
			$full_path = $base_dir . $rel_path;
			if ( file_exists( $full_path ) && is_readable( $full_path ) ) {
				$zip->addFile( $full_path, $rel_path );
				++$added;
			}
		}
		$zip->close();

		if ( $added === 0 ) {
			if ( file_exists( $zip_path ) ) {
				@unlink( $zip_path );
			}
			wp_send_json_error( [ 'message' => __( 'No files could be added to ZIP.', 'wp-powersuite' ) ] );
		}

		set_transient( $rate_key, 1, self::RATE_LIMIT_SECONDS );
		delete_transient( self::STATS_CACHE_KEY );

		$created   = time();
		$expiry_ts = $created + ( self::ZIP_EXPIRY_HOURS * HOUR_IN_SECONDS );
		wp_send_json_success( [
			'filename'  => $zip_name,
			'size'      => (int) filesize( $zip_path ),
			'created'   => $created,
			'expiry_ts' => $expiry_ts,
		] );
	}

	/**
	 * @param string[] $months
	 * @return array<string>
	 */
	private function getFilesForZip( string $type, string $month, array $months, int $folder_id, bool $include_subfolders ): array {
		$upload_dir = wp_upload_dir();
		$base       = trailingslashit( $upload_dir['basedir'] );
		$files      = [];

		if ( $type === 'full' ) {
			$files = $this->getAllUploadPaths( $base );
		} elseif ( $type === 'month' && $this->isValidMonthKey( $month ) ) {
			$parts     = explode( '-', $month );
			$full_path = $base . $parts[0] . DIRECTORY_SEPARATOR . $parts[1];
			if ( is_dir( $full_path ) ) {
				$files = $this->getPathsInDir( $full_path, $base );
			}
		} elseif ( $type === 'months' ) {
			foreach ( $months as $m ) {
				if ( ! $this->isValidMonthKey( $m ) ) {
					continue;
				}
				$parts     = explode( '-', $m );
				$full_path = $base . $parts[0] . DIRECTORY_SEPARATOR . $parts[1];
				if ( is_dir( $full_path ) ) {
					$files = array_merge( $files, $this->getPathsInDir( $full_path, $base ) );
				}
			}
		} elseif ( $type === 'folder' ) {
			$term_ids = [ $folder_id ];
			if ( $include_subfolders ) {
				$children = get_terms( [
					'taxonomy'   => self::FOLDER_TAXONOMY,
					'child_of'   => $folder_id,
					'hide_empty' => false,
					'fields'     => 'ids',
				] );
				if ( ! is_wp_error( $children ) ) {
					$term_ids = array_merge( $term_ids, array_map( 'intval', $children ) );
				}
			}
			foreach ( $this->getFolderAttachmentIds( $term_ids ) as $attachment_id ) {
				$file = get_attached_file( $attachment_id );
				if ( ! $file || ! file_exists( $file ) ) {
					continue;
				}
				$rel = str_replace( '\\', '/', ltrim( str_replace( $base, '', $file ), '/\\' ) );
				if ( strpos( $rel, '..' ) === false ) {
					$files[] = $rel;
				}
			}
		}

		return array_values( array_unique( $files ) );
	}

	/**
	 * @return array<string>
	 */
	private function getAllUploadPaths( string $base ): array {
		$files   = [];
		$exclude = self::DOWNLOADS_SUBDIR;
		$iterator = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator( $base, \RecursiveDirectoryIterator::SKIP_DOTS ),
			\RecursiveIteratorIterator::LEAVES_ONLY
		);
		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}
			$path = $file->getPathname();
			if ( strpos( $path, $exclude ) !== false ) {
				continue;
			}
			$rel = str_replace( '\\', '/', ltrim( str_replace( $base, '', $path ), '/\\' ) );
			if ( strpos( $rel, '..' ) === false ) {
				$files[] = $rel;
			}
		}
		return $files;
	}

	/**
	 * @return array<string>
	 */
	private function getPathsInDir( string $dir, string $base ): array {
		$files    = [];
		$iterator = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
			\RecursiveIteratorIterator::LEAVES_ONLY
		);
		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}
			$rel = str_replace( '\\', '/', ltrim( str_replace( $base, '', $file->getPathname() ), '/\\' ) );
			if ( strpos( $rel, '..' ) === false ) {
				$files[] = $rel;
			}
		}
		return $files;
	}

	public function ajaxDeleteZip(): void {
		$this->verifyRequest();

		$filename = isset( $_POST['filename'] ) ? sanitize_file_name( wp_unslash( $_POST['filename'] ) ) : '';
		if ( empty( $filename ) || strpos( $filename, '..' ) !== false || ! preg_match( '/\.zip$/i', $filename ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid filename.', 'wp-powersuite' ) ] );
		}

		if ( ! $this->deleteZipFile( $filename ) ) {
			wp_send_json_error( [ 'message' => __( 'Could not delete file.', 'wp-powersuite' ) ] );
		}

		delete_transient( self::STATS_CACHE_KEY );
		wp_send_json_success();
	}

	public function ajaxDownloadZip(): void {
		if ( ! $this->isEnabled() ) {
			wp_die( esc_html__( 'Module is disabled.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}

		$filename = '';
		if ( isset( $_POST['wpps_dml_file'] ) ) {
			$filename = sanitize_file_name( wp_unslash( $_POST['wpps_dml_file'] ) );
		} elseif ( isset( $_GET['wpps_dml_file'] ) ) {
			$filename = sanitize_file_name( wp_unslash( $_GET['wpps_dml_file'] ) );
		}

		if ( empty( $filename ) || strpos( $filename, '..' ) !== false || ! preg_match( '/\.zip$/i', $filename ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}
		if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
			wp_die( esc_html__( 'Invalid security token.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}
		if ( ! $this->canManagePlugin() ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}

		$path = $this->resolveZipPath( $filename );
		if ( $path === null ) {
			wp_die( esc_html__( 'File not found.', 'wp-powersuite' ), '', [ 'response' => 404 ] );
		}

		nocache_headers();
		header( 'Content-Type: application/zip' );
		header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
		header( 'Content-Length: ' . filesize( $path ) );
		readfile( $path );
		exit;
	}

	public function cleanupExpiredZips(): void {
		if ( ! $this->isEnabled() ) {
			return;
		}
		$dir = $this->getDownloadsDir();
		if ( ! is_dir( $dir ) ) {
			return;
		}
		$expiry = time() - ( self::ZIP_EXPIRY_HOURS * HOUR_IN_SECONDS );
		$files  = glob( trailingslashit( $dir ) . '*.zip' );
		if ( ! is_array( $files ) ) {
			return;
		}
		foreach ( $files as $path ) {
			$mtime = filemtime( $path );
			if ( $mtime !== false && $mtime < $expiry ) {
				@unlink( $path );
			}
		}
	}

	private function isValidMonthKey( string $month ): bool {
		return (bool) preg_match( '/^\d{4}-\d{2}$/', $month );
	}

	private function resolveZipPath( string $filename ): ?string {
		$dir  = $this->getDownloadsDir();
		$path = trailingslashit( $dir ) . $filename;
		if ( ! file_exists( $path ) || ! is_file( $path ) ) {
			return null;
		}
		$real_dir  = realpath( trailingslashit( $dir ) );
		$real_path = realpath( $path );
		if ( ! $real_dir || ! $real_path || strpos( $real_path, $real_dir ) !== 0 ) {
			return null;
		}
		return $real_path;
	}

	private function deleteZipFile( string $filename ): bool {
		$path = $this->resolveZipPath( $filename );
		if ( $path === null ) {
			return false;
		}
		return (bool) @unlink( $path );
	}
}
