<?php
/**
 * File Path: includes/api/class-mbs-onix-book-writer.php
 *
 * ONIX Book Writer -- My Book Showroom
 *
 * Persistence layer for ONIX import operations.  Owns all direct writes to
 * wp_posts, wp_postmeta, and mbs_format_rows that result from an ONIX feed.
 *
 * Extracted from MBS_Onix_Reconciler (REF-003, Session 136) to separate
 * the database-write concern from the reconciliation logic.
 *
 * Public API consumed by MBS_Onix_Reconciler:
 *   insert_book( array $book_data ): int|WP_Error
 *   update_book( int $post_id, array $book_data ): void
 *   insert_format_row( int $post_id, array $product ): void
 *   update_format_row( int $format_row_id, ?int $post_id, array $product ): void
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Api
 * @since      1.1.0
 */

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

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

	// -------------------------------------------------------------------------
	// Book-level persistence
	// -------------------------------------------------------------------------

	/**
	 * Create a new draft book post and write all available book-level meta.
	 *
	 * Returns the new post ID on success, or WP_Error on wp_insert_post failure.
	 * The caller (MBS_Onix_Reconciler::branch_c_create) is responsible for
	 * writing stats / notices and inserting the first format row.
	 *
	 * @param array $book_data Normalised product array (first product in group).
	 * @return int|WP_Error New post ID, or WP_Error on failure.
	 */
	public function insert_book( array $book_data ): int|WP_Error {
		$post_id = wp_insert_post( array(
			'post_type'    => MBS_POST_TYPE,
			'post_status'  => 'draft',
			'post_title'   => sanitize_text_field( $book_data['title'] ),
			'post_content' => wp_kses_post( $book_data['description'] ),
			'post_excerpt' => sanitize_text_field( $book_data['short_desc'] ),
		), true );

		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		}

		$post_id = (int) $post_id;
		$this->write_new_book_meta( $post_id, $book_data );
		return $post_id;
	}

	/**
	 * Update all book-level postmeta and post fields for an existing post.
	 *
	 * Called before branch dispatch on any group that has an existing post match.
	 * Spec: ONIX-DEC-017 -- always overwrite title and content fields.
	 *
	 * @param int   $post_id
	 * @param array $book_data Normalised product array (first product in group).
	 * @return void
	 */
	public function update_book( int $post_id, array $book_data ): void {
		// Always update title and content fields on branch A/B (spec ONIX-DEC-017).
		if ( '' !== $book_data['title'] ) {
			wp_update_post( array( 'ID' => $post_id, 'post_title' => sanitize_text_field( $book_data['title'] ) ) );
		}
		if ( '' !== $book_data['description'] ) {
			wp_update_post( array( 'ID' => $post_id, 'post_content' => wp_kses_post( $book_data['description'] ) ) );
		}
		if ( '' !== $book_data['short_desc'] ) {
			wp_update_post( array( 'ID' => $post_id, 'post_excerpt' => sanitize_text_field( $book_data['short_desc'] ) ) );
		}
		if ( '' !== $book_data['subtitle'] )          { update_post_meta( $post_id, 'mbs_subtitle',        sanitize_text_field( $book_data['subtitle'] ) ); }
		if ( '' !== $book_data['toc'] )              { update_post_meta( $post_id, 'mbs_toc',             sanitize_text_field( $book_data['toc'] ) ); }
		if ( '' !== $book_data['language'] )          { update_post_meta( $post_id, 'mbs_language',        MBS_Onix_Field_Map::sanitize_language_code( $book_data['language'] ) ); }
		// pub_date is canonical on mbs_format_rows (written by insert_format_row); postmeta write removed (B-010).
		if ( '' !== $book_data['publisher'] )         { update_post_meta( $post_id, 'mbs_publisher',       sanitize_text_field( $book_data['publisher'] ) ); }
		if ( '' !== $book_data['audience_code'] )     { update_post_meta( $post_id, 'mbs_audience_code',   sanitize_text_field( $book_data['audience_code'] ) ); }
		if ( '' !== $book_data['edition_statement'] ) { update_post_meta( $post_id, 'mbs_edition',         sanitize_text_field( $book_data['edition_statement'] ) ); }
		// BISAC codes: overwrite with all codes from the ONIX file (import is authoritative).
		if ( ! empty( $book_data['bisac_codes'] ) ) {
			MBS_Classification_DAO::save_book_codes( $post_id, 'bisac', array_values( $book_data['bisac_codes'] ) );
		}
		if ( ! empty( $book_data['thema_codes'] ) ) {
			MBS_Classification_DAO::save_book_codes( $post_id, 'thema', array_values( $book_data['thema_codes'] ) );
		}

		if ( isset( $book_data['prizes'] ) && is_array( $book_data['prizes'] ) ) {
			$this->write_prizes_meta( $post_id, $book_data['prizes'] );
		}
		// O1-G6: ProductContact (PUB-gated).
		$this->write_product_contact_meta( $post_id, $book_data );
		// O1-G6: Territorial rights v2 -- write to mbs_book_territorial_rights via DAO.
		if ( isset( $book_data['territorial_rights_v2'] ) && '' !== $book_data['territorial_rights_v2'] ) {
			$tr_decoded   = json_decode( $book_data['territorial_rights_v2'], true );
			$tr_type      = ( is_array( $tr_decoded ) && isset( $tr_decoded['type'] ) )
				? $tr_decoded['type'] : 'world';
			$tr_countries = ( is_array( $tr_decoded ) && isset( $tr_decoded['countries'] ) && is_array( $tr_decoded['countries'] ) )
				? $tr_decoded['countries'] : array();
			MBS_Book_Territorial_Rights_DAO::save( $post_id, $tr_type, $tr_countries );
		}
		// O1-G6: Contributor biographical note (PUB-gated).
		if ( mbs_is_pub() && isset( $book_data['contributor_statement'] ) && '' !== $book_data['contributor_statement'] ) {
			update_post_meta( $post_id, 'mbs_contributor_statement', sanitize_text_field( $book_data['contributor_statement'] ) );
		}
		// GAP-007: Series term -- assign in MBS_TAX_SERIES.
		if ( isset( $book_data['series_name'] ) && '' !== $book_data['series_name'] ) {
			$series_tid = $this->find_or_create_series_term( $book_data['series_name'] );
			if ( null !== $series_tid ) {
				wp_set_object_terms( $post_id, $series_tid, MBS_TAX_SERIES, true );
			}
		}
		// ARCH-SERIES-001: series position is book-level; write to post_meta.
		if ( isset( $book_data['series_number'] ) && '' !== $book_data['series_number'] ) {
			update_post_meta( $post_id, 'mbs_series_position', sanitize_text_field( $book_data['series_number'] ) );
		}
	}

	// -------------------------------------------------------------------------
	// Format-row persistence
	// -------------------------------------------------------------------------

	/**
	 * Insert a new format row for a book.
	 *
	 * @param int   $post_id
	 * @param array $product Normalised product array.
	 * @return void
	 */
	public function insert_format_row( int $post_id, array $product ): void {
		$format = MBS_Onix_Field_Map::product_form_to_slug(
			$product['product_form'],
			$product['product_detail']
		);

		// Series position is book-level (ARCH-SERIES-001) -- written by the book-level
		// save blocks above; not stored in format_rows.
		MBS_Format_Row_DAO::insert( array(
			'post_id'        => $post_id,
			'format'         => $format,
			'isbn13'         => $product['isbn'],
			'price'          => '' !== $product['price'] ? floatval( $product['price'] ) : null,
			'currency_code'  => '' !== $product['currency'] ? strtoupper( $product['currency'] ) : null,
			'page_length'    => $product['page_length'] > 0 ? $product['page_length'] : null,
			'duration_hh'    => $product['duration_hh'] > 0 ? $product['duration_hh'] : null,
			'duration_mm'    => $product['duration_mm'] > 0 ? $product['duration_mm'] : null,
			'narrator'       => '' !== $product['narrator'] ? $product['narrator'] : null,
			'pub_date'       => '' !== $product['pub_date'] ? $product['pub_date'] : null,
			'edition_number' => $product['edition_number'] > 0 ? $product['edition_number'] : null,
			'edition_type'   => '' !== $product['edition_type'] ? $product['edition_type'] : null,
			'sort_order'     => 0,
		) );
	}

	/**
	 * Update an existing format row's scalar fields.
	 *
	 * Called from MBS_Onix_Reconciler::branch_a_update() after the ISBN match.
	 * Series position is a book-level attribute and is written to postmeta by
	 * the caller, not to format_rows.
	 *
	 * @param int      $format_row_id Format row ID to update.
	 * @param int|null $post_id       Post ID (used only for series position update in caller).
	 * @param array    $product       Normalised product array.
	 * @return void
	 */
	public function update_format_row( int $format_row_id, ?int $post_id, array $product ): void {
		$format = MBS_Onix_Field_Map::product_form_to_slug(
			$product['product_form'],
			$product['product_detail']
		);

		MBS_Format_Row_DAO::update( $format_row_id, array(
			'format'         => $format,
			'price'          => '' !== $product['price'] ? floatval( $product['price'] ) : null,
			'currency_code'  => '' !== $product['currency'] ? strtoupper( $product['currency'] ) : null,
			'page_length'    => $product['page_length'] > 0 ? $product['page_length'] : null,
			'duration_hh'    => $product['duration_hh'] > 0 ? $product['duration_hh'] : null,
			'duration_mm'    => $product['duration_mm'] > 0 ? $product['duration_mm'] : null,
			'narrator'       => '' !== $product['narrator'] ? $product['narrator'] : null,
			'pub_date'       => '' !== $product['pub_date'] ? $product['pub_date'] : null,
			'edition_number' => $product['edition_number'] > 0 ? $product['edition_number'] : null,
			'edition_type'   => '' !== $product['edition_type'] ? $product['edition_type'] : null,
		) );
	}

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

	/**
	 * Write all postmeta and taxonomy terms for a newly created book post.
	 *
	 * Called only from insert_book() after wp_insert_post() succeeds.
	 *
	 * @param int   $post_id   Post ID returned by wp_insert_post().
	 * @param array $book_data Normalised product array (first product in the group).
	 * @return void
	 */
	private function write_new_book_meta( int $post_id, array $book_data ): void {
		if ( '' !== $book_data['subtitle'] )          { update_post_meta( $post_id, 'mbs_subtitle',        sanitize_text_field( $book_data['subtitle'] ) ); }
		if ( '' !== $book_data['toc'] )              { update_post_meta( $post_id, 'mbs_toc',             sanitize_text_field( $book_data['toc'] ) ); }
		if ( '' !== $book_data['publisher'] )         { update_post_meta( $post_id, 'mbs_publisher',       sanitize_text_field( $book_data['publisher'] ) ); }
		if ( '' !== $book_data['language'] )          { update_post_meta( $post_id, 'mbs_language',        MBS_Onix_Field_Map::sanitize_language_code( $book_data['language'] ) ); }
		// pub_date is canonical on mbs_format_rows (written by insert_format_row); postmeta write removed (B-010).
		if ( '' !== $book_data['audience_code'] )     { update_post_meta( $post_id, 'mbs_audience_code',   sanitize_text_field( $book_data['audience_code'] ) ); }
		if ( '' !== $book_data['edition_statement'] ) { update_post_meta( $post_id, 'mbs_edition',         sanitize_text_field( $book_data['edition_statement'] ) ); }
		// BISAC codes: overwrite with all codes from the ONIX file (import is authoritative).
		if ( ! empty( $book_data['bisac_codes'] ) ) {
			MBS_Classification_DAO::save_book_codes( $post_id, 'bisac', array_values( $book_data['bisac_codes'] ) );
		}
		if ( ! empty( $book_data['thema_codes'] ) ) {
			MBS_Classification_DAO::save_book_codes( $post_id, 'thema', array_values( $book_data['thema_codes'] ) );
		}

		if ( isset( $book_data['prizes'] ) && is_array( $book_data['prizes'] ) ) {
			$this->write_prizes_meta( $post_id, $book_data['prizes'] );
		}
		// O1-G6: ProductContact (PUB-gated).
		$this->write_product_contact_meta( $post_id, $book_data );
		// O1-G6: Territorial rights v2 -- write to mbs_book_territorial_rights via DAO.
		if ( isset( $book_data['territorial_rights_v2'] ) && '' !== $book_data['territorial_rights_v2'] ) {
			$tr_decoded   = json_decode( $book_data['territorial_rights_v2'], true );
			$tr_type      = ( is_array( $tr_decoded ) && isset( $tr_decoded['type'] ) )
				? $tr_decoded['type'] : 'world';
			$tr_countries = ( is_array( $tr_decoded ) && isset( $tr_decoded['countries'] ) && is_array( $tr_decoded['countries'] ) )
				? $tr_decoded['countries'] : array();
			MBS_Book_Territorial_Rights_DAO::save( $post_id, $tr_type, $tr_countries );
		}
		// O1-G6: Contributor biographical note (PUB-gated).
		if ( mbs_is_pub() && isset( $book_data['contributor_statement'] ) && '' !== $book_data['contributor_statement'] ) {
			update_post_meta( $post_id, 'mbs_contributor_statement', sanitize_text_field( $book_data['contributor_statement'] ) );
		}
		// GAP-006: Author term -- create/find in MBS_TAX_AUTHOR and assign.
		if ( '' !== $book_data['author_key_names'] ) {
			$author_term = term_exists( $book_data['author_key_names'], MBS_TAX_AUTHOR );
			if ( ! $author_term ) {
				$author_term = wp_insert_term( $book_data['author_key_names'], MBS_TAX_AUTHOR );
			}
			if ( ! is_wp_error( $author_term ) ) {
				$author_term_id = is_array( $author_term ) ? (int) $author_term['term_id'] : (int) $author_term;
				wp_set_object_terms( $post_id, $author_term_id, MBS_TAX_AUTHOR, true );
			}
		}

		// GAP-007: Series term -- create/find in MBS_TAX_SERIES and assign.
		if ( isset( $book_data['series_name'] ) && '' !== $book_data['series_name'] ) {
			$series_tid = $this->find_or_create_series_term( $book_data['series_name'] );
			if ( null !== $series_tid ) {
				wp_set_object_terms( $post_id, $series_tid, MBS_TAX_SERIES, true );
			}
		}
		// ARCH-SERIES-001: series position is book-level; write to post_meta.
		if ( isset( $book_data['series_number'] ) && '' !== $book_data['series_number'] ) {
			update_post_meta( $post_id, 'mbs_series_position', sanitize_text_field( $book_data['series_number'] ) );
		}
	}

	/**
	 * Write ProductContact post meta from imported ONIX data (PUB-gated).
	 *
	 * Writes mbs_product_contact_role, mbs_product_contact_name, and
	 * mbs_product_contact_email if the values are non-empty.  Only runs
	 * for PUB-tier sites; silently returns for FREE/PRO.
	 *
	 * @param int   $post_id   Post ID.
	 * @param array $book_data Normalised product array from parser.
	 * @return void
	 */
	private function write_product_contact_meta( int $post_id, array $book_data ): void {
		if ( ! mbs_is_pub() ) {
			return;
		}
		$role  = sanitize_text_field( (string) ( $book_data['product_contact_role']  ?? '' ) );
		$name  = sanitize_text_field( (string) ( $book_data['product_contact_name']  ?? '' ) );
		$email = sanitize_email(      (string) ( $book_data['product_contact_email'] ?? '' ) );
		if ( '' !== $role )  { update_post_meta( $post_id, 'mbs_product_contact_role',  $role  ); }
		if ( '' !== $name )  { update_post_meta( $post_id, 'mbs_product_contact_name',  $name  ); }
		if ( '' !== $email ) { update_post_meta( $post_id, 'mbs_product_contact_email', $email ); }
	}

	/**
	 * Write mbs_awards post meta from imported ONIX Prize composites.
	 *
	 * Converts ONIX Code List 41 prize codes back to MBS award status strings.
	 * Import is treated as authoritative: stored mbs_awards is overwritten.
	 *
	 * ONIX List 41 -> MBS status:
	 *   01 Winner               => winner
	 *   02 Runner-up            => finalist
	 *   03 Shortlisted          => nominee
	 *   04 Longlisted           => longlist
	 *   05 Commended            => honorable_mention
	 *   06 Special commendation => honorable_mention
	 *   (unknown)               => nominee (safe default)
	 *
	 * @param int   $post_id
	 * @param array $prizes  Array of {name, year, code} maps from the parser.
	 * @return void
	 */
	private function write_prizes_meta( int $post_id, array $prizes ): void {
		$code_to_status = array(
			'01' => 'winner',
			'02' => 'finalist',
			'03' => 'nominee',
			'04' => 'longlist',
			'05' => 'honorable_mention',
			'06' => 'honorable_mention',
		);

		$mbs_awards = array();
		foreach ( $prizes as $prize ) {
			$prize_name = sanitize_text_field( (string) ( $prize['name'] ?? '' ) );
			if ( '' === $prize_name ) {
				continue;
			}
			$status       = $code_to_status[ (string) ( $prize['code'] ?? '' ) ] ?? 'nominee';
			$mbs_awards[] = array(
				'name'   => $prize_name,
				'year'   => sanitize_text_field( (string) ( $prize['year'] ?? '' ) ),
				'status' => $status,
			);
		}

		if ( ! empty( $mbs_awards ) ) {
			MBS_Book_Awards_DAO::save( $post_id, $mbs_awards ); // Phase 2: write to custom table
		}
	}

	/**
	 * Find or create a series term in MBS_TAX_SERIES by name.
	 *
	 * Returns the term_id, or null if name is empty or term creation failed.
	 * GAP-007 fix (Session 95).
	 *
	 * @param string $series_name Series title.
	 * @return int|null Term ID, or null.
	 */
	private function find_or_create_series_term( string $series_name ): ?int {
		if ( '' === $series_name ) {
			return null;
		}
		$term = term_exists( $series_name, MBS_TAX_SERIES );
		if ( ! $term ) {
			$term = wp_insert_term( $series_name, MBS_TAX_SERIES );
		}
		if ( is_wp_error( $term ) ) {
			return null;
		}
		return is_array( $term ) ? (int) $term['term_id'] : (int) $term;
	}
}
