<?php
/**
 * File Path: includes/export/class-mbs-book-bible-field-counter.php
 *
 * Book Bible Field Counter
 *
 * Dynamic completeness scorer for the Export Book Bible metabox.
 * Uses a three-bucket model so the denominator reflects only fields
 * that are actually applicable to the book:
 *
 *   Mandatory   — always in denominator; missing = gap.
 *   Conditional — enters denominator only when its prerequisite exists
 *                 (e.g. Series fields only when book is in a series).
 *   Optional    — enters denominator only when filled; filling scores
 *                 +1 to both numerator and denominator (no penalty for
 *                 leaving blank).
 *
 * PUB-only per-format fields (F8–F12) are excluded from the denominator
 * for PRO users.
 *
 * No cover-image byte fetching, no classification DAO lookups beyond
 * get_book_codes, no THEMA/BIC mapping. DB reads limited to
 * get_post_meta, wp_get_object_terms, and DAO::get_by_post calls.
 *
 * @package My_Book_Showroom
 * @since   2.5.0
 */

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

/**
 * Class MBS_Book_Bible_Field_Counter
 *
 * @since 2.5.0
 */
class MBS_Book_Bible_Field_Counter { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Count populated and possible fields for a book post.
	 *
	 * Returns an array with keys 'populated' and 'possible'.
	 * Score = round( populated / possible * 100 ).
	 *
	 * Bucket rules:
	 *   - Mandatory:   $possible++; $populated++ if filled.
	 *   - Conditional: same as Mandatory, but only when $condition is true.
	 *   - Optional:    $possible++ AND $populated++ only when filled.
	 *
	 * @param int $post_id Book post ID.
	 * @return array{populated: int, possible: int}
	 * @since 2.5.0
	 */
	public static function count( int $post_id ): array {
		$populated = 0;
		$possible  = 0;

		// ── Lightweight data helpers ───────────────────────────────────────
		$meta = static fn( string $key ): string => (string) get_post_meta( $post_id, $key, true );
		$terms = static function( string $tax ) use ( $post_id ): int {
			// get_the_terms() handles caching and returns false (not WP_Error) when
			// no terms exist — safe to count without triggering the array_merge
			// TypeError that wp_get_object_terms 'fields'=>'count' can cause.
			$result = get_the_terms( $post_id, $tax );
			return is_array( $result ) ? count( $result ) : 0;
		};
		$json = static function( string $key ) use ( $post_id ): array {
			$raw = get_post_meta( $post_id, $key, true );
			$dec = is_array( $raw ) ? $raw : ( json_decode( (string) $raw, true ) ?: array() );
			return is_array( $dec ) ? $dec : array();
		};

		// ── Bucket closures ────────────────────────────────────────────────

		/**
		 * Mandatory: always enters denominator.
		 *
		 * @param bool $filled Whether the field has a value.
		 */
		$mandatory = static function( bool $filled ) use ( &$populated, &$possible ): void {
			$possible++;
			if ( $filled ) {
				$populated++;
			}
		};

		/**
		 * Conditional: enters denominator only when prerequisite exists.
		 *
		 * @param bool $condition Whether the prerequisite is met.
		 * @param bool $filled    Whether the field has a value.
		 */
		$conditional = static function( bool $condition, bool $filled ) use ( &$populated, &$possible ): void {
			if ( $condition ) {
				$possible++;
				if ( $filled ) {
					$populated++;
				}
			}
		};

		/**
		 * Optional: no penalty for blank; filling scores +1/+1.
		 *
		 * @param bool $filled Whether the field has a value.
		 */
		$optional = static function( bool $filled ) use ( &$populated, &$possible ): void {
			if ( $filled ) {
				$possible++;
				$populated++;
			}
		};

		// ── Publication Identity ───────────────────────────────────────────
		// 1.  Cover Image — Mandatory
		$mandatory( get_post_thumbnail_id( $post_id ) > 0 ); // R5-exempt: export field counter context
		// 2.  Title — Mandatory
		$mandatory( '' !== (string) get_post_field( 'post_title', $post_id ) );
		// 3.  Tagline — Optional
		$optional( '' !== $meta( 'mbs_tagline' ) );
		// 4.  Publisher — Mandatory
		$mandatory( '' !== $meta( 'mbs_publisher' ) );
		// 5.  Imprint — Optional
		$optional( $terms( MBS_TAX_IMPRINT ) > 0 );
		// 6.  Publication Date — Mandatory
		$mandatory( '' !== $meta( 'mbs_pub_date' ) );
		// 7.  Copyright Year — Mandatory
		$mandatory( '' !== $meta( 'mbs_copyright_year' ) );
		// 8.  Series — Conditional (only when book is in a series)
		$in_series = $terms( MBS_TAX_SERIES ) > 0;
		$conditional( $in_series, $in_series );
		// 9.  Series Number — Conditional (same prerequisite as Series)
		$conditional( $in_series, '' !== $meta( 'mbs_series_position' ) );
		// 10. LCCN — Optional
		$optional( '' !== $meta( 'mbs_lccn' ) );

		// ── Marketing ─────────────────────────────────────────────────────
		// 11. Teaser — Mandatory
		$mandatory( '' !== $meta( 'mbs_teaser' ) );
		// 12. Synopsis — Mandatory
		$mandatory( '' !== $meta( 'mbs_synopsis' ) );
		// 13. Genre — Mandatory
		$mandatory( $terms( MBS_TAX_GENRE ) > 0 );

		// ── Classification ────────────────────────────────────────────────
		$bisac  = MBS_Classification_DAO::get_book_codes( $post_id, 'bisac' );
		$thema  = MBS_Classification_DAO::get_book_codes( $post_id, 'thema' );
		$bowker = MBS_Classification_DAO::get_book_codes( $post_id, 'bowker' ); // R2-003
		// 14. BISAC 1 — Mandatory (primary industry code required)
		$mandatory( isset( $bisac[0] ) && '' !== (string) $bisac[0] );
		// 15. BISAC 2 — Optional
		$optional( isset( $bisac[1] ) && '' !== (string) $bisac[1] );
		// 16. BISAC 3 — Optional
		$optional( isset( $bisac[2] ) && '' !== (string) $bisac[2] );
		// 17. THEMA 1 — Mandatory (primary international code required)
		$mandatory( isset( $thema[0] ) && '' !== (string) $thema[0] );
		// 18. THEMA 2 — Optional
		$optional( isset( $thema[1] ) && '' !== (string) $thema[1] );
		// 19. THEMA 3 — Optional
		$optional( isset( $thema[2] ) && '' !== (string) $thema[2] );
		// 20. Bowker 1 — Optional
		$optional( isset( $bowker[0] ) && '' !== (string) $bowker[0] );
		// 21. Bowker 2 — Optional
		$optional( isset( $bowker[1] ) && '' !== (string) $bowker[1] );
		// 22. Audience — Mandatory
		$mandatory( '' !== $meta( 'mbs_audience_code' ) );
		// 23. Age Range — Optional
		$age_from = $meta( 'mbs_age_range_from' );
		$age_to   = $meta( 'mbs_age_range_to' );
		$optional(
			( '' !== $age_from && '0' !== $age_from )
			|| ( '' !== $age_to && '0' !== $age_to )
		);

		// ── Primary Author ────────────────────────────────────────────────
		$author_terms = wp_get_object_terms( $post_id, MBS_TAX_AUTHOR, array( 'fields' => 'all' ) );
		$author_terms = ( is_array( $author_terms ) && ! is_wp_error( $author_terms ) )
			? $author_terms : array();
		$primary = $author_terms[0] ?? null;
		// 24. Author Assigned — Mandatory
		$mandatory( ! empty( $author_terms ) );
		// 25. Author Bio — Mandatory
		$mandatory( $primary && '' !== (string) get_term_meta( $primary->term_id, 'mbs_author_bio', true ) );
		// 26. Author Name Inverted — Optional
		$optional( (bool) ( $primary && '' !== (string) get_term_meta( $primary->term_id, 'mbs_name_inverted', true ) ) );
		// 27. Author Birth Year — Optional
		$optional( (bool) ( $primary && '' !== (string) get_term_meta( $primary->term_id, 'mbs_author_birth_year', true ) ) );

		// ── Supplementary ─────────────────────────────────────────────────
		// 28. Additional Contributor — Optional
		$optional( ! empty( MBS_Contributor_Book_DAO::get_by_post( $post_id ) ) );
		// 29. Buy Link — Mandatory
		$mandatory( ! empty( MBS_Buy_Link_DAO::get_all_for_post( $post_id ) ) );

		// 30. Related Product — Conditional (only scored when related titles exist)
		// Auto-detect via shared taxonomy terms (Series/Author/Collection/Universe),
		// falling back to manual ONIX relation rows.
		$_related_tax_ids = array();
		foreach ( array( MBS_TAX_SERIES, MBS_TAX_AUTHOR, MBS_TAX_COLLECTION, MBS_TAX_UNIVERSE ) as $_rtax ) {
			$_tids = wp_get_object_terms( $post_id, $_rtax, array( 'fields' => 'ids' ) );
			if ( ! is_wp_error( $_tids ) && ! empty( $_tids ) ) {
				$_related_tax_ids[ $_rtax ] = $_tids;
			}
		}
		$_has_related = false;
		if ( ! empty( $_related_tax_ids ) ) {
			$_tax_q = array( 'relation' => 'OR' );
			foreach ( $_related_tax_ids as $_rtax => $_tids ) {
				$_tax_q[] = array( 'taxonomy' => $_rtax, 'field' => 'term_id', 'terms' => $_tids );
			}
			$_rel_posts = get_posts( array( // phpcs:ignore WordPress.DB.SlowDBQuery
				'post_type'      => MBS_POST_TYPE,
				'post_status'    => array( 'publish', 'draft', 'pending', 'future' ),
				'post__not_in'   => array( $post_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- standard WP query pattern; VIP rule does not apply to this plugin
				'posts_per_page' => 1,
				'fields'         => 'ids',
				'tax_query'      => $_tax_q, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- standard WP query pattern; VIP rule does not apply to this plugin
			) );
			$_has_related = ! empty( $_rel_posts );
		}
		if ( ! $_has_related ) {
			$_has_related = ! empty( MBS_Related_Products_DAO::get_for_post( $post_id ) );
		}
		// Condition: at least one related title is linkable from this book.
		$conditional( $_has_related, $_has_related );

		// ── Per-format fields ──────────────────────────────────────────────
		$format_rows = MBS_Format_Row_DAO::get_by_post( $post_id );
		$is_pub      = mbs_is_pub();

		foreach ( $format_rows as $row ) {
			$is_audio = ( 'audiobook' === (string) ( $row->format ?? '' ) );

			// F1: ISBN-13 — Mandatory for all formats; Optional for audiobook
			//     (many audiobook distributors assign their own identifiers).
			if ( $is_audio ) {
				$optional( ! empty( $row->isbn13 ) );
			} else {
				$mandatory( ! empty( $row->isbn13 ) );
			}
			// F2: ASIN — Optional
			$optional( ! empty( $row->asin ) );
			// F3: Subtitle — Optional
			$optional( ! empty( $row->subtitle ) );
			// F4: Page Length — Optional
			$optional( isset( $row->page_length ) && (int) $row->page_length > 0 );
			// F5: Edition Number — Optional
			$optional( ! empty( $row->edition_number ) );
			// F6: Edition Type — Optional
			$optional( ! empty( $row->edition_type ) );
			// F7: Format Pub Date — Mandatory
			$mandatory( ! empty( $row->pub_date ) );

			// F8–F12: PUB-only per-format fields.
			// Excluded from denominator entirely for PRO users.
			if ( $is_pub ) {
				// F8: Currency Code — Mandatory (admin global default always resolves).
				$mandatory( true );
				// F9: On Sale Date — Optional (pub_date is the UI default when blank).
				$optional( ! empty( $row->on_sale_date ) );
				// F10: Returnability — Mandatory
				$mandatory( ! empty( $row->returnability ) );
				// F11: Price Type — Mandatory
				$mandatory( ! empty( $row->price_type ) );
				// F12: Supplier Name — Mandatory
				$mandatory( ! empty( $row->supplier_name ) );
			}
		}

		return array(
			'populated' => $populated,
			'possible'  => $possible,
		);
	}
}
