<?php
/**
 * File Path: includes/api/class-mbs-onix-field-map.php
 *
 * ONIX 3.0 Field Map -- My Book Showroom
 *
 * Static lookup tables for ONIX 3.0 short-tag export and import:
 *   - MBS format_slug -> ProductForm / ProductFormDetail codes
 *   - ONIX ProductForm codes -> MBS format_slug (import reverse map)
 *   - TextType codes for post_content / mbs_teaser / mbs_toc
 *   - PublishingStatus codes from post_status
 *   - ContributorRole codes for each contributor type
 *   - ISO 639-2/B language code validation helper
 *
 * Spec: SPRINT-ONIX-EXPORT-IMPORT-Spec.md §5, §6
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Api
 * @since      1.1.0
 */

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

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

	// -------------------------------------------------------------------------
	// ProductForm mapping (export)
	// -------------------------------------------------------------------------

	/**
	 * Map MBS format_slug to ONIX ProductForm + optional ProductFormDetail code.
	 *
	 * Returns array{ form: string, detail: string } where detail may be empty.
	 *
	 * @param string $format_slug MBS format slug (e.g. 'hardcover', 'ebook').
	 * @return array{ form: string, detail: string }
	 */
	public static function slug_to_product_form( string $format_slug ): array {
		$map = array(
			'hardcover'    => array( 'form' => 'BB', 'detail' => '' ),
			'paperback'    => array( 'form' => 'BC', 'detail' => 'B102' ),
			'ebook'        => array( 'form' => 'ED', 'detail' => '' ),
			'audiobook'    => array( 'form' => 'AJ', 'detail' => '' ),
			'large-print'  => array( 'form' => 'BC', 'detail' => 'B305' ),
			'board-book'   => array( 'form' => 'BH', 'detail' => '' ),
			'spiral-bound' => array( 'form' => 'PB', 'detail' => '' ),
		);
		return $map[ $format_slug ] ?? array( 'form' => 'BB', 'detail' => '' );
	}

	/**
	 * Check whether a format slug has a known ONIX mapping.
	 *
	 * @param string $format_slug
	 * @return bool
	 */
	public static function slug_is_known( string $format_slug ): bool {
		$known = array( 'hardcover', 'paperback', 'ebook', 'audiobook', 'large-print', 'board-book', 'spiral-bound' );
		return in_array( $format_slug, $known, true );
	}

	// -------------------------------------------------------------------------
	// ProductForm reverse map (import)
	// -------------------------------------------------------------------------

	/**
	 * Map ONIX ProductForm + optional ProductFormDetail to MBS format_slug.
	 *
	 * @param string $product_form   ONIX ProductForm code (e.g. 'BB').
	 * @param string $product_detail ONIX ProductFormDetail code (may be empty).
	 * @return string MBS format slug; 'hardcover' as safe default for unknown codes.
	 */
	public static function product_form_to_slug( string $product_form, string $product_detail = '' ): string {
		// Detail-qualified lookups first (more specific).
		if ( 'BC' === $product_form && 'B102' === $product_detail ) {
			return 'paperback';
		}
		if ( 'BC' === $product_form && 'B305' === $product_detail ) {
			return 'large-print';
		}

		// Form-only fallback map.
		$map = array(
			'BB' => 'hardcover',
			'BC' => 'paperback',
			'ED' => 'ebook',
			'AJ' => 'audiobook',
			'BH' => 'board-book',
			'PB' => 'spiral-bound',
		);
		return $map[ $product_form ] ?? 'hardcover';
	}

	/**
	 * Check whether a ProductForm code is in the known mapping.
	 *
	 * @param string $product_form
	 * @return bool
	 */
	public static function product_form_is_known( string $product_form ): bool {
		$known = array( 'BB', 'BC', 'ED', 'AJ', 'BH', 'PB' );
		return in_array( $product_form, $known, true );
	}

	// -------------------------------------------------------------------------
	// TextType codes
	// -------------------------------------------------------------------------

	/**
	 * ONIX TextType code for a given MBS postmeta key.
	 *
	 * 02 = Short description / annotation  (mbs_teaser)
	 * 03 = Description                     (post_content)
	 * 04 = Table of contents               (mbs_toc)
	 * 23 = Back cover copy / flap copy     (mbs_synopsis, PRO)
	 *
	 * @param string $field_key MBS field key.
	 * @return string ONIX TextType code; empty string if no mapping.
	 */
	public static function text_type_for_field( string $field_key ): string {
		$map = array(
			'post_content' => '03',
			'mbs_teaser'   => '02',
			'mbs_toc'      => '04',
			'mbs_synopsis' => '23', // Back cover copy / flap copy (PRO).
		);
		return $map[ $field_key ] ?? '';
	}

	// -------------------------------------------------------------------------
	// Publishing status
	// -------------------------------------------------------------------------

	/**
	 * Map WordPress post_status + mbs_lifecycle_state to ONIX PublishingStatus code.
	 *
	 * 02 = Forthcoming          (preorder | coming_soon | draft | non-published)
	 * 04 = Active               (publish with no lifecycle override)
	 * 06 = Out of stock         (out_of_stock)
	 * 07 = Out of print         (out_of_print)
	 * 08 = Inactive             (discontinued)
	 *
	 * Lifecycle state takes priority over post_status. Mapping is book-level:
	 * all format rows for a book receive the same PublishingStatus.
	 *
	 * @param string $post_status    WordPress post_status value.
	 * @param string $lifecycle_state Book-level mbs_lifecycle_state value.
	 * @return string ONIX PublishingStatus code.
	 */
	public static function publishing_status( string $post_status, string $lifecycle_state = '' ): string {
		if ( 'out_of_print' === $lifecycle_state ) {
			return '07'; // Out of print
		}
		if ( 'out_of_stock' === $lifecycle_state ) {
			return '06'; // Out of stock indefinitely
		}
		if ( 'discontinued' === $lifecycle_state ) {
			return '08'; // Inactive
		}
		if ( 'preorder' === $lifecycle_state || 'coming_soon' === $lifecycle_state ) {
			return '02'; // Forthcoming
		}
		if ( 'draft' === $lifecycle_state ) {
			return '02'; // Forthcoming
		}
		return 'publish' === $post_status ? '04' : '02';
	}

	// -------------------------------------------------------------------------
	// ContributorRole codes
	// -------------------------------------------------------------------------

	/**
	 * ONIX ContributorRole code for a given contributor type.
	 *
	 * A01 = By (author)
	 * B01 = Edited by
	 * A12 = Illustrated by
	 * E07 = Read by (narrator/audiobook)
	 *
	 * @param string $contributor_type  'author' | 'editor' | 'illustrator' | 'narrator'
	 * @return string ONIX ContributorRole code; 'A01' as default.
	 */
	public static function contributor_role( string $contributor_type ): string {
		$map = array(
			'author'      => 'A01',
			'editor'      => 'B01',
			'illustrator' => 'A12',
			'narrator'    => 'E07',
		);
		return $map[ $contributor_type ] ?? 'A01';
	}

	// -------------------------------------------------------------------------
	// Language code validation
	// -------------------------------------------------------------------------

	/**
	 * Validate an ISO 639-2/B three-letter language code.
	 * Accepts any lowercase three-letter alphabetic string (broad allow-list).
	 *
	 * @param string $code
	 * @return bool
	 */
	public static function is_valid_language_code( string $code ): bool {
		return (bool) preg_match( '/^[a-z]{3}$/', $code );
	}

	/**
	 * Sanitize and validate a language code; return 'eng' as fallback.
	 *
	 * @param string $raw
	 * @return string
	 */
	public static function sanitize_language_code( string $raw ): string {
		$clean = strtolower( sanitize_text_field( $raw ) );
		// ISO 639-1 (2-letter) to ISO 639-2/B (3-letter) auto-conversion.
		static $iso_map = array(
			'en' => 'eng', 'es' => 'spa', 'fr' => 'fre', 'de' => 'ger',
			'it' => 'ita', 'pt' => 'por', 'ru' => 'rus', 'zh' => 'chi',
			'ja' => 'jpn', 'ko' => 'kor', 'ar' => 'ara', 'nl' => 'dut',
			'pl' => 'pol', 'sv' => 'swe', 'no' => 'nor', 'da' => 'dan',
			'fi' => 'fin', 'cs' => 'cze', 'hu' => 'hun', 'ro' => 'rum',
			'tr' => 'tur', 'he' => 'heb', 'hi' => 'hin',
		);
		if ( isset( $iso_map[ $clean ] ) ) {
			return $iso_map[ $clean ];
		}
		return self::is_valid_language_code( $clean ) ? $clean : 'eng';
	}

	// -------------------------------------------------------------------------
	// NotificationType
	// -------------------------------------------------------------------------

	/**
	 * ONIX NotificationType for standard catalog submissions.
	 * 03 = Notification confirmed on publication.
	 *
	 * @return string
	 */
	public static function notification_type(): string {
		return '03';
	}

	// -------------------------------------------------------------------------
	// ONIX PublishingStatus import mapping
	// -------------------------------------------------------------------------

	/**
	 * Map an ONIX PublishingStatus code to an MBS lifecycle_state string.
	 *
	 * Used by the reconciler to resolve per-format pub_status codes from
	 * imported ONIX files into a single book-level mbs_lifecycle_state value.
	 *
	 * 02/03 = Forthcoming / Postponed -> preorder
	 * 04    = Active                  -> '' (published, empty = default)
	 * 06    = Out of stock            -> out_of_stock
	 * 07    = Out of print            -> out_of_print
	 * 08/11 = Inactive / Withdrawn    -> discontinued
	 * other = Unrecognized            -> draft (ensures record appears in MBS)
	 *
	 * @param string $pub_status ONIX PublishingStatus code.
	 * @return string mbs_lifecycle_state value.
	 */
	public static function onix_code_to_lifecycle_state( string $pub_status ): string {
		$map = array(
			'02' => 'preorder',
			'03' => 'preorder',      // Postponed indefinitely -> treat as preorder
			'04' => '',              // Active -> published (empty = default)
			'06' => 'out_of_stock',
			'07' => 'out_of_print',
			'08' => 'discontinued',
			'11' => 'discontinued',  // Withdrawn from sale
		);
		return $map[ $pub_status ] ?? 'draft';
	}

	/**
	 * Return the priority weight for a lifecycle_state string.
	 *
	 * Higher weight = more active / alive. Used by the reconciler to resolve
	 * conflicting per-format pub_status codes when importing multi-format books:
	 * the lifecycle state with the highest weight wins.
	 *
	 *   5 = '' (published/active)  -- most alive
	 *   4 = preorder
	 *   3 = out_of_stock
	 *   2 = out_of_print
	 *   1 = discontinued
	 *   0 = draft               -- least alive, also the fallback for unknowns
	 *
	 * @param string $lifecycle_state mbs_lifecycle_state value.
	 * @return int Priority weight (0-5).
	 */
	public static function lifecycle_state_priority( string $lifecycle_state ): int {
		$weights = array(
			''             => 5,
			'preorder'     => 4,
			'coming_soon'  => 4,
			'out_of_stock' => 3,
			'out_of_print' => 2,
			'discontinued' => 1,
			'draft'        => 0,
		);
		return $weights[ $lifecycle_state ] ?? 0;
	}
}
