<?php
/**
 * Shared ZIP export utilities for plugin and theme download modules.
 *
 * @package WP_PowerSuite\Core\Export
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core\Export;

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

final class PluginThemeZipExporter {

	public const MAX_SIZE_BYTES      = 524288000;
	public const MAX_FILE_COUNT      = 50000;
	public const RATE_LIMIT_SECONDS  = 15;

	/**
	 * @var array<string, string>
	 */
	private static array $pending_temp_files = [];

	public static function validateSlug( string $slug ): bool {
		if ( $slug === '' || str_contains( $slug, '..' ) || str_contains( $slug, '/' ) || str_contains( $slug, '\\' ) ) {
			return false;
		}

		return (bool) preg_match( '/^[a-z0-9._\-]+$/i', $slug );
	}

	public static function isPathWithinRoot( string $path, string $root ): bool {
		$real_path = realpath( $path );
		$real_root = realpath( $root );

		if ( $real_path === false || $real_root === false ) {
			return false;
		}

		$real_path = wp_normalize_path( $real_path );
		$real_root = wp_normalize_path( $real_root );

		if ( $real_path === $real_root ) {
			return true;
		}

		return str_starts_with( $real_path, trailingslashit( $real_root ) );
	}

	public static function checkRateLimit( string $context ): ?\WP_Error {
		$user_id = get_current_user_id();
		if ( $user_id <= 0 ) {
			return new \WP_Error(
				'not_logged_in',
				__( 'You must be logged in to download exports.', 'wp-powersuite' )
			);
		}

		$key = 'wpps_zip_export_' . sanitize_key( $context ) . '_' . $user_id;
		if ( get_transient( $key ) ) {
			return new \WP_Error(
				'rate_limited',
				__( 'Please wait before starting another export.', 'wp-powersuite' )
			);
		}

		set_transient( $key, 1, self::RATE_LIMIT_SECONDS );

		return null;
	}

	/**
	 * Resolve a registered plugin to an on-disk source path.
	 *
	 * @return array{source: string, slug_for_zip: string, filename_base: string, is_single: bool}|\WP_Error
	 */
	public static function resolvePluginSource( string $plugin_file ): array|\WP_Error {
		$plugin_file = wp_normalize_path( trim( $plugin_file ) );
		if ( $plugin_file === '' || str_contains( $plugin_file, '..' ) ) {
			return new \WP_Error(
				'invalid_plugin',
				__( 'Invalid plugin specified.', 'wp-powersuite' )
			);
		}

		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugin_file = plugin_basename( $plugin_file );
		$plugins     = get_plugins();

		if ( ! isset( $plugins[ $plugin_file ] ) ) {
			return new \WP_Error(
				'invalid_plugin',
				__( 'Plugin not found or not registered.', 'wp-powersuite' )
			);
		}

		if ( str_contains( $plugin_file, '/' ) ) {
			$plugin_slug = dirname( $plugin_file );
			$is_single   = false;
		} else {
			$plugin_slug = basename( $plugin_file, '.php' );
			$is_single   = true;
		}

		if ( ! self::validateSlug( $plugin_slug ) ) {
			return new \WP_Error(
				'invalid_plugin',
				__( 'Invalid plugin path.', 'wp-powersuite' )
			);
		}

		if ( $is_single ) {
			$source = WP_PLUGIN_DIR . '/' . $plugin_file;
			if ( ! is_file( $source ) ) {
				return new \WP_Error(
					'not_found',
					__( 'Plugin file not found.', 'wp-powersuite' )
				);
			}
		} else {
			$source = WP_PLUGIN_DIR . '/' . $plugin_slug;
			if ( ! is_dir( $source ) ) {
				return new \WP_Error(
					'not_found',
					__( 'Plugin directory not found.', 'wp-powersuite' )
				);
			}
		}

		if ( ! self::isPathWithinRoot( $source, WP_PLUGIN_DIR ) ) {
			return new \WP_Error(
				'invalid_path',
				__( 'Invalid plugin path.', 'wp-powersuite' )
			);
		}

		$real_source = realpath( $source );
		if ( $real_source === false ) {
			return new \WP_Error(
				'invalid_path',
				__( 'Invalid plugin path.', 'wp-powersuite' )
			);
		}

		$filename_base = sanitize_file_name( $plugin_slug );

		return [
			'source'        => $real_source,
			'slug_for_zip'  => $filename_base,
			'filename_base' => $filename_base,
			'is_single'     => $is_single,
		];
	}

	/**
	 * Resolve a theme stylesheet slug to an on-disk source path.
	 *
	 * @return array{source: string, slug_for_zip: string, filename_base: string}|\WP_Error
	 */
	public static function resolveThemeSource( string $theme_slug ): array|\WP_Error {
		if ( ! self::validateSlug( $theme_slug ) ) {
			return new \WP_Error(
				'invalid_theme',
				__( 'Invalid theme path.', 'wp-powersuite' )
			);
		}

		$theme = wp_get_theme( $theme_slug );
		if ( ! $theme->exists() ) {
			return new \WP_Error(
				'not_found',
				__( 'Theme directory not found.', 'wp-powersuite' )
			);
		}

		$theme_dir = $theme->get_stylesheet_directory();
		if ( ! is_dir( $theme_dir ) ) {
			return new \WP_Error(
				'not_found',
				__( 'Theme directory not found.', 'wp-powersuite' )
			);
		}

		if ( ! self::isPathWithinRoot( $theme_dir, get_theme_root() ) ) {
			return new \WP_Error(
				'invalid_path',
				__( 'Invalid theme path.', 'wp-powersuite' )
			);
		}

		$real_source = realpath( $theme_dir );
		if ( $real_source === false ) {
			return new \WP_Error(
				'invalid_path',
				__( 'Invalid theme path.', 'wp-powersuite' )
			);
		}

		$filename_base = sanitize_file_name( $theme_slug );
		$version       = $theme->get( 'Version' );
		if ( is_string( $version ) && $version !== '' ) {
			$filename_base .= '-' . sanitize_file_name( $version );
		}

		return [
			'source'        => $real_source,
			'slug_for_zip'  => sanitize_file_name( basename( $real_source ) ),
			'filename_base' => $filename_base,
		];
	}

	/**
	 * @return string|\WP_Error Absolute path to a temporary ZIP file.
	 */
	public static function createZip( string $source, string $archive_folder_name, bool $is_single = false ): string|\WP_Error {
		if ( ! self::zipSupportAvailable() ) {
			return new \WP_Error(
				'no_zip_support',
				__( 'ZIP support is not available. Please contact your hosting provider to enable the PHP ZIP extension.', 'wp-powersuite' )
			);
		}

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

		@set_time_limit( 300 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged

		$source_size = self::getPathSize( $source );
		if ( is_wp_error( $source_size ) ) {
			return $source_size;
		}

		if ( $source_size > self::MAX_SIZE_BYTES ) {
			return new \WP_Error(
				'file_too_large',
				sprintf(
					/* translators: 1: archive size, 2: max size (500MB) */
					__( 'Archive size (%1$s) exceeds maximum (%2$s). Use FTP or another method.', 'wp-powersuite' ),
					size_format( $source_size ),
					size_format( self::MAX_SIZE_BYTES )
				)
			);
		}

		$required_space = $source_size * 2;
		if ( function_exists( 'disk_free_space' ) ) {
			$temp_dir = sys_get_temp_dir();
			$free     = @disk_free_space( $temp_dir ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			if ( $free !== false && $free < $required_space ) {
				return new \WP_Error(
					'insufficient_space',
					__( 'Insufficient disk space to create ZIP. Free up space and try again.', 'wp-powersuite' )
				);
			}
		}

		$temp_file = wp_tempnam( $archive_folder_name . '_' );
		if ( ! $temp_file ) {
			return new \WP_Error(
				'temp_failed',
				__( 'Cannot create temporary file. Check directory permissions.', 'wp-powersuite' )
			);
		}

		self::trackTempFile( $temp_file );

		if ( class_exists( 'ZipArchive' ) ) {
			$result = self::createZipWithZipArchive( $source, $archive_folder_name, $is_single, $temp_file );
		} else {
			$result = self::createZipWithPclZip( $source, $archive_folder_name, $is_single, $temp_file );
		}

		if ( is_wp_error( $result ) ) {
			self::deleteTempFile( $temp_file );
			return $result;
		}

		if ( ! file_exists( $temp_file ) || filesize( $temp_file ) === 0 ) {
			self::deleteTempFile( $temp_file );
			return new \WP_Error(
				'zip_empty',
				__( 'ZIP file was not created successfully.', 'wp-powersuite' )
			);
		}

		return $temp_file;
	}

	public static function streamZip( string $zip_file, string $filename_base ): void {
		if ( ! is_readable( $zip_file ) ) {
			self::deleteTempFile( $zip_file );
			wp_die(
				esc_html__( 'ZIP file is not readable.', 'wp-powersuite' ),
				esc_html__( 'Error', 'wp-powersuite' ),
				[ 'response' => 500 ]
			);
		}

		while ( ob_get_level() > 0 ) {
			ob_end_clean();
		}

		nocache_headers();
		header( 'Content-Type: application/zip' );
		header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename_base ) . '.zip"' );
		header( 'Content-Length: ' . (string) filesize( $zip_file ) );

		readfile( $zip_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile
		self::deleteTempFile( $zip_file );
		exit;
	}

	public static function shouldSkipFile( string $relative_path, string $context = 'export' ): bool {
		$relative_path = wp_normalize_path( $relative_path );
		$patterns      = [
			'node_modules/',
			'.git/',
			'.svn/',
			'.hg/',
			'.DS_Store',
			'Thumbs.db',
			'.env',
			'.env.local',
			'.env.production',
			'composer.lock',
			'package-lock.json',
			'yarn.lock',
			'.phpunit.result.cache',
			'.log',
			'.sql',
			'.sql.gz',
			'.bak',
			'.backup',
			'wp-config.php',
			'debug.log',
		];

		/**
		 * Filter relative paths excluded from plugin/theme ZIP exports.
		 *
		 * @since 1.0.0
		 * @param string[] $patterns       Path fragments to skip when matched inside the relative path.
		 * @param string   $relative_path  Path relative to the export root.
		 * @param string   $context        Export context identifier.
		 */
		$patterns = apply_filters( 'wp_powersuite_export_skip_patterns', $patterns, $relative_path, $context );

		foreach ( $patterns as $pattern ) {
			$pattern = (string) $pattern;
			if ( $pattern === '' ) {
				continue;
			}

			if ( str_ends_with( $pattern, '/' ) ) {
				if ( str_contains( $relative_path, $pattern ) ) {
					return true;
				}
				continue;
			}

			if ( str_contains( $relative_path, $pattern ) ) {
				return true;
			}
		}

		return false;
	}

	private static function zipSupportAvailable(): bool {
		if ( class_exists( 'ZipArchive' ) ) {
			return class_exists( 'RecursiveIteratorIterator' );
		}

		if ( ! class_exists( 'PclZip' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
		}

		return class_exists( 'PclZip' ) && class_exists( 'RecursiveIteratorIterator' );
	}

	/**
	 * @return int|\WP_Error
	 */
	private static function getPathSize( string $path ): int|\WP_Error {
		if ( is_file( $path ) ) {
			$size = filesize( $path );
			if ( $size === false ) {
				return new \WP_Error(
					'scan_failed',
					__( 'Could not determine file size.', 'wp-powersuite' )
				);
			}

			return (int) $size;
		}

		$size  = 0;
		$count = 0;

		try {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ),
				\RecursiveIteratorIterator::LEAVES_ONLY,
				\RecursiveIteratorIterator::CATCH_GET_CHILD
			);

			foreach ( $iterator as $file ) {
				if ( ! $file->isFile() ) {
					continue;
				}

				++$count;
				if ( $count > self::MAX_FILE_COUNT ) {
					return new \WP_Error(
						'too_many_files',
						__( 'Archive contains too many files to export safely.', 'wp-powersuite' )
					);
				}

				$size += $file->getSize();
			}
		} catch ( \Exception $e ) {
			return new \WP_Error(
				'scan_failed',
				__( 'Could not scan files for export.', 'wp-powersuite' )
			);
		}

		return $size;
	}

	private static function createZipWithZipArchive( string $source, string $archive_folder_name, bool $is_single, string $temp_file ): bool|\WP_Error {
		$zip = new \ZipArchive();
		if ( $zip->open( $temp_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
			return new \WP_Error(
				'zip_open_failed',
				__( 'Failed to create ZIP archive. Disk may be full or permissions incorrect.', 'wp-powersuite' )
			);
		}

		$count       = 0;
		$source_norm = wp_normalize_path( $source );

		if ( $is_single && is_file( $source ) ) {
			$zip->addFile( $source, $archive_folder_name . '.php' );
			$count = 1;
		} else {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( $source, \RecursiveDirectoryIterator::SKIP_DOTS ),
				\RecursiveIteratorIterator::LEAVES_ONLY,
				\RecursiveIteratorIterator::CATCH_GET_CHILD
			);

			foreach ( $iterator as $file ) {
				if ( ! $file->isFile() || ! $file->isReadable() ) {
					continue;
				}

				$file_path = $file->getRealPath();
				if ( $file_path === false ) {
					continue;
				}

				$file_path = wp_normalize_path( $file_path );
				if ( ! str_starts_with( $file_path, trailingslashit( $source_norm ) ) && $file_path !== $source_norm ) {
					continue;
				}

				$relative = ltrim( substr( $file_path, strlen( $source_norm ) ), '/' );
				if ( self::shouldSkipFile( $relative ) ) {
					continue;
				}

				$zip->addFile( $file_path, $archive_folder_name . '/' . $relative );
				++$count;
			}
		}

		$zip->close();

		if ( $count === 0 ) {
			return new \WP_Error(
				'no_files',
				__( 'No files were added to the ZIP. The directory may be empty or inaccessible.', 'wp-powersuite' )
			);
		}

		return true;
	}

	private static function createZipWithPclZip( string $source, string $archive_folder_name, bool $is_single, string $temp_file ): bool|\WP_Error {
		if ( ! class_exists( 'PclZip' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
		}

		if ( ! class_exists( 'PclZip' ) ) {
			return new \WP_Error(
				'pclzip_unavailable',
				__( 'PclZip is not available. Contact your hosting provider.', 'wp-powersuite' )
			);
		}

		$zip         = new \PclZip( $temp_file );
		$files       = [];
		$source_norm = wp_normalize_path( $source );

		if ( $is_single && is_file( $source ) ) {
			$files[] = $source;
		} else {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( $source, \RecursiveDirectoryIterator::SKIP_DOTS ),
				\RecursiveIteratorIterator::LEAVES_ONLY,
				\RecursiveIteratorIterator::CATCH_GET_CHILD
			);

			foreach ( $iterator as $file ) {
				if ( ! $file->isFile() || ! $file->isReadable() ) {
					continue;
				}

				$file_path = $file->getRealPath();
				if ( $file_path === false ) {
					continue;
				}

				$file_path = wp_normalize_path( $file_path );
				if ( ! str_starts_with( $file_path, trailingslashit( $source_norm ) ) && $file_path !== $source_norm ) {
					continue;
				}

				$relative = ltrim( substr( $file_path, strlen( $source_norm ) ), '/' );
				if ( self::shouldSkipFile( $relative ) ) {
					continue;
				}

				$files[] = $file_path;
			}
		}

		if ( $files === [] ) {
			return new \WP_Error(
				'no_files',
				__( 'No files were added to the ZIP. The directory may be empty or inaccessible.', 'wp-powersuite' )
			);
		}

		if ( $is_single && is_file( $source ) ) {
			$result = $zip->create( $files, PCLZIP_OPT_REMOVE_PATH, dirname( $source ), PCLZIP_OPT_ADD_PATH, $archive_folder_name );
		} else {
			$result = $zip->create( $files, PCLZIP_OPT_REMOVE_PATH, $source, PCLZIP_OPT_ADD_PATH, $archive_folder_name );
		}

		if ( $result === 0 ) {
			$err = $zip->errorInfo( true );
			return new \WP_Error(
				'pclzip_failed',
				sprintf(
					/* translators: %s: PclZip error message */
					__( 'Failed to create ZIP: %s', 'wp-powersuite' ),
					is_string( $err ) ? $err : ''
				)
			);
		}

		return true;
	}

	private static function trackTempFile( string $temp_file ): void {
		self::$pending_temp_files[ $temp_file ] = $temp_file;

		static $shutdown_registered = false;
		if ( $shutdown_registered ) {
			return;
		}

		$shutdown_registered = true;
		register_shutdown_function(
			static function (): void {
				foreach ( self::$pending_temp_files as $path ) {
					if ( is_string( $path ) && file_exists( $path ) ) {
						@unlink( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
					}
				}
				self::$pending_temp_files = [];
			}
		);
	}

	private static function deleteTempFile( string $temp_file ): void {
		unset( self::$pending_temp_files[ $temp_file ] );
		if ( file_exists( $temp_file ) ) {
			@unlink( $temp_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
		}
	}
}
