<?php
/**
 * Fetches the live WooCommerce catalog for CandidateIndex.
 *
 * @package EmberPrice
 */

namespace EmberPrice\Matching;

defined( 'ABSPATH' ) || exit;

/**
 * The only WordPress/WooCommerce-dependent piece of the catalog indexing
 * path (CandidateIndex itself stays pure). Streams published products in
 * batches of 500 so a large catalog is never fully materialized in memory
 * at once, and resolves GTIN/brand from WooCommerce 9+'s native field, a
 * handful of common third-party barcode plugin meta keys, and the
 * `product_brand` taxonomy when it's registered.
 */
final class WooCommerceCatalogRepository implements CatalogRepositoryInterface {

	private const BATCH_SIZE = 500;

	/**
	 * Checked in order after the native WooCommerce 9+ field; first
	 * non-empty value wins.
	 */
	private const GTIN_META_KEYS = array(
		'_global_unique_id', // Native WooCommerce 9+ GTIN/UPC/EAN/ISBN field.
		'_alg_ean',          // Barcodes for WooCommerce, by Algoritmika.
		'_wpm_gtin_code',
		'_ean',
		'_gtin',
		'_upc',
		'_isbn',
		'_barcode',
	);

	/**
	 * {@inheritDoc}
	 */
	public function get_products(): iterable {
		$page = 1;

		do {
			$products    = wc_get_products(
				array(
					'status'  => 'publish',
					'limit'   => self::BATCH_SIZE,
					'page'    => $page,
					'orderby' => 'ID',
					'order'   => 'ASC',
					'return'  => 'objects',
				)
			);
			$batch_count = count( $products );

			foreach ( $products as $product ) {
				yield $this->to_row( $product );
			}

			++$page;
		} while ( self::BATCH_SIZE === $batch_count );
	}

	/**
	 * Converts one WC_Product into the raw row shape CandidateIndex expects.
	 *
	 * @param \WC_Product $product A published product (simple or variable).
	 */
	private function to_row( \WC_Product $product ): array {
		$row = array(
			'product_id' => $product->get_id(),
			'sku'        => $product->get_sku(),
			'gtin'       => $this->gtin_for( $product->get_id() ),
			'brand'      => $this->brand_for( $product->get_id() ),
			'title'      => $product->get_name(),
		);

		if ( $product instanceof \WC_Product_Variable ) {
			$row['variations'] = $this->variations_for( $product );
		}

		return $row;
	}

	/**
	 * Flattens a variable product's children into raw variation rows.
	 *
	 * @param \WC_Product_Variable $product Parent variable product.
	 *
	 * @return array<int, array{variation_id: int, sku: string, gtin: string, title: string}>
	 */
	private function variations_for( \WC_Product_Variable $product ): array {
		$variations = array();

		foreach ( $product->get_children() as $variation_id ) {
			$variation = wc_get_product( $variation_id );

			if ( ! $variation instanceof \WC_Product_Variation ) {
				continue;
			}

			$variations[] = array(
				'variation_id' => $variation->get_id(),
				'sku'          => $variation->get_sku(),
				'gtin'         => $this->gtin_for( $variation->get_id() ),
				'title'        => $variation->get_name(),
			);
		}

		return $variations;
	}

	/**
	 * Resolves the first non-empty GTIN among the known meta keys.
	 *
	 * @param int $product_id Product or variation id.
	 */
	private function gtin_for( int $product_id ): string {
		foreach ( self::GTIN_META_KEYS as $meta_key ) {
			$value = get_post_meta( $product_id, $meta_key, true );

			if ( is_string( $value ) && '' !== trim( $value ) ) {
				return trim( $value );
			}
		}

		return '';
	}

	/**
	 * Resolves the product's `product_brand` term name, if the taxonomy exists.
	 *
	 * @param int $product_id Product id (variations don't carry their own brand).
	 */
	private function brand_for( int $product_id ): string {
		if ( ! taxonomy_exists( 'product_brand' ) ) {
			return '';
		}

		$terms = get_the_terms( $product_id, 'product_brand' );

		if ( ! is_array( $terms ) || empty( $terms ) ) {
			return '';
		}

		return (string) $terms[0]->name;
	}
}
