<?php
/**
 * File Path: includes/export/class-mbs-book-export-data.php
 *
 * Book Export Data Helper
 *
 * Shared data-gathering and row-building logic used by MBS_Exporter_CSV and
 * MBS_Exporter_XLSX. Consolidates the formerly duplicated get_book_data(),
 * build_row(), column_headers(), and safe_cell() methods that previously
 * existed independently in each exporter (D-001, D-002, D-003).
 *
 * Uses MBS_Formatting::format_series_number() (D-008) and
 * MBS_Formatting::price_for_csv() for consistent output across all export
 * formats. Fixes the latent XLSX price-formatting divergence (D-002).
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Load the CSV helpers so ACCEPTED_COLUMNS is available.
require_once dirname( __DIR__ ) . '/import/csv/class-mbs-child-importer-csv-helpers.php';

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

	/**
	 * Return the export column headers.
	 *
	 * Ordering matches ACCEPTED_COLUMNS so the exported file can be re-imported
	 * without any column remapping in the Tab 5 UI.
	 *
	 * @return string[]
	 */
	public static function column_headers(): array {
		return MBS_Child_Importer_CSV_Helpers::ACCEPTED_COLUMNS;
	}

	/**
	 * Gather post-level (book) data shared across all format rows.
	 *
	 * Delegates to MBS_Book_Serializer::serialize() for all book data reads.
	 * Callers may pass a pre-serialized $book_object to avoid a second
	 * serializer call when one has already been made in the same request
	 * (e.g. MBS_Exporter_Zip::download()).
	 *
	 * IEF-B-010 fix: synopsis is exported regardless of license tier per
	 * Q5 policy (all fields exported on every export operation). The PRO
	 * tier gate previously on this field has been removed.
	 *
	 * IEF-A-013 fix (side effect): buy links now flow through
	 * MBS_Book_Serializer, which correctly separates book-level and
	 * per-format links. The 42-column CSV format does not include a buy
	 * links column, so the fix is transparent here but prevents regression
	 * if a column is added in a future sprint.
	 *
	 * @param int        $post_id     WordPress post ID.
	 * @param array|null $book_object Optional pre-serialized MBS Book Object
	 *                                from MBS_Book_Serializer::serialize(). When
	 *                                null the serializer is called internally.
	 * @return array<string, string> Flat 42-column data keyed by column name.
	 *                               Returns an empty array for invalid post IDs.
	 * @since  1.0.0
	 */
	public static function get_book_data( int $post_id, ?array $book_object = null ): array {
		// Serialize via MBS_Book_Serializer (Phase 9 delegation).
		// If the caller already serialized the book, pass it in to avoid a
		// second DB round-trip.
		$obj = $book_object ?? MBS_Book_Serializer::serialize( $post_id );
		if ( empty( $obj ) ) {
			return array();
		}

		$meta     = $obj['post_meta']        ?? array();
		$tax      = $obj['taxonomies']        ?? array();
		$class    = $obj['classifications']   ?? array();
		$contribs = $obj['contributors']      ?? array();
		$related  = $obj['related_products']  ?? array();

		// Cover image URL: MBS_Book_Serializer stores an archive-relative path,
		// not a URL. The CSV export needs the WP attachment URL for round-trip
		// fidelity so the exported file can be re-imported without remapping.
		$thumbnail_id    = get_post_thumbnail_id( $post_id ); // R5-exempt: export data context
		$cover_image_url = $thumbnail_id ? (string) wp_get_attachment_url( $thumbnail_id ) : '';

		// Illustration types: not included in the MBS Book Object classifications
		// schema (bisac/thema/bic/bowker only). Read directly from the DAO --
		// same source the serializer uses for other schemes.
		$illustration_types = implode( '|', MBS_Classification_DAO::get_book_codes( $post_id, 'illustration' ) );

		// Contributors: serialised as Name:ROLE_CODE|...
		$contributor_parts = array();
		foreach ( $contribs as $cr ) {
			if ( ! empty( $cr['contributor_name'] ) ) {
				$contributor_parts[] = $cr['contributor_name'] . ':' . ( $cr['onix_role_code'] ?? 'A01' );
			}
		}

		// Related products: serialised as related_isbn:relation_code|...
		$rp_parts = array();
		foreach ( $related as $rp ) {
			if ( ! empty( $rp['related_isbn'] ) ) {
				$rp_parts[] = $rp['related_isbn'] . ':' . ( $rp['relation_code'] ?? '' );
			}
		}

		return array(
			'title'              => (string) ( $obj['post_title']             ?? '' ),
			'teaser'             => (string) ( $meta['mbs_teaser']            ?? '' ),
			'tagline'            => (string) ( $meta['mbs_tagline']           ?? '' ),
			'publisher'          => (string) ( $meta['mbs_publisher']         ?? '' ),
			'pub_date'           => (string) ( $meta['mbs_pub_date']          ?? '' ),
			'coming_soon'        => ! empty( $meta['mbs_coming_soon'] )      ? '1' : '',
			'author'             => (string) ( $tax['mbs_author'][0]          ?? '' ),
			'series'             => (string) ( $tax['mbs_series'][0]          ?? '' ),
			'series_number'      => MBS_Formatting::format_series_number( (string) ( $meta['mbs_series_position'] ?? '' ) ),
			'genre'              => implode( '|', $tax['mbs_genre']           ?? array() ),
			'tags'               => implode( '|', $tax['mbs_tag']             ?? array() ),
			'cover_image_url'    => $cover_image_url,
			'bisac_codes'        => implode( '|', $class['bisac']             ?? array() ),
			'bowker_codes'       => implode( '|', $class['bowker']            ?? array() ),
			'audience'           => (string) ( $meta['mbs_audience_code']     ?? '' ),
			'age_range_from'     => (string) ( $meta['mbs_age_range_from']    ?? '' ),
			'age_range_to'       => (string) ( $meta['mbs_age_range_to']      ?? '' ),
			'grade_range_from'   => (string) ( $meta['mbs_grade_range_from']  ?? '' ),
			'grade_range_to'     => (string) ( $meta['mbs_grade_range_to']    ?? '' ),
			'toc'                => (string) ( $meta['mbs_toc']               ?? '' ),
			'lccn'               => (string) ( $meta['mbs_lccn']              ?? '' ),
			'illustration_count' => (string) ( $meta['mbs_illustration_count'] ?? 0 ),
			'illustration_types' => $illustration_types,
			'edition_statement'  => (string) ( $meta['mbs_edition']           ?? '' ),
			'publication_time'   => (string) ( $meta['mbs_publication_time']  ?? '' ),
			'featured'           => ! empty( $meta['mbs_featured'] )         ? '1' : '',
			'imprint'            => (string) ( $tax['mbs_imprint'][0]         ?? '' ),
			'contributor'        => implode( '|', $contributor_parts ),
			'related_products'   => implode( '|', $rp_parts ),
			'look_inside_text'   => (string) ( $meta['mbs_look_inside_text']  ?? '' ),
			'thema_codes'        => implode( '|', $class['thema']             ?? array() ),
			'bic_codes'          => implode( '|', $class['bic']               ?? array() ),
			// Synopsis is a PRO-tier export feature -- consistent with ONIX export (EXP-018 fix).
			'synopsis'           => mbs_is_pro() ? (string) ( $meta['mbs_synopsis'] ?? '' ) : ''  ,
		);
	}

	/**
	 * Build a single export row by merging book-level data with one format row.
	 *
	 * Column order matches ACCEPTED_COLUMNS exactly (42 columns).
	 * Used by both MBS_Exporter_CSV and MBS_Exporter_XLSX.
	 *
	 * @param array       $book_data  From self::get_book_data().
	 * @param object|null $format_row A row object from MBS_Format_Row_DAO, or null.
	 * @return string[]  Ordered values matching column_headers().
	 */
	public static function build_row( array $book_data, ?object $format_row ): array {
		$isbn13         = '';
		$asin           = '';
		$format         = '';
		$price          = '';
		// series_number is book-level (ARCH-SERIES-001) -- comes from book_data.
		$series_number  = $book_data['series_number'];
		$page_length    = '';
		$edition_number = '';
		$edition_type   = '';
		$format_label   = '';

		if ( null !== $format_row ) {
			$isbn13      = (string) ( $format_row->isbn13 ?? '' );
			$asin        = (string) ( $format_row->asin ?? '' );
			$format      = (string) ( $format_row->format ?? '' );
			// PLG-028: bare numeric value -- no symbol, no ISO code.
			$price       = '' !== (string) ( $format_row->price ?? '' )
				? MBS_Formatting::price_for_csv( (float) $format_row->price )
				: '';
			$page_length    = ( ! empty( $format_row->page_length ) )
				? (string) (int) $format_row->page_length
				: '';
			$edition_number = (string) ( isset( $format_row->edition_number ) && null !== $format_row->edition_number
				? (int) $format_row->edition_number : '' );
			$edition_type   = (string) ( $format_row->edition_type ?? '' );
			$format_label   = (string) ( $format_row->format_label ?? '' );
		}

		$look_inside_text = $book_data['look_inside_text'] ?? '';

		// Column order must exactly match ACCEPTED_COLUMNS (42 columns). EXP-001 fixed.
		// S-18: Prefix cells that start with formula-injection characters.
		$sc = array( __CLASS__, 'safe_cell' );
		return array(
			call_user_func( $sc, $book_data['title'] ),             // [0]  title
			call_user_func( $sc, $book_data['teaser'] ),            // [1]  teaser
			call_user_func( $sc, $book_data['tagline'] ),           // [2]  tagline
			call_user_func( $sc, $book_data['publisher'] ),         // [3]  publisher
			$book_data['pub_date'],                                 // [4]  pub_date
			$book_data['coming_soon'],                              // [5]  coming_soon
			$isbn13,                                                // [6]  isbn13
			$asin,                                                  // [7]  asin
			call_user_func( $sc, $format ),                         // [8]  format
			call_user_func( $sc, $format_label ),                   // [9]  format_label
			$price,                                                 // [10] price
			call_user_func( $sc, $look_inside_text ),               // [11] look_inside_text
			call_user_func( $sc, $book_data['author'] ),            // [12] author
			call_user_func( $sc, $book_data['series'] ),            // [13] series
			$series_number,                                         // [14] series_number
			call_user_func( $sc, $book_data['genre'] ),             // [15] genre
			call_user_func( $sc, $book_data['tags'] ),              // [16] tags
			$page_length,                                           // [17] page_length
			$book_data['cover_image_url'],                          // [18] cover_image_url
			'',                                                     // [19] cover_image_file (not exportable)
			call_user_func( $sc, $book_data['bisac_codes'] ),       // [20] bisac_codes
			call_user_func( $sc, $book_data['thema_codes'] ),       // [21] thema_codes
			call_user_func( $sc, $book_data['bic_codes'] ),         // [22] bic_codes
			call_user_func( $sc, $book_data['bowker_codes'] ),      // [23] bowker_codes
			$book_data['audience'],                                 // [24] audience
			$book_data['age_range_from'],                           // [25] age_range_from
			$book_data['age_range_to'],                             // [26] age_range_to
			call_user_func( $sc, $book_data['grade_range_from'] ),  // [27] grade_range_from
			call_user_func( $sc, $book_data['grade_range_to'] ),    // [28] grade_range_to
			call_user_func( $sc, $book_data['synopsis'] ),          // [29] synopsis
			call_user_func( $sc, $book_data['toc'] ),               // [30] toc
			$book_data['lccn'],                                     // [31] lccn
			$book_data['illustration_count'],                       // [32] illustration_count
			$book_data['illustration_types'],                       // [33] illustration_types
			$edition_number,                                        // [34] edition_number
			$edition_type,                                          // [35] edition_type
			call_user_func( $sc, $book_data['edition_statement'] ), // [36] edition_statement
			$book_data['publication_time'],                         // [37] publication_time
			$book_data['featured'],                                 // [38] featured
			call_user_func( $sc, $book_data['imprint'] ),           // [39] imprint
			call_user_func( $sc, $book_data['contributor'] ),       // [40] contributor
			call_user_func( $sc, $book_data['related_products'] ),  // [41] related_products
		);
	}

	/**
	 * S-18: Neutralise CSV/XLSX formula injection.
	 *
	 * Prefixes values starting with formula characters (=, +, -, @) with a tab
	 * to prevent spreadsheet applications from interpreting them as formulas.
	 *
	 * @param string $value Raw cell value.
	 * @return string       Safe cell value.
	 */
	public static function safe_cell( string $value ): string {
		if ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', '`' ), true ) ) {
			return "\t" . $value;
		}
		return $value;
	}

	/**
	 * Return the canonical WP_Query arguments for a full catalog export.
	 *
	 * Shared by collect_all_rows() (XLSX), MBS_Exporter_CSV::download() (CSV),
	 * and MBS_Exporter_Zip::download() (ZIP) to prevent three independent copies
	 * of the same query definition (D-009 extension).
	 *
	 * @return array<string, mixed>
	 * @since  1.0.0
	 */
	public static function get_all_books_query_args(): array {
		return array(
			'post_type'      => MBS_POST_TYPE,
			'post_status'    => array( 'publish', 'private' ),
			'posts_per_page' => -1,
			'orderby'        => 'title',
			'order'          => 'ASC',
			'no_found_rows'  => true,
		);
	}

	/**
	 * Collect all export rows for the entire catalog.
	 *
	 * Runs the WP_Query loop and returns a flat array of row arrays suitable
	 * for XLSX export. CSV streaming bypasses this method and calls
	 * get_book_data() + build_row() inline to avoid accumulating the full
	 * catalog in memory.
	 *
	 * @return array<int, string[]>
	 */
	public static function collect_all_rows(): array {
		$rows  = array();
		$query = new WP_Query( static::get_all_books_query_args() );
		foreach ( $query->posts as $post ) {
			$post_id     = (int) $post->ID;
			$book_data   = static::get_book_data( $post_id );
			$format_rows = MBS_Format_Row_DAO::get_by_post( $post_id );
			if ( empty( $format_rows ) ) {
				$rows[] = static::build_row( $book_data, null );
			} else {
				foreach ( $format_rows as $fr ) {
					$rows[] = static::build_row( $book_data, $fr );
				}
			}
		}
		return $rows;
	}
}
