<?php
/**
 * File Path: includes/import/class-mbs-tutorial-importer.php
 *
 * MBS Tutorial Importer
 *
 * Downloads the tutorial catalog ZIP from the MBS API server, extracts it
 * via MBS_ZIP_Extractor::extract_catalog(), then imports the full 7-tab
 * catalog via MBS_Catalog_XLSX_Importer::import_from_path().  Every post,
 * attachment, and term created during import is flagged so
 * MBS_Tutorial_Uninstaller can remove it cleanly.
 *
 * Flags used:
 *   _mbs_tutorial_book   postmeta on mbs_book posts
 *   _mbs_tutorial_asset  postmeta on attachment posts
 *   _mbs_tutorial_term   term_meta on all MBS taxonomy terms
 *
 * Supersedes the 1.0.0 hand-coded install class.  All per-book data is
 * maintained in the tutorial catalog XLSX on the API server rather than
 * hard-coded PHP arrays.  Author bio and website URL are written as a
 * fallback stub (no-op if the XLSX import has already populated them via
 * Tab 5 columns H/I).
 *
 * @package    MyBookShowroom
 * @subpackage Import
 * @since      1.3.2
 */

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

/**
 * Installs the tutorial book catalog via the ZIP import pipeline.
 *
 * @since 1.3.2
 */
class MBS_Tutorial_Importer { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Relative API path to the tutorial catalog ZIP.
	 *
	 * @var string
	 */
	const TUTORIAL_ZIP_PATH = '/tutorials/tutorial-catalog-2.0.0.zip';

	// -------------------------------------------------------------------------
	// Public API
	// -------------------------------------------------------------------------

	/**
	 * Download, extract, and import the tutorial catalog.
	 *
	 * Downloads tutorial-catalog-2.0.0.zip from MBS_API_BASE, extracts via
	 * MBS_ZIP_Extractor::extract_catalog(), imports via
	 * MBS_Catalog_XLSX_Importer::import_from_path(), then applies tutorial
	 * flags to every created post, attachment, and taxonomy term.
	 *
	 * @since  1.3.2
	 * @param  bool $force  When true, overwrite/replace books that match an
	 *                      existing catalog row (reinstall path). When false
	 *                      (default, plain install), duplicate-checking is left
	 *                      to the importer so re-running does not create copies.
	 * @return array {
	 *     @type int      $books_installed  Number of book posts created.
	 *     @type string[] $errors           Human-readable error strings.
	 * }
	 */
	public static function run( bool $force = false ): array {
		$stats = array( 'books_installed' => 0, 'errors' => array() );

		require_once ABSPATH . 'wp-admin/includes/media.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';

		// 1. Provision a unique temp directory.
		$tmp_dir = self::make_temp_dir();
		if ( null === $tmp_dir ) {
			$stats['errors'][] = __( 'Could not create temp directory for tutorial assets.', 'my-book-showroom' );
			return $stats;
		}

		// 2. Stream-download the ZIP into the temp directory.
		self::set_progress( 'downloading', __( 'Fetching tutorial file…', 'my-book-showroom' ), 5 );
		$zip_path = self::download_zip( $tmp_dir, $stats );
		if ( null === $zip_path ) {
			self::cleanup_dir( $tmp_dir );
			return $stats;
		}

		// 3. Extract catalog.xlsx + all media subfolders.
		$extract = MBS_ZIP_Extractor::extract_catalog( $zip_path, $tmp_dir );
		// phpcs:ignore WordPress.PHP.NoSilencedErrors -- temp cleanup; error not actionable
		wp_delete_file( $zip_path );

		if ( is_wp_error( $extract ) ) {
			$stats['errors'][] = $extract->get_error_message();
			self::cleanup_dir( $tmp_dir );
			return $stats;
		}

		// Capture the highest existing attachment ID before any sideloading begins.
		// Used in step 6b to scope video poster-frame flagging to this import only.
		global $wpdb;
		$max_att_id_before = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- DAO layer -- direct DB access is intentional per MBS architecture
			"SELECT COALESCE( MAX(ID), 0 ) FROM {$wpdb->posts} WHERE post_type = 'attachment'"
		);

		// 4. Import via the full XLSX pipeline.
		self::set_progress( 'extracting', __( 'Unpacking archive…', 'my-book-showroom' ), 20 );
		try {
			$total_books_hint = 9; // approximate; updated once XLSX is parsed
			$result = MBS_Catalog_XLSX_Importer::import_from_path(
				$extract['data_file'],
				array(
					'images_dir'       => $extract['media_root'],
					'force_overwrite'  => $force,
					'post_status'      => 'publish',
					'progress_callback' => static function ( int $book_num, int $total, int $post_id, string $title ) {
						$pct = (int) round( 30 + ( $book_num / max( 1, $total ) ) * 60 );
						/* translators: 1: book number, 2: total books, 3: post ID */
						$msg = sprintf( __( 'Book #%1$d of %2$d — post ID %3$d', 'my-book-showroom' ),
							$book_num, $total, $post_id );
						self::set_progress( 'book', $msg, $pct, array(
							'book_num'  => $book_num,
							'total'     => $total,
							'post_id'   => $post_id,
							'title'     => $title,
					) );
					},
				)
			);

			$stats['books_installed'] = $result['imported'];
			$stats['errors']          = array_merge( $stats['errors'], $result['errors'] );

			self::set_progress( 'flagging', __( 'Finishing up…', 'my-book-showroom' ), 93 );

			// 5. Flag every imported post as tutorial content.
			foreach ( $result['post_ids'] as $post_id ) {
				update_post_meta( (int) $post_id, '_mbs_tutorial_book', '1' );
			}

			// 6. Flag every sideloaded attachment as a tutorial asset.
			//    Also flag any child attachments (e.g., video poster frames created by
			//    wp_generate_attachment_metadata, which are not in attachment_ids).
			foreach ( $result['attachment_ids'] as $att_id ) {
				update_post_meta( (int) $att_id, '_mbs_tutorial_asset', '1' );
				$child_ids = get_posts( array(
					'post_type'      => 'attachment',
					'post_status'    => 'any',
					'posts_per_page' => -1,
					'fields'         => 'ids',
					'post_parent'    => (int) $att_id,
					'no_found_rows'  => true,
				) );
				foreach ( $child_ids as $child_id ) {
					update_post_meta( (int) $child_id, '_mbs_tutorial_asset', '1' );
				}
			}

			// 6b. Flag video poster frames that WordPress assigned post_parent = 0
			//     (not the video attachment's ID). The post_parent query in step 6
			//     misses these. Scoped to IDs created during this import only.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$poster_frame_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT p.ID
					   FROM {$wpdb->posts} p
					   INNER JOIN {$wpdb->postmeta} pm
					           ON pm.post_id = p.ID
					          AND pm.meta_key = %s
					  WHERE p.post_type = %s
					    AND p.ID > %d
					    AND pm.meta_value LIKE %s",
					'_wp_attached_file',
					'attachment',
					$max_att_id_before,
					'%-mp4-image%'
				)
			);
			foreach ( $poster_frame_ids as $pf_id ) {
				update_post_meta( (int) $pf_id, '_mbs_tutorial_asset', '1' );
			}

			// 7. Flag all MBS taxonomy terms attached to imported books.
			self::flag_tutorial_terms( $result['post_ids'] );

			// 8. Write author bio/website fallback (no-op when XLSX provided them).
			self::maybe_write_author_meta();

			if ( $result['imported'] > 0 ) {
				update_option( 'mbs_tutorial_imported', '1' );
			}
			self::set_progress( 'done', __( 'Tutorial successfully installed.', 'my-book-showroom' ), 100 );
		} finally {
			self::cleanup_dir( $tmp_dir );
		}

		return $stats;
	}

	// -------------------------------------------------------------------------
	// Private helpers
	// -------------------------------------------------------------------------

	/**
	 * Create a unique temporary directory for ZIP extraction.
	 *
	 * @since  1.3.2
	 * @return string|null Absolute directory path, or null on failure.
	 */
	private static function make_temp_dir(): ?string {
		$dir = rtrim( get_temp_dir(), '/\\' ) . '/mbs-tutorial-' . wp_generate_password( 8, false );
		return wp_mkdir_p( $dir ) ? $dir : null;
	}

	/**
	 * Stream the tutorial catalog ZIP from the API server to a local path.
	 *
	 * @since  1.3.2
	 * @param  string $tmp_dir  Absolute path to the temp directory.
	 * @param  array  &$stats   Stats array; errors are appended on failure.
	 * @return string|null Absolute path to the downloaded ZIP, or null on failure.
	 */
	private static function download_zip( string $tmp_dir, array &$stats ): ?string {
		$zip_url  = MBS_API_BASE . self::TUTORIAL_ZIP_PATH;
		$zip_path = $tmp_dir . '/catalog.zip';

		$response = wp_remote_get(
			$zip_url,
			array(
				'timeout'  => 120,
				'stream'   => true,
				'filename' => $zip_path,
			)
		);

		if ( is_wp_error( $response ) ) {
			$stats['errors'][] = sprintf(
				/* translators: %s: error message */
				__( 'Tutorial asset download failed: %s', 'my-book-showroom' ),
				$response->get_error_message()
			);
			return null;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( 200 !== $code ) {
			$stats['errors'][] = sprintf(
				/* translators: %d: HTTP status code */
				__( 'Tutorial asset download failed: HTTP %d', 'my-book-showroom' ),
				$code
			);
			return null;
		}

		return $zip_path;
	}

	/**
	 * Flag every MBS taxonomy term attached to any imported book post.
	 *
	 * Writes _mbs_tutorial_term=1 on each unique term ID so
	 * MBS_Tutorial_Uninstaller can identify and remove tutorial-specific terms.
	 *
	 * @since  1.3.2
	 * @param  int[] $post_ids Imported mbs_book post IDs.
	 * @return void
	 */
	private static function flag_tutorial_terms( array $post_ids ): void {
		$mbs_taxes = array(
			MBS_TAX_AUTHOR, MBS_TAX_CONTRIBUTOR, MBS_TAX_PUBLISHER,
			MBS_TAX_IMPRINT, MBS_TAX_SERIES, MBS_TAX_COLLECTION,
			MBS_TAX_UNIVERSE, MBS_TAX_GENRE, MBS_TAX_TAG,
			MBS_TAX_THEME, MBS_TAX_CONTENT_WARNING,
		);
		$seen = array();

		foreach ( $post_ids as $post_id ) {
			foreach ( $mbs_taxes as $tax ) {
				$term_ids = wp_get_object_terms( (int) $post_id, $tax, array( 'fields' => 'ids' ) );
				if ( is_wp_error( $term_ids ) ) { continue; }
				foreach ( (array) $term_ids as $tid ) {
					$tid = (int) $tid;
					if ( isset( $seen[ $tid ] ) ) { continue; }
					$seen[ $tid ] = true;
					update_term_meta( $tid, '_mbs_tutorial_term', '1' );
				}
			}
		}
	}

	/**
	 * Write bio and website URL for the three tutorial authors if not already set.
	 *
	 * Uses add_term_meta() with $unique = true so values already populated by
	 * the XLSX import pipeline (Tab 5 columns H/I) are never overwritten.  This
	 * method is a fallback for catalog versions that predate the Tab 5 schema.
	 *
	 * @since  1.3.2
	 * @return void
	 */
	private static function maybe_write_author_meta(): void {
		$authors = array(
			'Cody Render'      => array(
				'bio'     => '<p>Cody Render is the bestselling author of The Book Showroom Quest series, published by CodeForge Press. A former junior wizard of the Custom Field Order, Cody writes sweeping tales of metaboxes, format rows, and taxonomical survival. When not battling the Cursor of Doom, Cody can be found sanitizing inputs and arguing with rogue repeater fields at unreasonable hours.</p>',
				'website' => 'https://codyrender.example.com/',
			),
			'Ima Write'        => array(
				'bio'     => '<p>Ima Write is the author of the Cosmos Codex series and co-author of <em>The Taxonomy Tamer</em>, published by CodeForge Press. A literary disruptor who refuses to commit to a single genre, Ima writes across the full spectrum of code-magic fiction. Her debut novel reached the Plugin Wars bestseller list and stayed there for eleven weeks.</p>',
				'website' => 'https://imawrite.example.com/',
			),
			'Devra Codewright' => array(
				'bio'     => '<p>Devra Codewright is the creator of the Cosmos Quest series, published by Cosmos Digital Press. Devra\'s work explores the outer reaches of the Plugin Cosmos -- where API routes run dark and taxonomies grow wild. A former back-end architect turned novelist, Devra brings technical precision and genuine dread to every chapter.</p>',
				'website' => 'https://devracodewright.example.com/',
			),
		);

		foreach ( $authors as $name => $meta ) {
			$term = get_term_by( 'name', $name, MBS_TAX_AUTHOR );
			if ( ! $term instanceof WP_Term ) { continue; }
			add_term_meta( $term->term_id, 'mbs_author_bio',         wp_kses_post( $meta['bio'] ),    true );
			add_term_meta( $term->term_id, 'mbs_author_website_url', esc_url_raw( $meta['website'] ), true );
		}
	}

	/**
	 * Write a progress snapshot to a transient for the polling endpoint.
	 *
	 * @since  2.0.0
	 * @param  string $step    Machine-readable step identifier.
	 * @param  string $message Human-readable status message.
	 * @param  int    $pct     Completion percentage (0-100).
	 * @param  array  $extra   Optional extra data for the JS layer.
	 * @return void
	 */
	private static function set_progress( string $step, string $message, int $pct, array $extra = array() ): void {
		set_transient( 'mbs_tutorial_progress', array_merge( array(
			'step'    => $step,
			'message' => $message,
			'pct'     => max( 0, min( 100, $pct ) ),
		), $extra ), 300 );
	}

	/**
	 * Recursively delete a temporary directory and all its contents.
	 *
	 * @since  1.3.2
	 * @param  string $dir Absolute path to the directory to remove.
	 * @return void
	 */
	private static function cleanup_dir( string $dir ): void {
		if ( ! is_dir( $dir ) ) { return; }
		$items = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::CHILD_FIRST
		);
		foreach ( $items as $item ) {
			if ( $item->isDir() ) {
				// phpcs:ignore WordPress.PHP.NoSilencedErrors
				@rmdir( $item->getRealPath() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)
			} else {
				// phpcs:ignore WordPress.PHP.NoSilencedErrors
				wp_delete_file( $item->getRealPath() );
			}
		}
		// phpcs:ignore WordPress.PHP.NoSilencedErrors
		@rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)
	}
}
