<?php
/**
 * File Path: includes/import/class-mbs-import-orchestrator.php
 *
 * MBS Import Orchestrator
 *
 * Coordinates a full archive import end-to-end:
 *
 *   1. Validate + parse archive via MBS_Catalog_Archive_Reader.
 *   2. Restore taxonomy terms via MBS_Import_Taxonomy_Restorer.
 *   4. Hydrate each book via MBS_Book_Hydrator.
 *   5. Sideload each book's media via MBS_Import_Media_Sideloader.
 *   6. Clean up the extracted temp directory.
 *
 * Single responsibility: call the right class in the right order and aggregate
 * results. Contains no business logic. Per IEF-A-012: any concern that grows
 * beyond ~50 lines is extracted to a dedicated helper class.
 *
 * Progress model (Sprint Plan §Phase 6 Q7):
 *   The AJAX layer (MBS_Tab_Child_Archive_Import) calls hydrate_book() once per
 *   book, passing the parsed archive context stored in a transient. Each call
 *   returns a progress snapshot. The JS caller fires the next request after each
 *   response, incrementing the progress bar. Media sideloading is a separate
 *   final pass triggered after all books are hydrated.
 *
 * @package    My_Book_Showroom
 * @since      1.0.0
 */

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

class MBS_Import_Orchestrator { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Transient prefix used to store the parsed archive between AJAX calls.
	 * TTL: 3600 seconds (1 hour) — long enough for large catalogs.
	 */
	const TRANSIENT_PREFIX = 'mbs_archive_import_';
	const TRANSIENT_TTL    = 3600;

	// -------------------------------------------------------------------------
	// Phase A: Parse + Prepare (called once on upload)
	// -------------------------------------------------------------------------

	/**
	 * Validate and parse a ZIP archive, store the result in a transient,
	 * and return a session token + book count so the JS can start polling.
	 *
	 * @param string $zip_path Absolute path to the uploaded ZIP file.
	 * @return array|WP_Error {
	 *   @type string $token        Session token (key into the transient).
	 *   @type int    $total        Total number of books in the archive.
	 *   @type array  $taxonomy     Summary of taxonomy restore result.
	 * }
	 */
	public static function prepare( string $zip_path ): array|\WP_Error {
		// Parse archive (validates + extracts).
		$parsed = MBS_Catalog_Archive_Reader::read( $zip_path );
		if ( is_wp_error( $parsed ) ) {
			return $parsed;
		}

		// Restore taxonomy terms before any books are hydrated so that
		// wp_set_object_terms() in MBS_Book_Hydrator finds terms ready.
		$tax_result = MBS_Import_Taxonomy_Restorer::restore( $parsed['taxonomy'] ?? array() );

		// Store books + extract_dir in a transient keyed by a session token.
		$token = wp_generate_uuid4();
		set_transient(
			self::TRANSIENT_PREFIX . $token,
			array(
				'books'       => $parsed['books'],
				'extract_dir' => $parsed['extract_dir'],
				'manifest'    => $parsed['manifest'],
			),
			self::TRANSIENT_TTL
		);

		return array(
			'token'    => $token,
			'total'    => count( $parsed['books'] ),
			'taxonomy' => $tax_result,
		);
	}

	// -------------------------------------------------------------------------
	// Phase B: Hydrate one book (called once per book by AJAX polling loop)
	// -------------------------------------------------------------------------

	/**
	 * Hydrate the book at position $index in the stored archive session.
	 *
	 * @param string $token  Session token from prepare().
	 * @param int    $index  Zero-based book index.
	 * @param array  $options Options passed to MBS_Book_Hydrator::hydrate().
	 * @return array|WP_Error {
	 *   @type int    $post_id  Post ID on success (0 on duplicate/skip).
	 *   @type int    $index    The index that was processed.
	 *   @type int    $total    Total books in session.
	 *   @type bool   $done     True when $index === $total - 1.
	 *   @type array  $errors   Non-fatal error strings from hydration.
	 * }
	 */
	public static function hydrate_book( string $token, int $index, array $options = array() ): array|\WP_Error {
		$session = get_transient( self::TRANSIENT_PREFIX . $token );
		if ( ! $session ) {
			return new \WP_Error( 'mbs_session_expired', 'Import session expired or not found. Please re-upload the archive.' );
		}

		$books = $session['books'];
		$total = count( $books );

		if ( $index < 0 || $index >= $total ) {
			return new \WP_Error( 'mbs_bad_index', sprintf( 'Book index %d is out of range (0-%d).', $index, $total - 1 ) );
		}

		$book_object = $books[ $index ];
		$errors      = array();

		$result = MBS_Book_Hydrator::hydrate( $book_object, $options );

		if ( is_wp_error( $result ) ) {
			// Both expected skips (duplicate) and unexpected failures set post_id=0
			// and add a warning message. The 'duplicate' code is an expected skip;
			// other codes are unexpected failures. Both are non-fatal: the loop continues.
			$post_id  = 0;
			$errors[] = $result->get_error_message();
		} else {
			$post_id = $result;

			// Book Club (no PRO gate — data always imported; license gates front-end display).
			self::import_book_club( $post_id, $book_object );

			// ARC (no PRO gate — data always imported; license gates front-end display).
			self::import_arc( $post_id, $book_object );

			// Collect non-fatal hydration warnings stored by the hydrator.
			$warnings = get_post_meta( $post_id, '_mbs_import_warnings', true );
			if ( is_array( $warnings ) ) {
				$errors = array_merge( $errors, $warnings );
			}
		}

		// Store post_id back into session for the media pass.
		$session['post_ids'][ $index ] = $post_id;
		set_transient( self::TRANSIENT_PREFIX . $token, $session, self::TRANSIENT_TTL );

		return array(
			'post_id' => $post_id,
			'index'   => $index,
			'total'   => $total,
			'done'    => ( $index >= $total - 1 ),
			'title'   => sanitize_text_field( $book_object['post_title'] ?? '' ),
			'errors'  => $errors,
		);
	}

	// -------------------------------------------------------------------------
	// Phase C: Sideload media for one book (separate pass after all hydrations)
	// -------------------------------------------------------------------------

	/**
	 * Sideload all media for the book at position $index.
	 *
	 * Reads image paths from the book object, resolves them against the
	 * extracted temp directory, and calls MBS_Import_Media_Sideloader::sideload().
	 *
	 * @param string $token  Session token from prepare().
	 * @param int    $index  Zero-based book index.
	 * @return array|WP_Error {
	 *   @type int   $index    Index processed.
	 *   @type int   $total    Total books.
	 *   @type bool  $done     True when last book media processed.
	 *   @type array $errors   Sideload error strings (non-fatal; book stays).
	 * }
	 */
	public static function sideload_media( string $token, int $index ): array|\WP_Error {
		$session = get_transient( self::TRANSIENT_PREFIX . $token );
		if ( ! $session ) {
			return new \WP_Error( 'mbs_session_expired', 'Import session expired or not found.' );
		}

		$books       = $session['books'];
		$total       = count( $books );
		$extract_dir = $session['extract_dir'] ?? '';
		$post_ids    = $session['post_ids'] ?? array();
		$errors      = array();

		if ( $index < 0 || $index >= $total ) {
			return new \WP_Error( 'mbs_bad_index', sprintf( 'Book index %d out of range.', $index ) );
		}

		$post_id     = (int) ( $post_ids[ $index ] ?? 0 );
		$book_object = $books[ $index ];

		if ( $post_id > 0 && ! empty( $extract_dir ) ) {
			$errors = self::sideload_book_media( $post_id, $book_object, $extract_dir );
		}

		$done = ( $index >= $total - 1 );

		// On final book, clean up the extracted temp directory.
		if ( $done && ! empty( $extract_dir ) ) {
			MBS_Catalog_Archive_Reader::cleanup_extracted( $extract_dir );
			delete_transient( self::TRANSIENT_PREFIX . $token );
		}

		return array(
			'index'  => $index,
			'total'  => $total,
			'done'   => $done,
			'errors' => $errors,
		);
	}

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

	/**
	 * Sideload all media files referenced by one book object.
	 *
	 * Delegates to MBS_Catalog_Media_Importer for all media types. The media
	 * importer class uses correct key names and subdirectory resolution.
	 *
	 * @param int    $post_id     Book post ID.
	 * @param array  $book_object MBS Book Object with image path references.
	 * @param string $extract_dir Absolute path to extracted ZIP temp dir.
	 * @return string[] Non-fatal error strings.
	 */
	private static function sideload_book_media( int $post_id, array $book_object, string $extract_dir ): array {
		$errors = array();
		// Book-level and format-level media (covers, banners, trailers, spinning videos,
		// per-format covers, audio samples) via the canonical media importer class.
		MBS_Catalog_Media_Importer::sideload_media( $post_id, $book_object, $extract_dir );
		// Term banners — idempotent within a single import run (skips if already set).
		MBS_Catalog_Media_Importer::sideload_term_banners( $post_id, $book_object, $extract_dir );
		// Contributor profiles — headshots (idempotent), bio, and website URL.
		MBS_Catalog_Media_Importer::sideload_contributor_profiles( $post_id, $book_object, $extract_dir );
		// Downloadable book-club files.
		MBS_Catalog_Media_Importer::sideload_downloadable_files( $post_id, $book_object, $extract_dir );
		return $errors;
	}


	// ─────────────────────────────────────────────────────────────────────────

	/**
	 * Write book club text fields from the book object to the DAO.
	 *
	 * Writes the enabled flag, discussion guide HTML, and ordered discussion
	 * questions. Downloadable files are handled by sideload_book_media().
	 * No PRO gate — data is always imported; the license gates front-end display.
	 *
	 * Key mapping: XLSX mapper uses 'discussion_guide' (Tab 1 col BB).
	 *
	 * @param int   $post_id     Book post ID.
	 * @param array $book_object MBS Book Object.
	 * @return void
	 * @since  1.0.0
	 */
	private static function import_book_club( int $post_id, array $book_object ): void {
		$bc = $book_object['book_club'] ?? array();
		if ( empty( $bc ) ) {
			return;
		}

		$enabled = ! empty( $bc['enabled'] ) && '0' !== (string) $bc['enabled'];
		$guide   = wp_kses_post( $bc['discussion_guide'] ?? '' );
		MBS_Book_Club_DAO::upsert_header( $post_id, $enabled, $guide );

		$questions = array_values( array_filter( (array) ( $bc['questions'] ?? array() ) ) );
		MBS_Book_Club_DAO::replace_questions( $post_id, $questions );
	}

	/**
	 * Write ARC campaign and reviewer data from the book object to the DAOs.
	 *
	 * Wipes all existing ARC data for the book then reinserts from the book
	 * object. No PRO gate — data is always imported; license gates front-end.
	 *
	 * Sequence:
	 *   1. MBS_Arc_Recipient_DAO::delete_all_for_book() — clear everything.
	 *   2. MBS_Arc_Recipient_DAO::save_campaign()       — upsert header row.
	 *   3. MBS_Arc_Recipient_DAO::insert()              — one row per reviewer.
	 *   4. MBS_Arc_Recipient_DAO::save_urls()           — review URLs per recipient.
	 *
	 * @param int   $post_id     Book post ID.
	 * @param array $book_object MBS Book Object.
	 * @return void
	 * @since  1.0.0
	 */
	private static function import_arc( int $post_id, array $book_object ): void {
		$arc = $book_object['arc'] ?? array();
		if ( empty( $arc ) ) {
			return;
		}

		MBS_Arc_Recipient_DAO::delete_all_for_book( $post_id );

		MBS_Arc_Recipient_DAO::save_campaign( $post_id, array(
			'status'             => $arc['status']             ?? 'not_started',
			'copy_count'         => $arc['copy_count']         ?? 0,
			'distribution_date'  => $arc['distribution_date']  ?? '',
			'close_date'         => $arc['close_date']         ?? '',
			'target_review_date' => $arc['target_review_date'] ?? '',
		) );

		foreach ( $arc['reviewers'] ?? array() as $r ) {
			if ( '' === ( $r['reviewer_name'] ?? '' ) ) {
				continue;
			}
			$recipient_id = MBS_Arc_Recipient_DAO::insert( array(
				'post_id'        => $post_id,
				'reviewer_name'  => $r['reviewer_name'],
				'contact_method' => $r['contact_method'] ?? '',
				'contact_type'   => $r['contact_type']   ?? '',
				'date_sent'      => '' !== ( $r['date_sent'] ?? '' ) ? $r['date_sent'] : null,
				'review_posted'  => $r['review_posted']  ?? 0,
			) );
			if ( false !== $recipient_id && ! empty( $r['review_urls'] ) ) {
				MBS_Arc_Recipient_DAO::save_urls( $recipient_id, $r['review_urls'] );
			}
		}
	}
}
