<?php
/**
 * SVG icon MCP abilities for Elementor.
 *
 * Registers tools for uploading SVG icons and using them with Elementor
 * icon and icon-box widgets.
 *
 * @package EMCP_Tools
 * @since   1.2.0
 */

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

/**
 * Registers and implements the SVG icon abilities.
 *
 * @since 1.2.0
 */
class EMCP_Tools_Svg_Icon_Abilities {

	/**
	 * @var EMCP_Tools_Data
	 */
	private $data;

	/**
	 * @var EMCP_Tools_Element_Factory
	 */
	private $factory;

	/**
	 * Constructor.
	 *
	 * @since 1.2.0
	 *
	 * @param EMCP_Tools_Data            $data    The data access layer.
	 * @param EMCP_Tools_Element_Factory $factory The element factory.
	 */
	public function __construct( EMCP_Tools_Data $data, EMCP_Tools_Element_Factory $factory ) {
		$this->data    = $data;
		$this->factory = $factory;
	}

	/**
	 * Returns the ability names registered by this class.
	 *
	 * @since 1.2.0
	 *
	 * @return string[]
	 */
	public function get_ability_names(): array {
		return array(
			'emcp-tools/upload-svg-icon',
		);
	}

	/**
	 * Registers all SVG icon abilities.
	 *
	 * @since 1.2.0
	 */
	public function register(): void {
		$this->register_upload_svg_icon();
	}

	// -------------------------------------------------------------------------
	// Permission callbacks
	// -------------------------------------------------------------------------

	/**
	 * Permission check for uploading SVG files.
	 *
	 * @since 1.2.0
	 *
	 * @return bool
	 */
	public function check_upload_permission(): bool {
		return current_user_can( 'upload_files' );
	}

	// -------------------------------------------------------------------------
	// upload-svg-icon
	// -------------------------------------------------------------------------

	/**
	 * Registers the upload-svg-icon ability.
	 *
	 * @since 1.2.0
	 */
	private function register_upload_svg_icon(): void {
		emcp_tools_register_ability(
			'emcp-tools/upload-svg-icon',
			array(
				'label'               => __( 'Upload SVG Icon', 'emcp-tools' ),
				'description'         => __( 'Uploads an SVG icon to the WordPress Media Library and returns an Elementor icon object ready to use with any widget that accepts icons (icon, icon-box, button, etc.). Accepts either an external SVG URL or raw SVG markup. The returned icon object has the format: { "value": { "id": 123, "url": "..." }, "library": "svg" }. Use this value for the selected_icon setting in icon/icon-box widgets, or the selected_icon setting in button widgets.', 'emcp-tools' ),
				'category'            => 'emcp-tools',
				'execute_callback'    => array( $this, 'execute_upload_svg_icon' ),
				'permission_callback' => array( $this, 'check_upload_permission' ),
				'input_schema'        => array(
					'type'       => 'object',
					'properties' => array(
						'svg_url'     => array(
							'type'        => 'string',
							'description' => __( 'External URL to an SVG file to download and import. Use this OR svg_content, not both.', 'emcp-tools' ),
						),
						'svg_content' => array(
							'type'        => 'string',
							'description' => __( 'Raw SVG markup string (e.g. "<svg viewBox=\'0 0 24 24\'><path d=\'M12 ...\'/></svg>"). Use this OR svg_url, not both.', 'emcp-tools' ),
						),
						'title'       => array(
							'type'        => 'string',
							'description' => __( 'Title for the SVG in the Media Library. Falls back to filename or "Custom SVG Icon".', 'emcp-tools' ),
						),
					),
				),
				'output_schema'       => array(
					'type'       => 'object',
					'properties' => array(
						'attachment_id' => array( 'type' => 'integer' ),
						'url'           => array( 'type' => 'string' ),
						'icon_object'   => array(
							'type'        => 'object',
							'description' => __( 'Ready-to-use Elementor icon object. Pass this directly as the selected_icon setting.', 'emcp-tools' ),
							'properties'  => array(
								'value'   => array(
									'type'       => 'object',
									'properties' => array(
										'id'  => array( 'type' => 'integer' ),
										'url' => array( 'type' => 'string' ),
									),
								),
								'library' => array( 'type' => 'string' ),
							),
						),
					),
				),
				'meta'                => array(
					'annotations'  => array(
						'readonly'    => false,
						'destructive' => false,
						'idempotent'  => false,
					),
					'show_in_rest' => true,
				),
			)
		);
	}

	/**
	 * Executes the upload-svg-icon ability.
	 *
	 * Handles two input modes:
	 * 1. svg_url: Downloads an external SVG file and imports it.
	 * 2. svg_content: Takes raw SVG markup, writes to temp file, and imports it.
	 *
	 * @since 1.2.0
	 *
	 * @param array $input The input parameters.
	 * @return array|\WP_Error
	 */
	public function execute_upload_svg_icon( $input ) {
		$svg_url     = esc_url_raw( $input['svg_url'] ?? '' );
		$svg_content = $input['svg_content'] ?? '';
		$title       = sanitize_text_field( $input['title'] ?? '' );

		if ( empty( $svg_url ) && empty( $svg_content ) ) {
			return new \WP_Error(
				'missing_input',
				__( 'Either svg_url or svg_content is required.', 'emcp-tools' )
			);
		}

		if ( ! empty( $svg_url ) && ! empty( $svg_content ) ) {
			return new \WP_Error(
				'conflicting_input',
				__( 'Provide either svg_url or svg_content, not both.', 'emcp-tools' )
			);
		}

		// Load required WordPress media functions.
		if ( ! function_exists( 'media_handle_sideload' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/media.php';
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}

		// Enable SVG uploads temporarily via filter.
		$allow_svg = function ( $mimes ) {
			$mimes['svg'] = 'image/svg+xml';
			return $mimes;
		};
		add_filter( 'upload_mimes', $allow_svg );

		// Also bypass real MIME check for SVG (WordPress misidentifies SVG MIME).
		$fix_svg_mime = function ( $data, $file, $filename ) {
			if ( ! empty( $data['ext'] ) && 'svg' === $data['ext'] ) {
				$data['type'] = 'image/svg+xml';
			}
			if ( empty( $data['ext'] ) && ! empty( $filename ) ) {
				$ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
				if ( 'svg' === $ext ) {
					$data['ext']             = 'svg';
					$data['type']            = 'image/svg+xml';
					$data['proper_filename'] = $filename;
				}
			}
			return $data;
		};
		add_filter( 'wp_check_filetype_and_ext', $fix_svg_mime, 10, 3 );

		if ( ! empty( $svg_url ) ) {
			$result = $this->upload_from_url( $svg_url, $title );
		} else {
			$result = $this->upload_from_content( $svg_content, $title );
		}

		// Remove our temporary filters.
		remove_filter( 'upload_mimes', $allow_svg );
		remove_filter( 'wp_check_filetype_and_ext', $fix_svg_mime, 10 );

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$attachment_id = $result['attachment_id'];
		$local_url     = $result['url'];

		// Set the title.
		if ( ! empty( $title ) ) {
			wp_update_post( array(
				'ID'         => $attachment_id,
				'post_title' => $title,
			) );
		}

		// Build the Elementor icon object.
		$icon_object = array(
			'value'   => array(
				'id'  => $attachment_id,
				'url' => $local_url,
			),
			'library' => 'svg',
		);

		return array(
			'attachment_id' => $attachment_id,
			'url'           => $local_url,
			'icon_object'   => $icon_object,
		);
	}

	/**
	 * Uploads an SVG file from an external URL.
	 *
	 * @since 1.2.0
	 *
	 * @param string $url   The external SVG URL.
	 * @param string $title Optional title for filename fallback.
	 * @return array|\WP_Error Array with attachment_id and url on success.
	 */
	private function upload_from_url( string $url, string $title ) {
		// SSRF-guarded download: blocks private/reserved/loopback hosts and
		// re-validates each redirect hop.
		$tmp_file = EMCP_Tools_Url_Guard::safe_download( $url, 30 );

		if ( is_wp_error( $tmp_file ) ) {
			return new \WP_Error(
				'download_failed',
				sprintf(
					/* translators: %s: error message */
					__( 'Failed to download SVG: %s', 'emcp-tools' ),
					$tmp_file->get_error_message()
				)
			);
		}

		// Validate the downloaded file contains SVG content.
		$validation = $this->validate_svg_file( $tmp_file );
		if ( is_wp_error( $validation ) ) {
			wp_delete_file( $tmp_file );
			return $validation;
		}

		// Determine filename.
		$url_path = wp_parse_url( $url, PHP_URL_PATH );
		$filename = $url_path ? basename( $url_path ) : 'icon.svg';

		if ( ! preg_match( '/\.svg$/i', $filename ) ) {
			$filename = ( ! empty( $title ) ? sanitize_title( $title ) : 'icon' ) . '.svg';
		}

		return $this->do_sideload( $tmp_file, $filename );
	}

	/**
	 * Uploads an SVG from raw markup content.
	 *
	 * @since 1.2.0
	 *
	 * @param string $content Raw SVG markup.
	 * @param string $title   Optional title for filename.
	 * @return array|\WP_Error Array with attachment_id and url on success.
	 */
	private function upload_from_content( string $content, string $title ) {
		// Basic validation: must contain <svg tag.
		if ( stripos( $content, '<svg' ) === false ) {
			return new \WP_Error(
				'invalid_svg',
				__( 'The svg_content does not contain valid SVG markup. Must include an <svg> element.', 'emcp-tools' )
			);
		}

		// Sanitize the SVG content.
		$sanitized = $this->sanitize_svg_content( $content );
		if ( is_wp_error( $sanitized ) ) {
			return $sanitized;
		}

		// Write to a temp file.
		$tmp_file = wp_tempnam( 'svg_icon_' );
		if ( ! $tmp_file ) {
			return new \WP_Error(
				'temp_file_failed',
				__( 'Could not create temporary file for SVG upload.', 'emcp-tools' )
			);
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		file_put_contents( $tmp_file, $sanitized );

		$filename = ( ! empty( $title ) ? sanitize_title( $title ) : 'custom-icon' ) . '.svg';

		return $this->do_sideload( $tmp_file, $filename );
	}

	/**
	 * Performs the actual sideload into the WordPress Media Library.
	 *
	 * @since 1.2.0
	 *
	 * @param string $tmp_file Path to the temporary file.
	 * @param string $filename The desired filename.
	 * @return array|\WP_Error Array with attachment_id and url on success.
	 */
	private function do_sideload( string $tmp_file, string $filename ) {
		$file_array = array(
			'name'     => sanitize_file_name( $filename ),
			'tmp_name' => $tmp_file,
		);

		$attachment_id = media_handle_sideload( $file_array, 0 );

		if ( is_wp_error( $attachment_id ) ) {
			if ( file_exists( $tmp_file ) ) {
				wp_delete_file( $tmp_file );
			}

			return new \WP_Error(
				'sideload_failed',
				sprintf(
					/* translators: %s: error message */
					__( 'Failed to sideload SVG: %s', 'emcp-tools' ),
					$attachment_id->get_error_message()
				)
			);
		}

		$local_url = wp_get_attachment_url( $attachment_id );

		return array(
			'attachment_id' => $attachment_id,
			'url'           => $local_url ? $local_url : '',
		);
	}

	/**
	 * Validates that a file contains SVG content.
	 *
	 * @since 1.2.0
	 *
	 * @param string $file_path Path to the file to validate.
	 * @return true|\WP_Error True if valid, WP_Error if not.
	 */
	private function validate_svg_file( string $file_path ) {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$content = file_get_contents( $file_path );

		if ( empty( $content ) ) {
			return new \WP_Error(
				'empty_svg',
				__( 'The downloaded file is empty.', 'emcp-tools' )
			);
		}

		if ( stripos( $content, '<svg' ) === false ) {
			return new \WP_Error(
				'not_svg',
				__( 'The downloaded file does not contain SVG markup.', 'emcp-tools' )
			);
		}

		// Check for potentially dangerous content.
		if ( preg_match( '/<script/i', $content ) ) {
			return new \WP_Error(
				'svg_has_script',
				__( 'SVG contains script elements and was rejected for security.', 'emcp-tools' )
			);
		}

		// Sanitize with Elementor's SVG sanitizer ( sanitize_file() rewrites the
		// file in place and also handles gzip-encoded SVGs ). Fail closed when
		// no sanitizer is available rather than storing minimally-checked markup.
		if ( ! class_exists( '\Elementor\Core\Utils\Svg\Svg_Sanitizer' ) ) {
			return new \WP_Error(
				'no_svg_sanitizer',
				__( 'No SVG sanitizer is available; the SVG upload was rejected for security.', 'emcp-tools' )
			);
		}

		$sanitizer = new \Elementor\Core\Utils\Svg\Svg_Sanitizer();
		if ( ! $sanitizer->sanitize_file( $file_path ) ) {
			return new \WP_Error(
				'svg_sanitization_failed',
				__( 'SVG failed security sanitization and was rejected.', 'emcp-tools' )
			);
		}

		return true;
	}

	/**
	 * Sanitizes raw SVG content.
	 *
	 * Uses Elementor's SVG sanitizer if available, otherwise performs
	 * basic security checks.
	 *
	 * @since 1.2.0
	 *
	 * @param string $content Raw SVG markup.
	 * @return string|\WP_Error Sanitized SVG content or WP_Error.
	 */
	private function sanitize_svg_content( string $content ) {
		// Strip PHP tags.
		$content = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $content );

		// Remove script tags.
		if ( preg_match( '/<script/i', $content ) ) {
			return new \WP_Error(
				'svg_has_script',
				__( 'SVG contains script elements and was rejected for security.', 'emcp-tools' )
			);
		}

		// Remove event handlers (on*="..."). The /s (DOTALL) flag is required so
		// the quoted value also matches across line boundaries — without it a
		// handler whose value contains a newline (e.g. onclick="alert(1)\n")
		// slips through the regex (F-008).
		$content = preg_replace( '/\s+on\w+\s*=\s*(["\']).*?\1/is', '', $content );

		// Remove javascript: URLs.
		$content = preg_replace( '/javascript\s*:/i', '', $content );

		// Sanitize with Elementor's SVG sanitizer. Fail closed when none is
		// available rather than returning minimally-checked markup.
		if ( ! class_exists( '\Elementor\Core\Utils\Svg\Svg_Sanitizer' ) ) {
			return new \WP_Error(
				'no_svg_sanitizer',
				__( 'No SVG sanitizer is available; the SVG was rejected for security.', 'emcp-tools' )
			);
		}

		$sanitizer = new \Elementor\Core\Utils\Svg\Svg_Sanitizer();
		$sanitized = $sanitizer->sanitize( $content );
		if ( false === $sanitized || '' === $sanitized ) {
			return new \WP_Error(
				'svg_sanitization_failed',
				__( 'SVG failed security sanitization. Ensure it contains valid SVG markup.', 'emcp-tools' )
			);
		}

		return $sanitized;
	}
}
