<?php
/**
 * File Path: includes/export/class-mbs-book-bible-tab-writer-pub.php
 *
 * Book Bible Tab Writer -- PUB Tier
 *
 * Extends MBS_Book_Bible_Tab_Writer with two additional worksheet tabs
 * available only at the PUB license tier:
 *   Tab 7: Copyright Page -- ready-to-use copyright page draft (CMOS conventions)
 *   Tab 8: EPUB OPF Metadata -- ready-to-paste XML <metadata> block
 *
 * Loaded automatically when mbs_is_pub() is true via MBS_Book_Bible_Tab_Writer::make().
 *
 * @package MyBookShowroom
 * @since   2.6.0
 */

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

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

	/**
	 * Add PUB-tier tabs (Copyright Page and EPUB OPF Metadata) to the workbook.
	 *
	 * Called by MBS_Book_Bible_Tab_Writer::build() after the six core tabs and
	 * before the Specs lookup sheet.
	 *
	 * @param MBS_Book_Bible_Builder $b    Builder instance.
	 * @param array                  $data Gathered book data.
	 * @return void
	 * @since 2.6.0
	 */
	protected function build_extra_tabs( MBS_Book_Bible_Builder $b, array $data ): void {
		$this->build_tab7( $b, $data );
		$this->build_tab8( $b, $data );
	}

	// -------------------------------------------------------------------------
	// PUB-only tab builders
	// -------------------------------------------------------------------------

	/**
	 * Tab 7: Copyright Page (PUB tier only).
	 *
	 * Assembles a ready-to-use copyright page draft. Follows CMOS conventions:
	 * copyright notice, rights statement, publisher block, identifiers, CIP,
	 * production credits, disclaimer, country of printing.
	 *
	 * @param MBS_Book_Bible_Builder $b    Builder instance.
	 * @param array                  $data Gathered book data.
	 * @return void
	 * @since 2.5.0
	 */
	private function build_tab7( MBS_Book_Bible_Builder $b, array $data ): void {
		$s = $b->add_sheet( 'Copyright Page' );
		$this->colophon( $b, $s, $data );
		$row = 3;

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'COPYRIGHT PAGE' );

		// Copyright notice — amber if year or author missing.
		$cr_year     = (string) $data['copyright_year'];
		$author_name = ! empty( $data['author_data'] ) ? (string) $data['author_data'][0]['name'] : '';
		$cr_notice   = ( '' !== $cr_year && '' !== $author_name )
			? "Copyright \u{00A9} {$cr_year} by {$author_name}"
			: '';
		$this->lbl( $b, $s, $row++, 'Copyright Notice', $cr_notice, 1, 7, true );

		// Rights statement (CMOS standard boilerplate; never amber).
		$rights = 'All rights reserved. No part of this publication may be reproduced, distributed, '
			. 'or transmitted in any form or by any means, including photocopying, recording, or '
			. 'other electronic or mechanical methods, without the prior written permission of '
			. 'the publisher, except in the case of brief quotations embodied in critical reviews '
			. 'and certain other noncommercial uses permitted by copyright law.';
		$this->lbl( $b, $s, $row++, 'Rights Statement', $rights, 1, 7, false, true );

		// Publisher block.
		$this->lbl( $b, $s, $row++, 'Publisher', (string) $data['publisher'], 1, 7, true );
		$this->lbl( $b, $s, $row++, 'Imprint', (string) $data['imprint'] );
		$this->lbl( $b, $s, $row++, 'Publisher Address', (string) ( $data['pub_address'] ?? '' ), 1, 7, false, true );
		$pub_url = (string) get_post_meta( (int) $data['post_id'], 'mbs_publisher_url', true );
		$this->lbl( $b, $s, $row++, 'Publisher Website', $pub_url );

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'EDITION INFORMATION' );

		$this->lbl( $b, $s, $row++, 'Publication Date', (string) $data['pub_date'], 1, 7, true );

		$first_fr     = ! empty( $data['format_rows'] ) ? $data['format_rows'][0] : null;
		$edition_num  = $first_fr ? (string) ( $first_fr->edition_number ?? '' ) : '';
		$edition_type = $first_fr ? (string) ( $first_fr->edition_type ?? '' ) : '';

		// L-001: decode common ONIX edition-type codes to human-readable labels.
		$edition_type_labels = array(
			'UBR' => 'Unabridged', 'ABR' => 'Abridged',   'ILL' => 'Illustrated',
			'ENH' => 'Enhanced',   'REV' => 'Revised',     'NED' => 'New edition',
			'BRL' => 'Braille',    'LTE' => 'Large print', 'ORG' => 'Original',
			'RPT' => 'Reprint',
		);
		$edition_type_label = $edition_type_labels[ strtoupper( $edition_type ) ] ?? $edition_type;

		$edition_label = '';
		if ( '' !== $edition_type_label && '' !== $edition_num ) {
			$edition_label = $edition_type_label . ' ' . $edition_num;
		} elseif ( '' !== $edition_type_label ) {
			$edition_label = $edition_type_label;
		} elseif ( '' !== $edition_num ) {
			$edition_label = $edition_num;
		}
		$this->lbl( $b, $s, $row++, 'Edition', $edition_label );

		// Printer's key: descending line from 10 to edition_number (e.g. "10 9 8 7 6 5 4 3 2 1").
		$edition_int = (int) $edition_num;
		if ( $edition_int >= 1 && $edition_int <= 10 ) {
			$this->lbl( $b, $s, $row++, 'Printing History', implode( ' ', range( 10, $edition_int ) ) );
		}

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'IDENTIFIERS' );

		$isbn_rows = array_values( array_filter( $data['format_rows'], static fn( $r ) => ! empty( $r->isbn13 ) ) );
		if ( ! empty( $isbn_rows ) ) {
			foreach ( $isbn_rows as $fr ) {
				// H-002: spec §4b row 10+ — value = '{format_label}: {isbn13}'; label col = 'ISBN'.
				$fl  = ! empty( $fr->format_label ) ? (string) $fr->format_label : '';
				$val = '' !== $fl ? $fl . ': ' . (string) $fr->isbn13 : (string) $fr->isbn13;
				$this->lbl( $b, $s, $row++, 'ISBN', $val, 1, 7, true );
			}
		} else {
			$this->lbl( $b, $s, $row++, 'ISBN', '', 1, 7, true );
		}

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'LIBRARY OF CONGRESS' );
		// H-003: spec §4b row 11 — value = 'Library of Congress Control Number: {lccn}'.
		$lccn_val = '' !== (string) $data['lccn']
			? 'Library of Congress Control Number: ' . (string) $data['lccn']
			: '';
		$this->lbl( $b, $s, $row++, 'LCCN', $lccn_val );

		// CIP block: inverted author name + birth year, title/byline, ISBNs.
		$cip_lines = [];
		if ( ! empty( $data['author_data'] ) ) {
			$a     = $data['author_data'][0];
			$inv   = '' !== $a['name_inv'] ? $a['name_inv'] : $a['name'];
			$byear = $a['birth_year'];
			$cip_lines[] = $inv . ( '' !== $byear ? ', ' . $byear . "\u{2014}" : "\u{2014}" );
		}
		if ( '' !== (string) $data['title'] ) {
			$byline      = ! empty( $data['author_data'] ) ? ' / ' . $data['author_data'][0]['name'] : '';
			$cip_lines[] = '  ' . $data['title'] . $byline . '.';
		}
		foreach ( $isbn_rows as $fr ) {
			$fl          = ! empty( $fr->format_label ) ? (string) $fr->format_label : 'ISBN';
			$cip_lines[] = '  ISBN ' . (string) $fr->isbn13 . ' (' . $fl . ')';
		}
		$this->lbl( $b, $s, $row++, 'CIP Block', implode( "\n", $cip_lines ), 1, 7, false, true );

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'PRODUCTION CREDITS' );

		if ( ! empty( $data['contributors'] ) ) {
			foreach ( $data['contributors'] as $c ) {
				// H-004: spec §4b row 13+ — value = '{role_label}: {contributor_name}'.
				$role  = ! empty( $c->role_label ) ? (string) $c->role_label : 'Contributor';
				$cname = (string) ( $c->contributor_name ?? '' );
				$this->lbl( $b, $s, $row++, 'Contributor', $role . ': ' . $cname );
			}
		} else {
			$this->sp( $b, $s, $row++ );
		}

		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'DISCLAIMER' );
		$this->lbl( $b, $s, $row++, 'Disclaimer', (string) ( $data['pub_disclaimer'] ?? '' ), 1, 7, false, true );

		$this->sp( $b, $s, $row++ );
		$country = '' !== (string) ( $data['pub_country'] ?? '' )
			? (string) $data['pub_country']
			: 'United States of America';
		$this->lbl( $b, $s, $row, 'Country', 'Printed in the ' . $country );
	}

	/**
	 * Tab 8: EPUB OPF Metadata (PUB tier only).
	 *
	 * Outputs a ready-to-paste XML <metadata> block for EPUB 2.0.1 content.opf,
	 * followed by a field-by-field reference table for verification.
	 *
	 * @param MBS_Book_Bible_Builder $b    Builder instance.
	 * @param array                  $data Gathered book data.
	 * @return void
	 * @since 2.5.0
	 */
	private function build_tab8( MBS_Book_Bible_Builder $b, array $data ): void {
		$s = $b->add_sheet( 'EPUB OPF Metadata' );
		$this->colophon( $b, $s, $data );
		$row = 3;

		// ---- FIELD REFERENCE at top (easier to use while filling in the XML) ----
		$this->hdr( $b, $s, $row++, 'FIELD REFERENCE' );

		// Reference table header.
		$b->set_cell( $s, $row, 0, 'OPF Element', 2 );
		$b->set_cell( $s, $row, 1, 'Value',        2 );
		$b->set_cell( $s, $row, 2, '',             2 );
		$b->set_cell( $s, $row, 3, '',             2 );
		$b->set_cell( $s, $row, 4, 'Source',       2 );
		$b->set_cell( $s, $row, 5, '',             2 );
		$b->set_cell( $s, $row, 6, '',             2 );
		$b->set_cell( $s, $row, 7, '',             2 );
		$row++;

		// Primary ebook format detection.
		$ebook_row = null;
		foreach ( $data['format_rows'] as $fr ) {
			$fmt_str = strtolower( (string) ( $fr->format ?? '' ) . ' ' . (string) ( $fr->format_label ?? '' ) );
			if ( preg_match( '/ebook|epub|kindle|digital/', $fmt_str ) ) {
				$ebook_row = $fr;
				break;
			}
		}
		if ( null === $ebook_row ) {
			foreach ( $data['format_rows'] as $fr ) {
				if ( ! empty( $fr->isbn13 ) ) {
					$ebook_row = $fr;
					break;
				}
			}
		}

		// Language: convert ISO 639-2/B to BCP 47.
		$lang_map = array(
			'eng' => 'en', 'fre' => 'fr', 'ger' => 'de', 'spa' => 'es',
			'ita' => 'it', 'por' => 'pt', 'dut' => 'nl', 'swe' => 'sv',
			'nor' => 'no', 'dan' => 'da', 'fin' => 'fi', 'pol' => 'pl',
			'rus' => 'ru', 'jpn' => 'ja', 'zho' => 'zh', 'ara' => 'ar',
			'kor' => 'ko', 'hin' => 'hi',
		);
		$lang_raw  = (string) ( $data['language'] ?? 'eng' );
		$lang_bcp  = $lang_map[ $lang_raw ] ?? $lang_raw;

		$author_names = array_map( static fn( $a ) => $a['name'], $data['author_data'] );
		$author_str   = implode( ', ', $author_names );

		$ebook_isbn   = $ebook_row ? (string) ( $ebook_row->isbn13 ?? '' ) : '';
		$epub_uuid    = (string) ( $data['epub_uuid'] ?? '' );
		$export_date  = gmdate( 'Y-m-d' );

		// Reference rows.
		$this->epub_ref_row( $b, $s, $row++, 'cover meta',           'cover.jpg',     'Conventional cover manifest id — verify against your EPUB manifest',    false );
		$has_series = '' !== (string) $data['series_name'];
		if ( $has_series ) {
			$this->epub_ref_row( $b, $s, $row++, 'meta series',       (string) $data['series_name'], 'mbs_series (taxonomy)',                                  false );
		}
		$this->epub_ref_row( $b, $s, $row++, 'dc:language',          $lang_bcp,       'mbs_language (ISO 639-2/B) → BCP 47',                           '' === $lang_bcp );
		// H-001: UUID is always amber — spec §5h 'never generated by MBS'.
		$this->epub_ref_row( $b, $s, $row++, 'dc:identifier (UUID)', '',            'Always amber — paste your EPUB UUID here; must match unique-identifier on <package> element and toc.ncx', true );

		if ( '' !== $ebook_isbn ) {
			$isbn10 = MBS_ISBN_Helper::isbn13_to_isbn10( $ebook_isbn );
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier ISBN',   $ebook_isbn,               'Primary ebook format row isbn13',                       false );
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier EISBN',  $ebook_isbn,               'Primary ebook format row isbn13',                       false );
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier ISBN-13', $ebook_isbn,              'Primary ebook format row isbn13',                       false );
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier ISBN-10', $isbn10 ? $isbn10 : '(979-prefix: no ISBN-10)', 'Derived from ISBN-13',           false );
		} else {
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier ISBN',   '',                        'No isbn13 found on any format row',                     true );
		}

		// Additional format rows (non-ebook).
		foreach ( $data['format_rows'] as $fr ) {
			if ( $fr === $ebook_row || empty( $fr->isbn13 ) ) {
				continue;
			}
			$fl = ! empty( $fr->format_label ) ? (string) $fr->format_label : 'Format';
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier ' . $fl . ' ISBN', (string) $fr->isbn13, $fl . ' format row isbn13', false );
		}

		$this->epub_ref_row( $b, $s, $row++, 'dc:title',            (string) $data['title'],    'post_title',                                                '' === (string) $data['title'] );
		$desc = '' !== (string) $data['synopsis'] ? (string) $data['synopsis'] : (string) $data['teaser'];
		$desc = preg_replace( '/\*{1,2}([^*]+)\*{1,2}/', '$1', $desc );
		$desc = preg_replace( '/_([^_]+)_/', '$1', $desc );
		$desc = preg_replace( '/^[-*+]\s+/m', '', $desc );
		$desc = preg_replace( '/^#+\s+/m', '', $desc );
		$desc = trim( $desc );
		$this->epub_ref_row( $b, $s, $row++, 'dc:description',      $desc,                      'mbs_synopsis (fallback: mbs_teaser)',                       false );
		$this->epub_ref_row( $b, $s, $row++, 'dc:publisher',        (string) $data['publisher'], 'mbs_publisher',                                            false );
		$this->epub_ref_row( $b, $s, $row++, 'dc:contributor bkp',  (string) $data['publisher'], 'Publisher as book producer (bkp)',                         false );
		$this->epub_ref_row( $b, $s, $row++, 'dc:creator aut',      $author_str,                'MBS_TAX_AUTHOR terms (display-order names)',                '' === $author_str );

		foreach ( $data['contributors'] as $c ) {
			if ( 'A01' === strtoupper( (string) ( $c->onix_role_code ?? '' ) ) ) {
				continue;
			}
			$marc = MBS_Contributor_Book_DAO::get_marc_role( (string) ( $c->onix_role_code ?? 'Z01' ) );
			$cname = (string) ( $c->contributor_name ?? '' );
			$this->epub_ref_row( $b, $s, $row++, 'dc:contributor ' . $marc, $cname, 'MBS_Contributor_Book_DAO (onix_role_code → marc)', false );
		}

		$cr_year = (string) $data['copyright_year'];
		$rights  = '' !== $cr_year && '' !== $author_str
			? "Copyright \u{00A9} {$cr_year} by {$author_str}. All rights reserved."
			: '';
		$this->epub_ref_row( $b, $s, $row++, 'dc:rights',           $rights,                    'mbs_copyright_year + MBS_TAX_AUTHOR',                      false );
		$this->epub_ref_row( $b, $s, $row++, 'dc:date publication', (string) $data['pub_date'], 'mbs_pub_date',             false );
		$this->epub_ref_row( $b, $s, $row++, 'dc:date modification', $export_date,              'Generated at export (today)',                               false );

		foreach ( $data['bisac_codes'] as $code ) {
			if ( '' === (string) $code ) { continue; }
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier BISAC', (string) $code, 'mbs_bisac_codes', false );
		}
		foreach ( $data['bic_codes'] as $bic ) {
			$code = is_array( $bic ) ? ( $bic['bic_code'] ?? '' ) : (string) $bic;
			if ( '' === (string) $code ) { continue; }
			// H-005: skip Thema qualifier codes (numeric prefix) in BIC output.
			if ( isset( $code[0] ) && ctype_digit( $code[0] ) ) { continue; }
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier BIC', (string) $code, 'Derived from THEMA via MBS_Classification_DAO', false );
		}
		foreach ( $data['thema_codes'] as $code ) {
			if ( '' === (string) $code ) { continue; }
			$this->epub_ref_row( $b, $s, $row++, 'dc:identifier Thema', (string) $code, 'mbs_thema_codes', false );
		}
		foreach ( $data['genre_terms'] as $term ) {
			if ( '' === (string) $term ) { continue; }
			$this->epub_ref_row( $b, $s, $row++, 'dc:subject', (string) $term, 'MBS_TAX_GENRE', false );
		}
		foreach ( $data['tag_terms'] as $term ) {
			if ( '' === (string) $term ) { continue; }
			$this->epub_ref_row( $b, $s, $row++, 'dc:subject', (string) $term, 'MBS_TAX_TAG', false );
		}

		// ---- OPF XML block below field reference ------------------------------
		$this->sp( $b, $s, $row++ );
		$this->hdr( $b, $s, $row++, 'EPUB 2 OPF METADATA — READY TO PASTE INTO content.opf' );
		$instruction = 'Copy the XML block below and replace the existing <metadata>...</metadata> '
			. 'section in your EPUB content.opf file. Verify the UUID matches the unique-identifier '
			. 'attribute on your <package> element and the dtb:uid in toc.ncx.';
		$this->fill( $b, $s, $row++, 0, 7, $instruction, 1, true );

		// Emit the OPF XML one line per row — no blank separator rows in the XML area.
		$opf_lines     = $this->assemble_epub_opf_xml( $data );
		$in_uuid_block = false;
		foreach ( $opf_lines as $opf_line ) {
			if ( '' === trim( $opf_line ) ) {
				$in_uuid_block = false;
				continue; // skip blank separator lines — no spacer rows in the XML area
			}
			$t = ltrim( $opf_line );
			// Detect the UUID dc:identifier block — amber from open to close tag.
			if ( 0 === strpos( $t, '<dc:identifier id="BookId"' ) ) {
				$in_uuid_block = true;
			}
			if ( 0 === strpos( $t, '<!--' ) ) {
				$line_style = 1; // attribution gray — comment lines
			} elseif ( 0 === strpos( $t, '<metadata' ) || 0 === strpos( $t, '</metadata' ) ) {
				$line_style = 3; // label gray — outer wrapper tags
			} elseif ( $in_uuid_block || false !== strpos( $opf_line, '[PASTE' ) ) {
				$line_style = 5; // amber — UUID placeholder block
			} else {
				$line_style = 4; // data — all other elements
			}
			$this->fill( $b, $s, $row++, 0, 7, $opf_line, $line_style );
			if ( $in_uuid_block && 0 === strpos( $t, '</dc:identifier>' ) ) {
				$in_uuid_block = false;
			}
		}
	}

	/**
	 * Assemble the EPUB 2 OPF <metadata> XML block.
	 *
	 * All dynamic values are escaped with ENT_XML1 | ENT_QUOTES.
	 * © symbol (U+00A9) is used directly — valid in UTF-8 XML.
	 *
	 * @param  array  $data Gathered book data.
	 * @return string[]     Lines of the <metadata>...</metadata> XML block.
	 * @since  2.5.0
	 */
	private function assemble_epub_opf_xml( array $data ): array {
		$xe = static fn( string $v ): string => htmlspecialchars( $v, ENT_XML1 | ENT_QUOTES, 'UTF-8' );

		$lang_map = array(
			'eng' => 'en', 'fre' => 'fr', 'ger' => 'de', 'spa' => 'es',
			'ita' => 'it', 'por' => 'pt', 'dut' => 'nl', 'swe' => 'sv',
			'nor' => 'no', 'dan' => 'da', 'fin' => 'fi', 'pol' => 'pl',
			'rus' => 'ru', 'jpn' => 'ja', 'zho' => 'zh', 'ara' => 'ar',
			'kor' => 'ko', 'hin' => 'hi',
		);
		$lang_raw = (string) ( $data['language'] ?? 'eng' );
		$lang_bcp = $lang_map[ $lang_raw ] ?? $lang_raw;

		// Primary ebook format.
		$ebook_row = null;
		foreach ( $data['format_rows'] as $fr ) {
			$fmt_str = strtolower( (string) ( $fr->format ?? '' ) . ' ' . (string) ( $fr->format_label ?? '' ) );
			if ( preg_match( '/ebook|epub|kindle|digital/', $fmt_str ) ) {
				$ebook_row = $fr;
				break;
			}
		}
		if ( null === $ebook_row ) {
			foreach ( $data['format_rows'] as $fr ) {
				if ( ! empty( $fr->isbn13 ) ) {
					$ebook_row = $fr;
					break;
				}
			}
		}

		$author_names = array_map( static fn( $a ) => $a['name'], $data['author_data'] );
		$author_str   = implode( ', ', $author_names );
		$ebook_isbn   = $ebook_row ? (string) ( $ebook_row->isbn13 ?? '' ) : '';
		$epub_uuid    = (string) ( $data['epub_uuid'] ?? '' );
		$cr_year      = (string) $data['copyright_year'];
		$export_date  = gmdate( 'Y-m-d' );
		$desc         = '' !== (string) $data['synopsis'] ? (string) $data['synopsis'] : (string) $data['teaser'];
		// M-002: strip common Markdown formatting before embedding in XML plain-text.
		$desc = preg_replace( '/\*{1,2}([^*]+)\*{1,2}/', '$1', $desc );
		$desc = preg_replace( '/_([^_]+)_/', '$1', $desc );
		$desc = preg_replace( '/^[-*+]\s+/m', '', $desc );
		$desc = preg_replace( '/^#+\s+/m', '', $desc );
		$desc = trim( $desc );

		$lines   = [];
		$lines[] = '<metadata xmlns:opf="http://www.idpf.org/2007/opf"';
		$lines[] = '          xmlns:dc="http://purl.org/dc/elements/1.1/">';
		$lines[] = '';
		$lines[] = '  <!-- Cover image reference -->'; 
		$lines[] = '  <meta name="cover" content="cover.jpg" />';
		$lines[] = '';

		// Series (omit if absent).
		if ( '' !== (string) $data['series_name'] ) {
			$lines[] = '  <meta content="' . $xe( (string) $data['series_name'] ) . '" name="series" />';
			$lines[] = '';
		}

		$lines[] = '  <dc:language>' . $xe( $lang_bcp ) . '</dc:language>';
		$lines[] = '';

		// UUID identifier — per spec §5h always amber placeholder.
		// MBS never generates a UUID; the user pastes their own from their EPUB tool.
		$lines[] = '  <dc:identifier id="BookId" opf:scheme="UUID">';
		$lines[] = '    [PASTE YOUR EPUB UUID HERE — must match unique-identifier on package element and toc.ncx]';
		$lines[] = '  </dc:identifier>';
		$lines[] = '';

		// ISBNs for primary ebook format.
		if ( '' !== $ebook_isbn ) {
			$isbn10 = MBS_ISBN_Helper::isbn13_to_isbn10( $ebook_isbn );
			$lines[] = '  <dc:identifier opf:scheme="ISBN">'    . $xe( $ebook_isbn ) . '</dc:identifier>';
			$lines[] = '  <dc:identifier opf:scheme="EISBN">'   . $xe( $ebook_isbn ) . '</dc:identifier>';
			$lines[] = '  <dc:identifier opf:scheme="ISBN-13">' . $xe( $ebook_isbn ) . '</dc:identifier>';
			if ( false !== $isbn10 ) {
				$lines[] = '  <dc:identifier opf:scheme="ISBN-10">' . $xe( $isbn10 ) . '</dc:identifier>';
			}
			$lines[] = '';
		}

		// Additional format ISBNs.
		foreach ( $data['format_rows'] as $fr ) {
			if ( $fr === $ebook_row || empty( $fr->isbn13 ) ) {
				continue;
			}
			// M-001: use actual format_label; fall back to neutral 'Format' per spec §5c.
			$fl      = ! empty( $fr->format_label ) ? (string) $fr->format_label : 'Format';
			$lines[] = '  <dc:identifier opf:scheme="' . $xe( $fl ) . ' ISBN">' . $xe( (string) $fr->isbn13 ) . '</dc:identifier>';
		}
		if ( ! empty( array_filter( $data['format_rows'], static fn( $r ) => $r !== $ebook_row && ! empty( $r->isbn13 ) ) ) ) {
			$lines[] = '';
		}

		$lines[] = '  <dc:title>' . $xe( (string) $data['title'] ) . '</dc:title>';
		$lines[] = '';

		if ( '' !== $desc ) {
			$lines[] = '  <dc:description>' . $xe( $desc ) . '</dc:description>';
			$lines[] = '';
		}

		$lines[] = '  <dc:publisher>' . $xe( (string) $data['publisher'] ) . '</dc:publisher>';
		$lines[] = '  <dc:contributor opf:role="bkp">' . $xe( (string) $data['publisher'] ) . '</dc:contributor>';
		$lines[] = '';

		if ( '' !== $author_str ) {
			$lines[] = '  <dc:creator opf:file-as="' . $xe( $author_str ) . '" opf:role="aut">' . $xe( $author_str ) . '</dc:creator>';
			$lines[] = '  <dc:contributor opf:role="aut">' . $xe( $author_str ) . '</dc:contributor>';
			$lines[] = '';
		}

		// Additional contributors (A01 excluded — already in dc:creator block).
		foreach ( $data['contributors'] as $c ) {
			if ( 'A01' === strtoupper( (string) ( $c->onix_role_code ?? '' ) ) ) {
				continue;
			}
			$marc  = MBS_Contributor_Book_DAO::get_marc_role( (string) ( $c->onix_role_code ?? 'Z01' ) );
			$cname = (string) ( $c->contributor_name ?? '' );
			if ( '' === $cname ) {
				continue;
			}
			$lines[] = '  <dc:contributor opf:file-as="' . $xe( $cname ) . '" opf:role="' . $xe( $marc ) . '">' . $xe( $cname ) . '</dc:contributor>';
		}
		if ( ! empty( $data['contributors'] ) ) {
			$lines[] = '';
		}

		// Rights.
		if ( '' !== $cr_year && '' !== $author_str ) {
			$lines[] = '  <dc:rights>Copyright © ' . $xe( $cr_year ) . ' by ' . $xe( $author_str ) . '. All rights reserved.</dc:rights>';
			$lines[] = '';
		}

		$lines[] = '  <dc:date opf:event="publication">' . $xe( (string) $data['pub_date'] ) . '</dc:date>';
		$lines[] = '  <dc:date opf:event="modification">' . $xe( $export_date ) . '</dc:date>';
		$lines[] = '';

		// Classification codes.
		foreach ( $data['bisac_codes'] as $code ) {
			if ( '' !== (string) $code ) {
				$lines[] = '  <dc:identifier opf:scheme="BISAC">' . $xe( (string) $code ) . '</dc:identifier>';
			}
		}
		foreach ( $data['bic_codes'] as $bic ) {
			$code = is_array( $bic ) ? ( $bic['bic_code'] ?? '' ) : (string) $bic;
			if ( '' === (string) $code ) { continue; }
			// H-005: Thema qualifiers start with a digit — not valid BIC codes.
			if ( isset( $code[0] ) && ctype_digit( $code[0] ) ) { continue; }
			$lines[] = '  <dc:identifier opf:scheme="BIC">' . $xe( (string) $code ) . '</dc:identifier>';
		}
		foreach ( $data['thema_codes'] as $code ) {
			if ( '' !== (string) $code ) {
				$lines[] = '  <dc:identifier opf:scheme="Thema">' . $xe( (string) $code ) . '</dc:identifier>';
			}
		}
		$has_class = array_filter( $data['bisac_codes'], static fn( $c ) => '' !== (string) $c )
			|| array_filter( $data['bic_codes'] )
			|| array_filter( $data['thema_codes'], static fn( $c ) => '' !== (string) $c );
		if ( $has_class ) {
			$lines[] = '';
		}

		// Subjects (genre + tag terms).
		foreach ( array_merge( (array) $data['genre_terms'], (array) $data['tag_terms'] ) as $term ) {
			if ( '' !== (string) $term ) {
				$lines[] = '  <dc:subject>' . $xe( (string) $term ) . '</dc:subject>';
			}
		}

		$lines[] = '';
		$lines[] = '</metadata>';

		return $lines;
	}

	/**
	 * Write one row of the EPUB OPF reference table in Tab 8.
	 *
	 * Layout: col 0 = OPF element (style 3), cols 1-3 = value (style 4/5),
	 *         cols 4-7 = source note (style 1).
	 *
	 * @param MBS_Book_Bible_Builder $b      Builder instance.
	 * @param int                    $s      Sheet index.
	 * @param int                    $row    0-based row number.
	 * @param string                 $elem   OPF element name.
	 * @param string                 $value  Resolved value.
	 * @param string                 $source Human-readable source note.
	 * @param bool                   $amber  Whether to use amber style for empty/special values.
	 * @return void
	 * @since 2.5.0
	 */
	private function epub_ref_row( MBS_Book_Bible_Builder $b, int $s, int $row, string $elem, string $value, string $source, bool $amber ): void {
		$ds = ( $amber || '' === $value ) && $amber ? 5 : 4;
		$b->set_cell( $s, $row, 0, $elem,   3 );
		$b->set_cell( $s, $row, 1, $value,  $ds, 's', static::r( $row, 1, 3 ) );
		$b->set_cell( $s, $row, 2, '',      $ds );
		$b->set_cell( $s, $row, 3, '',      $ds );
		$b->set_cell( $s, $row, 4, $source, 1,   's', static::r( $row, 4, 7 ) );
		$b->set_cell( $s, $row, 5, '',      1 );
		$b->set_cell( $s, $row, 6, '',      1 );
		$b->set_cell( $s, $row, 7, '',      1 );
	}
}
