<?php
/**
 * File Path: includes/compat/class-mbs-woocommerce-bridge.php
 *
 * WooCommerce integration bridge — loaded only when WooCommerce is active.
 *
 * Responsibilities
 * ----------------
 * - create_or_link_product()  : Create/link a virtual WC product for a book.
 * - sync_price_to_product()   : Sync the lowest-priced active format row price
 *                               to the linked WC product on every book save.
 *                               Also sets SKU to the first available ISBN-13.
 * - auto_populate_product()   : Called on save_post_{MBS_POST_TYPE} at
 *                               priority 20 (after format rows are saved at 10).
 *                               Auto-creates the WC product link whenever
 *                               mbs_wc_enabled is true and at least one format
 *                               row has a price, then syncs the price.
 * - suppress_from_shop()      : Exclude MBS-linked products from /shop/ archive.
 * - get_product_for_book()    : Return WC_Product for a book, or null.
 * - register_book_meta()      : Expose mbs_book_id on WC product via REST API
 *                               so FooSales and Square can trace a WC product
 *                               back to its MBS book record.
 *
 * Security note
 * -------------
 * No financial data passes through this bridge.  mbs_wc_product_id is a
 * product reference only; all payment flows use WooCommerce directly.
 * WC webhook handlers are out of scope.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Compat
 * @since      1.1.0 (price sync + POS hooks added 1.1.9)
 */

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

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

	// =========================================================================
	// Product create / link
	// =========================================================================

	/**
	 * Create a virtual WooCommerce product linked to the given book post, or
	 * return the ID of an existing linked product.
	 *
	 * The product is set to virtual + catalog_visibility hidden so it carries
	 * no shipping logic and does not appear in the /shop/ archive.
	 * mbs_wc_product_id postmeta stores the link.
	 *
	 * Also writes mbs_book_id as product postmeta so the WC REST API exposes
	 * the reverse link back to the MBS book (used by FooSales + Square).
	 *
	 * @param int $post_id The mbs_book post ID.
	 * @return int WooCommerce product ID, or 0 on failure.
	 */
	public static function create_or_link_product( int $post_id ): int {

		// Return existing linked product ID if already valid.
		$existing = (int) get_post_meta( $post_id, 'mbs_wc_product_id', true );
		if ( $existing > 0 && 'product' === get_post_type( $existing ) ) {
			return $existing;
		}

		$book_title = get_post_meta( $post_id, 'mbs_book_title', true )
			?: get_the_title( $post_id );

		$product = new WC_Product_Simple();
		$product->set_name( $book_title );
		$product->set_status( 'publish' );
		$product->set_virtual( true );
		$product->set_catalog_visibility( 'hidden' ); // Keep out of /shop/ query.

		// Set SKU to the first ISBN-13 found so barcode scanners can look up
		// the product by ISBN (used by FooSales and Square Terminal).
		$isbn = self::get_primary_isbn( $post_id );
		if ( $isbn ) {
			$product->set_sku( $isbn );
		}

		$product_id = $product->save();

		if ( ! $product_id ) {
			return 0;
		}

		// Write reverse link: WC product meta -> MBS book post ID.
		update_post_meta( $product_id, 'mbs_book_id', $post_id );

		update_post_meta( $post_id, 'mbs_wc_product_id', $product_id );

		delete_transient( 'mbs_wc_linked_product_ids' );

		return $product_id;
	}

	// =========================================================================
	// Price sync (POS-1)
	// =========================================================================

	/**
	 * Sync the lowest non-null price from the book's active format rows to the
	 * linked WooCommerce product's regular price.
	 *
	 * Skips silently when:
	 *   - mbs_wc_product_id is not set or the WC product no longer exists.
	 *   - No format rows have a price (WC product price is left unchanged).
	 *
	 * @param int $post_id The mbs_book post ID.
	 * @return void
	 */
	public static function sync_price_to_product( int $post_id ): void {
		$product = self::get_product_for_book( $post_id );
		if ( ! $product ) {
			return;
		}

		$rows  = MBS_Format_Row_DAO::get_by_post( $post_id ) ?: array();
		$price = null;
		$isbn  = '';

		foreach ( $rows as $row ) {
			// Track the lowest non-null price for the regular_price field.
			if ( null !== $row->price && ( null === $price || (float) $row->price < $price ) ) {
				$price = (float) $row->price;
			}
			// First non-empty ISBN wins as the SKU.
			if ( '' === $isbn && ! empty( $row->isbn13 ) ) {
				$isbn = $row->isbn13;
			}
		}

		$changed = false;

		if ( null !== $price ) {
			$product->set_regular_price( number_format( $price, 2, '.', '' ) );
			$changed = true;
		}

		if ( $isbn && $product->get_sku() !== $isbn ) {
			// Only update SKU when it differs — avoids triggering WC stock events.
			try {
				$product->set_sku( $isbn );
				$changed = true;
			} catch ( \WC_Data_Exception $e ) {
				// SKU already in use on another product; leave it unchanged.
			}
		}

		if ( $changed ) {
			$product->save();
		}
	}

	// =========================================================================
	// Auto-populate + price sync hook (POS-1 + POS-2)
	// =========================================================================

	/**
	 * Hooked to save_post_{MBS_POST_TYPE} at priority 20 (after format rows
	 * are saved at priority 10 by MBS_Metabox_Format_Repeater).
	 *
	 * When mbs_wc_enabled is true and the book has at least one priced format
	 * row: ensures the WC product exists (auto-populates mbs_wc_product_id if
	 * missing) then syncs price + SKU to the WC product.
	 *
	 * @param int      $post_id Post ID.
	 * @param \WP_Post $post    Post object.
	 * @return void
	 */
	public function auto_populate_product( int $post_id, \WP_Post $post ): void {
		// Bail on autosave / revisions / wrong type.
		if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
			return;
		}
		if ( MBS_POST_TYPE !== $post->post_type ) {
			return;
		}
		if ( ! (bool) get_post_meta( $post_id, 'mbs_wc_enabled', true ) ) {
			return;
		}

		// Ensure the product exists (idempotent).
		self::create_or_link_product( $post_id );

		// Sync price + SKU from format rows.
		self::sync_price_to_product( $post_id );
	}

	// =========================================================================
	// Shop suppression
	// =========================================================================

	/**
	 * Exclude mbs_book-linked products from the /shop/ archive query.
	 *
	 * @param \WP_Query $query The current query.
	 * @return void
	 */
	public function suppress_from_shop( \WP_Query $query ): void {
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		if ( ! $query->is_post_type_archive( 'product' ) && ! $query->is_shop() ) {
			return;
		}

		$cache_key   = 'mbs_wc_linked_product_ids';
		$product_ids = get_transient( $cache_key );
		if ( false === $product_ids ) {
			$linked_ids = get_posts( array(
				'post_type'      => MBS_POST_TYPE,
				'post_status'    => 'any',
				'posts_per_page' => -1,
				'fields'         => 'ids',
				'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- standard WP query pattern; VIP rule does not apply to this plugin
					array(
						'key'     => 'mbs_wc_product_id',
						'compare' => 'EXISTS',
					),
				),
			) );

			$product_ids = array();
			foreach ( $linked_ids as $book_id ) {
				$pid = (int) get_post_meta( $book_id, 'mbs_wc_product_id', true );
				if ( $pid > 0 ) {
					$product_ids[] = $pid;
				}
			}
			set_transient( $cache_key, $product_ids, HOUR_IN_SECONDS );
		}

		if ( ! empty( $product_ids ) ) {
			$existing = (array) $query->get( 'post__not_in' );
			$query->set( 'post__not_in', array_merge( $existing, $product_ids ) );
		}
	}

	// =========================================================================
	// WC REST API meta registration (POS-1)
	// =========================================================================

	/**
	 * Register mbs_book_id as a product postmeta field exposed via the
	 * WooCommerce REST API.
	 *
	 * FooSales and Square for WooCommerce fetch products through the WC REST
	 * API; by exposing mbs_book_id on the product object they can resolve the
	 * MBS book record for any WC product in the catalog.
	 *
	 * Hooked to init (called from __construct via add_action).
	 *
	 * @return void
	 */
	public static function register_book_meta(): void {
		register_post_meta( 'product', 'mbs_book_id', array(
			'type'              => 'integer',
			'description'       => 'MBS book post ID linked to this WooCommerce product.',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'absint',
			'auth_callback'     => static function (): bool {
				return current_user_can( 'edit_posts' );
			},
		) );
	}

	// =========================================================================
	// Helpers
	// =========================================================================

	/**
	 * Return the WC_Product for a book post, or null if none is linked or WC
	 * cannot load the product.
	 *
	 * @param int $post_id The mbs_book post ID.
	 * @return \WC_Product|null
	 */
	public static function get_product_for_book( int $post_id ): ?\WC_Product {
		$product_id = (int) get_post_meta( $post_id, 'mbs_wc_product_id', true );
		if ( $product_id <= 0 ) {
			return null;
		}

		$product = wc_get_product( $product_id );

		return ( $product instanceof \WC_Product ) ? $product : null;
	}

	/**
	 * Return the first non-empty ISBN-13 from a book's active format rows.
	 *
	 * @param int $post_id The mbs_book post ID.
	 * @return string ISBN-13 string, or '' if none found.
	 */
	private static function get_primary_isbn( int $post_id ): string {
		$rows = MBS_Format_Row_DAO::get_by_post( $post_id ) ?: array();
		foreach ( $rows as $row ) {
			if ( ! empty( $row->isbn13 ) ) {
				return (string) $row->isbn13;
			}
		}
		return '';
	}

	// =========================================================================
	// Bootstrap
	// =========================================================================

	public function __construct() {
		add_action( 'pre_get_posts',                      array( $this, 'suppress_from_shop' ) );
		add_action( 'save_post_' . MBS_POST_TYPE,         array( $this, 'auto_populate_product' ), 20, 2 );
		add_action( 'init',                               array( __CLASS__, 'register_book_meta' ) );
	}
}
