<?php

/**
 * Import snippet
 *
 * @package Woody_Code_Snippets
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class WINP_Import_Snippet {

	/**
	 * Process import files and return results
	 *
	 * @param array<array<string, mixed>>|array<string, mixed> $files Array of files from $_FILES or REST API.
	 * @param string                                           $dup_action Duplicate action: 'ignore', 'replace', or 'skip'.
	 *
	 * @return array<string, mixed> Array with 'count', 'error', and 'errors' keys
	 */
	public function process_import_files( $files, $dup_action = 'ignore' ) {
		$count  = 0;
		$error  = false;
		$errors = [];

		// Sanitize duplicate action.
		$dup_action = sanitize_text_field( $dup_action );
		if ( ! in_array( $dup_action, [ 'ignore', 'replace', 'skip' ], true ) ) {
			$dup_action = 'ignore';
		}

		// Normalize file array structure.
		$normalized_files = [];
		if ( isset( $files['tmp_name'] ) ) {
			// Handle both single file and multiple files format.
			$file_count = count( $files['tmp_name'] );
			for ( $i = 0; $i < $file_count; $i++ ) {
				$normalized_files[] = [
					'name'     => isset( $files['name'][ $i ] ) ? sanitize_file_name( $files['name'][ $i ] ) : '',
					'type'     => isset( $files['type'][ $i ] ) ? sanitize_text_field( $files['type'][ $i ] ) : '',
					'tmp_name' => isset( $files['tmp_name'][ $i ] ) ? $files['tmp_name'][ $i ] : '',
					'error'    => isset( $files['error'][ $i ] ) ? (int) $files['error'][ $i ] : UPLOAD_ERR_NO_FILE,
					'size'     => isset( $files['size'][ $i ] ) ? (int) $files['size'][ $i ] : 0,
				];
			}
		} else {
			// Already in correct format (from REST API) - still need to sanitize.
			foreach ( $files as $file ) {
				$normalized_files[] = [
					'name'     => isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : '',
					'type'     => isset( $file['type'] ) ? sanitize_text_field( $file['type'] ) : '',
					'tmp_name' => isset( $file['tmp_name'] ) ? $file['tmp_name'] : '',
					'error'    => isset( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE,
					'size'     => isset( $file['size'] ) ? (int) $file['size'] : 0,
				];
			}
		}

		foreach ( $normalized_files as $file ) {
			// Validate tmp_name path.
			if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) {
				$error = true;
				// translators: %s is the file name.
				$errors[] = sprintf( __( 'Invalid or missing temporary file for: %s', 'insert-php' ), $file['name'] );
				continue;
			}

			$ext         = pathinfo( $file['name'], PATHINFO_EXTENSION );
			$ext         = strtolower( sanitize_text_field( $ext ) );
			$mime_type   = $file['type'];
			$import_file = $file['tmp_name'];

			if ( 'json' === $ext || 'application/json' === $mime_type ) {
				$result = $this->import_snippet( $import_file, $dup_action );
			} elseif ( 'zip' === $ext || 'application/zip' === $mime_type ) {
				$result = $this->import_zip_snippets( $import_file, $dup_action );
			} else {
				$result = apply_filters( 'wbcr/inp/import/snippet', false, $ext, $mime_type, $import_file, $dup_action );
			}

			if ( false === $result || - 1 === $result ) {
				$error = true;
				// translators: %s is the file name.
				$errors[] = sprintf( __( 'Failed to import file: %s', 'insert-php' ), $file['name'] );
			} else {
				$count += count( $result );
			}
		}

		return [
			'count'  => $count,
			'error'  => $error,
			'errors' => $errors,
		];
	}

	/**
	 * Import snippets
	 *
	 * @param string $file File path.
	 * @param string $dup_action Duplicate action: 'ignore', 'replace', or 'skip'.
	 *
	 * @return int|bool|array<int>
	 */
	public function import_snippet( $file, $dup_action ) {
		if ( ! file_exists( $file ) || ! is_file( $file ) ) {
			return false;
		}

		$raw_data = file_get_contents( $file );
		$data     = json_decode( $raw_data, true );
		$snippets = isset( $data['snippets'] ) ? $data['snippets'] : [];

		$imported = $this->save_imported_snippets( $snippets, $dup_action );

		return $imported;
	}

	/**
	 * Import snippets from ZIP archive
	 *
	 * @param string $file File path.
	 * @param string $dup_action Duplicate action: 'ignore', 'replace', or 'skip'.
	 *
	 * @return int|bool|array<int>
	 */
	public function import_zip_snippets( $file, $dup_action ) {
		if ( ! class_exists( 'ZipArchive' ) ) {
			return false;
		}

		$zip = new ZipArchive();

		if ( true !== $zip->open( $file ) ) {
			return false;
		}

		$result     = [];
		$upload_dir = wp_get_upload_dir();
		// Use unique directory name to prevent race conditions.
		$unzip_path = $upload_dir['path'] . '/winp_' . uniqid();

		// Create extraction directory.
		if ( ! wp_mkdir_p( $unzip_path ) ) {
			$zip->close();
			return false;
		}

		// Validate and extract only safe files.
		for ( $i = 0; $i < $zip->numFiles; $i++ ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			$filename = $zip->getNameIndex( $i );
			
			// Skip if filename cannot be retrieved.
			if ( false === $filename ) {
				continue;
			}
			
			// Security: Skip files with directory traversal attempts.
			if ( false !== strpos( $filename, '..' ) || false !== strpos( $filename, '/' ) || false !== strpos( $filename, '\\' ) ) {
				continue;
			}

			// Only extract files (not directories).
			$file_info = $zip->statIndex( $i );
			if ( ! empty( $file_info ) && 0 === $file_info['size'] ) {
				continue;
			}

			$zip->extractTo( $unzip_path, $filename );
		}

		$zip->close();

		// Process extracted files using modern iterator.
		try {
			$iterator = new DirectoryIterator( $unzip_path );
			foreach ( $iterator as $file_info ) {
				if ( $file_info->isFile() && $file_info->getSize() > 0 ) {
					$filepath = $file_info->getPathname();
					$_result  = $this->import_snippet( $filepath, $dup_action );
					if ( is_array( $_result ) ) {
						$result = array_merge( $result, $_result );
					}
				}
			}
		} catch ( Exception $e ) {
			$this->cleanup_directory( $unzip_path );
			return false;
		}

		// Cleanup: Delete all files and directory.
		$this->cleanup_directory( $unzip_path );

		return $result;
	}

	/**
	 * Cleanup directory and its contents
	 *
	 * @param string $dir_path Directory path.
	 * 
	 * @return void
	 */
	private function cleanup_directory( $dir_path ) {
		if ( ! is_dir( $dir_path ) ) {
			return;
		}

		try {
			$iterator = new DirectoryIterator( $dir_path );
			foreach ( $iterator as $file_info ) {
				if ( $file_info->isFile() ) {
					wp_delete_file( $file_info->getPathname() );
				}
			}
		} catch ( Exception $e ) {
			return;
		}

		$wp_filesystem = WINP_Helper::get_wp_filesystem();

		if ( false !== $wp_filesystem ) {
			$wp_filesystem->rmdir( $dir_path, true );
		}
	}

	/**
	 * Update taxonomy tags
	 *
	 * @param int           $snippet_id Snippet ID.
	 * @param array<string> $tags Tags slugs.
	 * 
	 * @return void
	 */
	private function update_taxonomy_tags( $snippet_id, $tags ) {
		if ( ! empty( $tags ) ) {
			foreach ( $tags as $tag_slug ) {
				$term = get_term_by( 'slug', $tag_slug, WINP_SNIPPETS_TAXONOMY );
				if ( $term ) {
					wp_set_post_terms( $snippet_id, [ $term->term_id ], WINP_SNIPPETS_TAXONOMY, true );
				}
			}
		}
	}

	/**
	 * Update post meta
	 *
	 * @param int    $post_id Post ID.
	 * @param string $meta_name Meta name.
	 * @param mixed  $meta_value Meta value.
	 * 
	 * @return void
	 */
	private function update_meta( $post_id, $meta_name, $meta_value ) {
		update_post_meta( $post_id, 'wbcr_inp_' . $meta_name, $meta_value );
	}

	/**
	 * Save snippet
	 *
	 * @param array<string, mixed> $snippet Snippet data.
	 *
	 * @return int
	 */
	private function save_snippet( $snippet ) {
		$content = $snippet['content'];

		if ( WINP_SNIPPET_TYPE_TEXT != $snippet['type'] && WINP_SNIPPET_TYPE_AD != $snippet['type'] ) {
			$content = empty( $content ) && isset( $snippet['code'] ) && ! empty( $snippet['code'] ) ? $snippet['code'] : $content;
		}

		$data = [
			'post_title'   => $snippet['title'],
			'post_content' => $content,
			'post_status'  => 'publish',
			'post_type'    => WINP_SNIPPETS_POST_TYPE,
		];

		if ( isset( $snippet['id'] ) && 0 != $snippet['id'] ) {
			$data['ID'] = $snippet['id'];
		}

		$snippet['id'] = wp_insert_post( $data );

		$this->update_meta( $snippet['id'], 'snippet_location', $snippet['location'] );
		$this->update_meta( $snippet['id'], 'snippet_type', $snippet['type'] );
		$this->update_meta( $snippet['id'], 'snippet_filters', $snippet['filters'] );
		$this->update_meta( $snippet['id'], 'changed_filters', $snippet['changed_filters'] );
		$this->update_meta( $snippet['id'], 'snippet_scope', $snippet['scope'] );
		$this->update_meta( $snippet['id'], 'snippet_description', $snippet['description'] );
		$this->update_meta( $snippet['id'], 'snippet_tags', $snippet['attributes'] );
		$this->update_meta( $snippet['id'], 'snippet_activate', 0 );
		$this->update_meta( $snippet['id'], 'snippet_priority', $snippet['priority'] );
		$this->update_meta( $snippet['id'], 'snippet_custom_name', $snippet['custom_name'] );

		$this->update_taxonomy_tags( $snippet['id'], $snippet['tags'] );

		return $snippet['id'];
	}

	/**
	 * Save imported snippets
	 *
	 * @param array<array<string, mixed>> $snippets Snippets data.
	 * @param string                      $dup_action Duplicate action: 'ignore', 'replace', or 'skip'.
	 *
	 * @return array<int> Imported snippet IDs.
	 */
	private function save_imported_snippets( $snippets, $dup_action ) {
		$existing_snippets = [];
		
		if ( 'replace' === $dup_action || 'skip' === $dup_action ) {
			$all_snippets = get_posts(
				[ 
					'post_type'      => WINP_SNIPPETS_POST_TYPE,
					'posts_per_page' => -1,
					'post_status'    => 'any',
				] 
			);

			foreach ( $all_snippets as $snippet ) {
				// Store by both post_name (slug) and post_title for matching.
				$existing_snippets[ $snippet->post_name ]  = $snippet->ID;
				$existing_snippets[ $snippet->post_title ] = $snippet->ID;
			}
		}

		$imported = [];

		foreach ( $snippets as $snippet ) {
			$is_duplicate = false;
			$duplicate_id = null;

			if ( 'ignore' !== $dup_action ) {
				if ( isset( $snippet['name'] ) && isset( $existing_snippets[ $snippet['name'] ] ) ) {
					$is_duplicate = true;
					$duplicate_id = $existing_snippets[ $snippet['name'] ];
				} elseif ( isset( $snippet['title'] ) && isset( $existing_snippets[ $snippet['title'] ] ) ) {
					$is_duplicate = true;
					$duplicate_id = $existing_snippets[ $snippet['title'] ];
				}

				if ( $is_duplicate ) {
					if ( 'replace' === $dup_action ) {
						$snippet['id'] = $duplicate_id;
					} elseif ( 'skip' === $dup_action ) {
						continue;
					}
				}
			}

				$snippet_id = $this->save_snippet( $snippet );
			if ( $snippet_id ) {
				$imported[] = $snippet_id;
			}
		}

		return $imported;
	}
}
