<?php
/**
 * File Path: includes/export/class-mbs-export-media-collector.php
 *
 * MBS Export Media Collector
 *
 * Given a post_id, an MBS Book Object, and an open ZipArchive, resolves all
 * WP attachment file paths referenced by the book and adds each file to the
 * ZIP archive under the correct images/ subdirectory. Returns an updated copy
 * of the MBS Book Object with image keys replaced by the archive-relative
 * paths that were actually written.
 *
 * This class has one responsibility: read attachment metadata, copy files
 * into the ZIP, and return updated path references. It does not build the
 * archive envelope (manifest, books.json, taxonomy.json) — that is
 * MBS_Exporter_Zip's job (MBS_Archive_Builder is retired).
 *
 * Subdirectory mapping:
 *   cover_image_id  (per-format)  → images/covers/{post_slug}-{format}.{ext}
 *   mbs_banner_image_id (postmeta) → images/banners/{post_slug}-banner.{ext}
 *   featured image (thumbnail)    → images/covers/{post_slug}-main.{ext}
 *   spinning_video_id (per-format) → images/media/{post_slug}-{format}-spin.{ext}
 *   audio_sample_id  (per-format) → images/media/{post_slug}-{format}-audio.{ext}
 *   mbs_trailer_video_id (postmeta) → images/media/{post_slug}-trailer.{ext}
 *
 * @package    My_Book_Showroom
 * @since      1.0.0
 */

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

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

	/**
	 * Add all media files from one book to the ZIP and return an updated
	 * MBS Book Object with archive-relative path strings in all image keys.
	 *
	 * @param int         $post_id     Book post ID.
	 * @param array       $book_object MBS Book Object from MBS_Book_Serializer.
	 * @param ZipArchive  $zip         Open, writable ZipArchive instance.
	 * @return array Updated MBS Book Object with resolved image paths.
	 */
	public static function collect( int $post_id, array $book_object, \ZipArchive $zip ): array {
		$post = get_post( $post_id );
		$slug = $post ? sanitize_title( $post->post_title ) : 'book-' . $post_id;

		// ── Book-level images ─────────────────────────────────────────────────

		// Featured image (cover_main).
		$thumbnail_id = (int) get_post_thumbnail_id( $post_id ); // R5-exempt: media collection for ZIP export
		if ( $thumbnail_id > 0 ) {
			$path = self::add_attachment( $zip, $thumbnail_id, 'images/covers', $slug . '-main' );
			if ( $path ) {
				$book_object['images']['cover_main'] = $path;
			}
		}

		// Banner image.
		$banner_id = (int) get_post_meta( $post_id, 'mbs_banner_image_id', true );
		if ( $banner_id > 0 ) {
			$path = self::add_attachment( $zip, $banner_id, 'images/banners', $slug . '-banner' );
			if ( $path ) {
				$book_object['images']['banner'] = $path;
			}
		}

		// Trailer video (stored as postmeta attachment ID).
		$trailer_id = (int) get_post_meta( $post_id, 'mbs_trailer_video_id', true );
		if ( $trailer_id > 0 ) {
			$path = self::add_attachment( $zip, $trailer_id, 'images/media', $slug . '-trailer' );
			if ( $path ) {
				$book_object['images']['trailer_video_id'] = $trailer_id; // ID preserved; path added.
				$book_object['images']['trailer_video_path'] = $path;
			}
		}

		// ── Per-format images ─────────────────────────────────────────────────
		if ( ! empty( $book_object['format_rows'] ) ) {
			foreach ( $book_object['format_rows'] as $idx => $fr ) {
				$format_slug = sanitize_title( $fr['format'] ?? 'format-' . $idx );
				$format_rows_raw = MBS_Format_Row_DAO::get_by_post( $post_id );

				// Match by index — same order as MBS_Book_Serializer::build_format_rows().
				$raw_row = $format_rows_raw[ $idx ] ?? null;

				if ( $raw_row ) {
					// Cover image.
					$cover_id = (int) ( $raw_row->cover_image_id ?? 0 );
					if ( $cover_id > 0 ) {
						$path = self::add_attachment( $zip, $cover_id, 'images/covers', $slug . '-' . $format_slug );
						if ( $path ) {
							$book_object['format_rows'][ $idx ]['cover_image'] = $path;
						}
					}

					// Spinning video.
					$spin_id = (int) ( $raw_row->spinning_video_id ?? 0 );
					if ( $spin_id > 0 ) {
						$path = self::add_attachment( $zip, $spin_id, 'images/media', $slug . '-' . $format_slug . '-spin' );
						if ( $path ) {
							$book_object['format_rows'][ $idx ]['spinning_video'] = $path;
						}
					}

					// Audio sample.
					$audio_id = (int) ( $raw_row->audio_sample_id ?? 0 );
					if ( $audio_id > 0 ) {
						$path = self::add_attachment( $zip, $audio_id, 'images/media', $slug . '-' . $format_slug . '-audio' );
						if ( $path ) {
							$book_object['format_rows'][ $idx ]['audio_sample'] = $path;
						}
					}
				}
			}
		}

		return $book_object;
	}


	// -------------------------------------------------------------------------
	// Public attachment resolution helpers (used by MBS_Exporter_Zip)
	// -------------------------------------------------------------------------

	/**
	 * Return the lowercase file extension for a WP attachment.
	 *
	 * Resolves the attachment via get_attached_file(). Returns 'jpg' when the
	 * attachment cannot be resolved — a safe default for image exports.
	 *
	 * Used by MBS_Exporter_Zip before building the human-readable cover filename,
	 * because the extension must be known before the filename can be constructed.
	 *
	 * @param int $att_id WP attachment post ID.
	 * @return string Lowercase extension without leading dot (e.g. 'jpg', 'png', 'mp4').
	 * @since  1.0.0
	 */
	public static function get_attachment_ext( int $att_id ): string {
		$file = get_attached_file( $att_id );
		if ( ! $file || '' === $file ) {
			return 'jpg';
		}
		return strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
	}

	/**
	 * Add a WP attachment to a ZIP archive at a caller-specified archive path.
	 *
	 * Returns true when the file was successfully added to the archive; false
	 * when the attachment cannot be resolved, the file does not exist on disk,
	 * or ZipArchive::addFile() fails.
	 *
	 * Used by MBS_Exporter_Zip to add cover images with a human-readable
	 * initials-based naming convention while delegating all attachment resolution
	 * to this class (Sprint Plan Phase 9 — MBS_Exporter_Zip refactor).
	 *
	 * @param \ZipArchive $zip          Open, writable ZipArchive instance.
	 * @param int          $att_id       WP attachment post ID.
	 * @param string       $archive_path Archive-relative path including filename
	 *                                   and extension (e.g. 'covers/AB_CD_cover.jpg').
	 * @return bool True on success, false on failure.
	 * @since  1.0.0
	 */
	public static function add_attachment_as( \ZipArchive $zip, int $att_id, string $archive_path ): bool {
		$file = get_attached_file( $att_id );
		if ( ! $file || ! file_exists( $file ) ) {
			return false;
		}
		return $zip->addFile( $file, $archive_path );
	}

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

	/**
	 * Resolve an attachment to its filesystem path, add it to the ZIP under
	 * $subdir/$basename.{ext}, and return the archive-relative path string.
	 *
	 * Returns null (and does not add to ZIP) if the attachment file cannot be
	 * resolved or does not exist on disk.
	 *
	 * @param ZipArchive $zip        Open ZipArchive.
	 * @param int        $att_id     WP attachment post ID.
	 * @param string     $subdir     Archive subdirectory (e.g. 'images/covers').
	 * @param string     $basename   Filename without extension.
	 * @return string|null Archive-relative path, or null on failure.
	 */
	private static function add_attachment( \ZipArchive $zip, int $att_id, string $subdir, string $basename ): ?string {
		$file = get_attached_file( $att_id );
		if ( ! $file || ! file_exists( $file ) ) {
			return null;
		}

		$ext          = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
		$archive_name = trim( $subdir, '/' ) . '/' . $basename . ( $ext ? '.' . $ext : '' );

		// addFile() with localname prevents directory structure leakage.
		if ( $zip->addFile( $file, $archive_name ) ) {
			return $archive_name;
		}

		return null;
	}
}
