
­­­­­­­­­­­­­­­­­­
<!DOCTYPE html>
<html>
<?php

namespace Code_Snippets\REST_API\Import;

use Code_Snippets\Model\Snippet;
use Code_Snippets\REST_API\REST_Controller;
use DOMDocument;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use function Code_Snippets\code_snippets;
use function Code_Snippets\get_snippets;
use function Code_Snippets\save_snippet;

/**
 * Manages the import of code snippets from uploaded files via REST API.
 */
class File_Import_REST_Controller extends REST_Controller {

	/**
	 * Current API version.
	 */
	public const VERSION = 1;

	/**
	 * The base of this controller's route.
	 */
	public const BASE_ROUTE = 'import/file-upload';

	/**
	 * Registers REST API routes for file import.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			self::BASE_ROUTE . '/parse',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'parse_uploaded_files' ],
				'permission_callback' => [ $this, 'permission_callback' ],
			]
		);

		register_rest_route(
			$this->namespace,
			self::BASE_ROUTE . '/import',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'import_selected_snippets' ],
				'permission_callback' => [ $this, 'permission_callback' ],
				'args'                => [
					'snippets'         => [
						'description' => __( 'Snippet data to import', 'code-snippets' ),
						'type'        => 'array',
						'required'    => true,
					],
					'duplicate_action' => [
						'description' => __( 'Action to take when duplicate snippets are found', 'code-snippets' ),
						'type'        => 'string',
						'enum'        => [ 'ignore', 'replace', 'skip' ],
						'default'     => 'ignore',
					],
					'network'          => [
						'description' => __( 'Whether to import to network table', 'code-snippets' ),
						'type'        => 'boolean',
						'default'     => false,
					],
				],
			]
		);
	}

	/**
	 * Determine whether the request has permission to import snippets.
	 *
	 * @param WP_REST_Request $request Incoming HTTP request.
	 *
	 * @return bool
	 */
	public function permission_callback( WP_REST_Request $request ): bool {
		return code_snippets()->current_user_can();
	}

	/**
	 * Parses uploaded files and extracts code snippets.
	 *
	 * @param WP_REST_Request $request The REST request.
	 *
	 * @return WP_Error|WP_REST_Response Parsed snippets or error.
	 */
	public function parse_uploaded_files( WP_REST_Request $request ) {
		$nonce = $request->get_header( 'X-WP-Nonce' );

		if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
			return new WP_Error(
				'rest_cookie_invalid_nonce',
				__( 'Cookie check failed', 'code-snippets' ),
				[ 'status' => 403 ]
			);
		}

		$file_params = $request->get_file_params();

		if ( empty( $file_params ) || empty( $file_params['files'] ) ) {
			return new WP_Error(
				'no_files',
				__( 'No files were uploaded.', 'code-snippets' ),
				[ 'status' => 400 ]
			);
		}

		$files = $file_params['files'];

		if ( ! isset( $files['name'], $files['type'], $files['tmp_name'], $files['error'] ) ) {
			return new WP_Error(
				'invalid_file_data',
				__( 'Invalid file upload data.', 'code-snippets' ),
				[ 'status' => 400 ]
			);
		}

		$all_snippets = [];
		$errors = [];

		$file_count = is_array( $files['name'] ) ? count( $files['name'] ) : 1;

		for ( $i = 0; $i < $file_count; $i++ ) {
			$file_name = is_array( $files['name'] ) ? $files['name'][ $i ] : $files['name'];
			$file_type = is_array( $files['type'] ) ? $files['type'][ $i ] : $files['type'];
			$file_tmp = is_array( $files['tmp_name'] ) ? $files['tmp_name'][ $i ] : $files['tmp_name'];
			$file_error = is_array( $files['error'] ) ? $files['error'][ $i ] : $files['error'];

			if ( UPLOAD_ERR_OK !== $file_error ) {
				/* translators: %1$s: file name, %2$s: error message */
				$error_message = __( 'Upload error for file %1$s: %2$s', 'code-snippets' );
				$errors[] = sprintf( $error_message, $file_name, $this->get_upload_error_message( $file_error ) );
				continue;
			}

			$file_info = pathinfo( $file_name );
			$extension = strtolower( $file_info['extension'] ?? '' );
			$mime_type = sanitize_mime_type( $file_type );

			if ( ! $this->is_valid_file_type( $extension, $mime_type ) ) {
				/* translators: %s: file name */
				$error_message = __( 'Invalid file type for %s. Only JSON and XML files are allowed.', 'code-snippets' );
				$errors[] = sprintf( $error_message, $file_name );
				continue;
			}

			$snippets = $this->parse_file_content( $file_tmp, $extension, $mime_type, $file_name );

			if ( is_wp_error( $snippets ) ) {
				/* translators: %1$s: file name, %2$s: error message */
				$error_message = __( 'Error parsing %1$s: %2$s', 'code-snippets' );
				$errors[] = sprintf( $error_message, $file_name, $snippets->get_error_message() );
			} else {
				$all_snippets = array_merge( $all_snippets, $snippets );
			}
		}

		if ( empty( $all_snippets ) ) {
			return new WP_Error(
				'no_snippets_found',
				__( 'No valid snippets found in the uploaded files.', 'code-snippets' ),
				[
					'status' => 400,
					'errors' => $errors,
				],
			);
		}

		/* translators: %d: number of snippets */
		$message = _n(
			'Found %d snippet ready for import.',
			'Found %d snippets ready for import.',
			count( $all_snippets ),
			'code-snippets',
		);

		$response = [
			'snippets'    => $all_snippets,
			'total_count' => count( $all_snippets ),
			'message'     => sprintf( $message, count( $all_snippets ) ),
		];

		if ( ! empty( $errors ) ) {
			$response['warnings'] = $errors;
		}

		return rest_ensure_response( $response );
	}

	/**
	 * Imports selected snippets into the system.
	 *
	 * @param WP_REST_Request $request The REST request.
	 *
	 * @return WP_Error|WP_REST_Response Import result or error.
	 */
	public function import_selected_snippets( WP_REST_Request $request ) {
		$snippets_data = $request->get_param( 'snippets' );
		$duplicate_action = $request->get_param( 'duplicate_action' ) ?? 'ignore';
		$network = $request->get_param( 'network' ) ?? false;

		if ( empty( $snippets_data ) || ! is_array( $snippets_data ) ) {
			return new WP_Error(
				'no_snippets',
				__( 'No snippet data provided for import.', 'code-snippets' ),
				[ 'status' => 400 ]
			);
		}

		$snippets = [];
		foreach ( $snippets_data as $snippet_data ) {
			$snippet = new Snippet();
			$snippet->network = $network;

			$import_fields = [
				'name',
				'desc',
				'description',
				'code',
				'tags',
				'scope',
				'priority',
				'shared_network',
				'modified',
				'cloud_id',
			];

			foreach ( $import_fields as $field ) {
				if ( isset( $snippet_data[ $field ] ) ) {
					$snippet->set_field( $field, $snippet_data[ $field ] );
				}
			}

			$snippets[] = $snippet;
		}

		$imported = $this->save_snippets( $snippets, $duplicate_action, $network );

		/* translators: %d: number of snippets */
		$message = _n(
			'Successfully imported %d snippet.',
			'Successfully imported %d snippets.',
			count( $imported ),
			'code-snippets',
		);

		$response = [
			'imported'     => count( $imported ),
			'imported_ids' => $imported,
			'message'      => sprintf( $message, count( $imported ) ),
		];

		return rest_ensure_response( $response );
	}

	/**
	 * Parses the content of a file based on its type.
	 *
	 * @param string $file_path The path to the file.
	 * @param string $extension The file extension.
	 * @param string $mime_type The MIME type of the file.
	 * @param string $file_name The original file name.
	 *
	 * @return array|WP_Error Parsed snippets or error.
	 */
	private function parse_file_content( string $file_path, string $extension, string $mime_type, string $file_name ) {
		if ( ! file_exists( $file_path ) || ! is_file( $file_path ) ) {
			return new WP_Error(
				'file_not_found',
				__( 'File not found or is not a valid file.', 'code-snippets' )
			);
		}

		if ( 'json' === $extension || 'application/json' === $mime_type ) {
			return $this->parse_json_file( $file_path, $file_name );
		} elseif ( 'xml' === $extension || in_array( $mime_type, [ 'text/xml', 'application/xml' ], true ) ) {
			return $this->parse_xml_file( $file_path, $file_name );
		}

		return new WP_Error(
			'unsupported_file_type',
			__( 'Unsupported file type.', 'code-snippets' )
		);
	}

	/**
	 * Parses a JSON file to extract snippets.
	 *
	 * @param string $file_path The path to the JSON file.
	 * @param string $file_name The original file name.
	 *
	 * @return array|WP_Error Parsed snippets or error.
	 */
	private function parse_json_file( string $file_path, string $file_name ) {

		// TODO: replace this with use of WordPress Filesystem API.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$raw_data = file_get_contents( $file_path );

		$data = json_decode( $raw_data, true );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			/* translators: %1$s: file name, %2$s: error message */
			$message = sprintf( __( 'Invalid JSON in file %1$s: %2$s', 'code-snippets' ), $file_name, json_last_error_msg() );
			return new WP_Error( 'invalid_json', $message );
		}

		if ( ! isset( $data['snippets'] ) || ! is_array( $data['snippets'] ) ) {
			/* translators: %s: file name */
			$message = __( 'No snippets found in file %s', 'code-snippets' );
			return new WP_Error( 'no_snippets_in_file', sprintf( $message, $file_name ) );
		}

		$results = [];

		foreach ( $data['snippets'] as $snippet_data ) {
			if ( ! is_array( $snippet_data ) ) {
				continue;
			}

			$snippet_data['source_file'] = $file_name;
			$snippet_data['table_data'] = [
				'id'          => $snippet_data['id'] ?? uniqid(),
				'title'       => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
				'scope'       => $snippet_data['scope'] ?? 'global',
				'tags'        => is_array( $snippet_data['tags'] ?? null )
					? implode( ', ', $snippet_data['tags'] )
					: '',
				'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
				'type'        => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' ),
			];

			$results[] = $snippet_data;
		}

		return $results;
	}

	/**
	 * Parse snippets from XML file for importing.
	 *
	 * @param string $file_path Path to file.
	 * @param string $file_name Name of file.
	 *
	 * @return array|WP_Error
	 *
	 * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
	 */
	private function parse_xml_file( string $file_path, string $file_name ) {
		$dom = new DOMDocument( '1.0', get_bloginfo( 'charset' ) );

		if ( ! $dom->load( $file_path ) ) {
			/* translators: %s: file name */
			$message = __( 'Invalid XML in file %s', 'code-snippets' );
			return new WP_Error( 'invalid_xml', sprintf( $message, $file_name ) );
		}

		$snippets_xml = $dom->getElementsByTagName( 'snippet' );
		$fields = [ 'name', 'description', 'desc', 'code', 'tags', 'scope' ];

		$snippets = [];
		$index = 0;

		foreach ( $snippets_xml as $snippet_xml ) {
			$snippet_data = [];

			foreach ( $fields as $field_name ) {
				$field = $snippet_xml->getElementsByTagName( $field_name )->item( 0 );

				if ( isset( $field->nodeValue ) ) {
					$snippet_data[ $field_name ] = $field->nodeValue;
				}
			}

			$scope = $snippet_xml->getAttribute( 'scope' );
			if ( ! empty( $scope ) ) {
				$snippet_data['scope'] = $scope;
			}

			$snippet_data['source_file'] = $file_name;

			$snippet_data['table_data'] = [
				'id'          => ++$index,
				'title'       => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
				'scope'       => $snippet_data['scope'] ?? 'global',
				'tags'        => $snippet_data['tags'] ?? '',
				'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
				'type'        => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' ),
			];

			$snippets[] = $snippet_data;
		}

		return $snippets;
	}

	/**
	 * Saves snippets to the database, handling duplicates based on the specified action.
	 *
	 * @param array  $snippets         Array of Snippet objects to save.
	 * @param string $duplicate_action Action to take on duplicates: 'ignore', 'replace', or 'skip'.
	 * @param bool   $network          Whether to save to the network table.
	 *
	 * @return array Array of imported snippet IDs.
	 */
	private function save_snippets( array $snippets, string $duplicate_action, bool $network ): array {
		$existing_snippets = [];

		if ( 'replace' === $duplicate_action || 'skip' === $duplicate_action ) {
			$all_snippets = get_snippets( [], $network );

			foreach ( $all_snippets as $snippet ) {
				if ( $snippet->name ) {
					$existing_snippets[ $snippet->name ] = $snippet->id;
				}
			}
		}

		$imported = [];

		foreach ( $snippets as $snippet ) {
			if ( 'ignore' !== $duplicate_action && isset( $existing_snippets[ $snippet->name ] ) ) {
				if ( 'replace' === $duplicate_action ) {
					$snippet->id = $existing_snippets[ $snippet->name ];
				} elseif ( 'skip' === $duplicate_action ) {
					continue;
				}
			}

			$saved_snippet = save_snippet( $snippet );

			$snippet_id = $saved_snippet->id;

			if ( $snippet_id ) {
				$imported[] = $snippet_id;
			}
		}

		return $imported;
	}

	/**
	 * Determines if the file type is valid for import.
	 *
	 * @param string $extension File extension, without leading dot.
	 * @param string $mime_type MIME type of the file.
	 *
	 * @return bool
	 */
	private function is_valid_file_type( string $extension, string $mime_type ): bool {
		$valid_extensions = [ 'json', 'xml' ];
		$valid_mime_types = [ 'application/json', 'text/xml', 'application/xml' ];

		return in_array( $extension, $valid_extensions, true ) ||
		       in_array( $mime_type, $valid_mime_types, true );
	}


	/**
	 * Convert upload error code into a human-readable message.
	 *
	 * @param int $error_code Error code.
	 *
	 * @return string Translated error message.
	 */
	private function get_upload_error_message( int $error_code ): string {
		$error_messages = [
			UPLOAD_ERR_INI_SIZE   => __( 'File exceeds the upload_max_filesize directive.', 'code-snippets' ),
			UPLOAD_ERR_FORM_SIZE  => __( 'File exceeds the MAX_FILE_SIZE directive.', 'code-snippets' ),
			UPLOAD_ERR_PARTIAL    => __( 'File was only partially uploaded.', 'code-snippets' ),
			UPLOAD_ERR_NO_FILE    => __( 'No file was uploaded.', 'code-snippets' ),
			UPLOAD_ERR_NO_TMP_DIR => __( 'Missing a temporary folder.', 'code-snippets' ),
			UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'code-snippets' ),
			UPLOAD_ERR_EXTENSION  => __( 'A PHP extension stopped the file upload.', 'code-snippets' ),
		];

		return $error_messages[ $error_code ] ?? __( 'Unknown upload error.', 'code-snippets' );
	}
}
