<?php
/**
 * Product Profile Builder
 *
 * Transforms WooCommerce products into AI profiles for indexing.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 * @updated 2.5.0 Added field filtering support for multi-profile indexing
 */

namespace AIPT\Api\StoreIndex;

/**
 * ProductProfileBuilder class
 *
 * Converts WooCommerce product data into AI-consumable profile format.
 */
class ProductProfileBuilder {

	/**
	 * Maximum summary length in characters.
	 */
	private const MAX_SUMMARY_LENGTH = 200;

	/**
	 * Build AI profile from product data.
	 *
	 * @param \WC_Product|object $product              Product object or raw DB row.
	 * @param array              $attributes           Attribute slugs to include.
	 * @param array              $prefetched_attributes Pre-fetched attributes from bulk query (optional).
	 * @param array              $included_fields      Fields to include in the profile.
	 * @return array AIProfile structure.
	 */
	public function build( $product, array $attributes = array(), array $prefetched_attributes = array(), array $included_fields = array() ): array {

		if ( empty( $included_fields ) ) {
			$included_fields = ProfileManager::getDefaultIncludedFields();
		}

		$included_fields = ProfileManager::ensureMandatoryFields( $included_fields );

		if ( $product instanceof \WC_Product ) {
			return $this->buildFromWCProduct( $product, $attributes, $prefetched_attributes, $included_fields );
		}

		return $this->buildFromRawData( $product, $attributes, $prefetched_attributes, $included_fields );
	}

	/**
	 * Build profiles for multiple products (batch).
	 *
	 * Uses bulk attribute fetching for efficiency when processing raw DB rows.
	 *
	 * @param array $products        Array of products or raw DB rows.
	 * @param array $attributes      Attribute slugs to include.
	 * @param array $included_fields Fields to include in the profile.
	 * @return array<int, array> Map of product_id => AIProfile.
	 */
	public function buildBatch( array $products, array $attributes = array(), array $included_fields = array() ): array {
		$profiles = array();

		if ( empty( $included_fields ) ) {
			$included_fields = ProfileManager::getDefaultIncludedFields();
		}

		$included_fields = ProfileManager::ensureMandatoryFields( $included_fields );

		$product_ids = array();
		foreach ( $products as $product ) {
			if ( $product instanceof \WC_Product ) {
				$product_ids[] = $product->get_id();
			} else {
				$product_ids[] = (int) $product->ID;
			}
		}

		if ( ! empty( $product_ids ) ) {
			update_meta_cache( 'post', $product_ids );
		}

		$batch_attributes = array();
		if ( ! empty( $attributes ) && ! empty( $product_ids ) && in_array( 'attributes', $included_fields, true ) ) {
			$batch_attributes = $this->getBatchAttributes( $product_ids, $attributes );
		}

		foreach ( $products as $product ) {
			$product_id = $product instanceof \WC_Product
				? $product->get_id()
				: (int) $product->ID;

			$profile                    = $this->build( $product, $attributes, $batch_attributes[ $product_id ] ?? array(), $included_fields );
			$profiles[ $profile['id'] ] = $profile;
		}

		return $profiles;
	}

	/**
	 * Fetch attributes for multiple products in bulk.
	 *
	 * Uses efficient SQL queries to fetch taxonomy and custom attributes
	 * for an entire batch, avoiding N+1 query problems.
	 *
	 * @param array $product_ids Array of product IDs.
	 * @param array $attributes  Attribute slugs to include.
	 * @return array<int, array<string, string>> Map of product_id => [slug => value].
	 */
	public function getBatchAttributes( array $product_ids, array $attributes ): array {
		if ( empty( $product_ids ) || empty( $attributes ) ) {
			return array();
		}

		global $wpdb;

		$result = array_fill_keys( $product_ids, array() );

		$taxonomy_attrs = array_filter(
			$attributes,
			function ( $attr ) {
				return strpos( $attr, 'pa_' ) === 0;
			}
		);

		if ( ! empty( $taxonomy_attrs ) ) {
			$id_placeholders  = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) );
			$tax_placeholders = implode( ',', array_fill( 0, count( $taxonomy_attrs ), '%s' ) );

			$query = $wpdb->prepare(
				"SELECT tr.object_id, t.name, tt.taxonomy
                 FROM {$wpdb->term_relationships} tr
                 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
                 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
                 WHERE tr.object_id IN ({$id_placeholders})
                   AND tt.taxonomy IN ({$tax_placeholders})",
				...array_merge( $product_ids, $taxonomy_attrs )
			);

			$rows = $wpdb->get_results( $query );

			$temp_results = array();
			foreach ( $rows as $row ) {
				$pid  = (int) $row->object_id;
				$attr = $row->taxonomy;

				if ( ! isset( $temp_results[ $pid ] ) ) {
					$temp_results[ $pid ] = array();
				}
				if ( ! isset( $temp_results[ $pid ][ $attr ] ) ) {
					$temp_results[ $pid ][ $attr ] = array();
				}
				$temp_results[ $pid ][ $attr ][] = $row->name;
			}

			foreach ( $temp_results as $pid => $attrs ) {
				foreach ( $attrs as $attr_key => $values ) {
					$result[ $pid ][ $attr_key ] = implode( ', ', $values );
				}
			}
		}

		$custom_attrs = array_filter(
			$attributes,
			function ( $attr ) {
				return strpos( $attr, 'pa_' ) !== 0;
			}
		);

		if ( ! empty( $custom_attrs ) ) {
			$id_placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) );

			$query = $wpdb->prepare(
				"SELECT post_id, meta_value
                 FROM {$wpdb->postmeta}
                 WHERE post_id IN ({$id_placeholders})
                   AND meta_key = '_product_attributes'",
				...$product_ids
			);

			$rows = $wpdb->get_results( $query );

			foreach ( $rows as $row ) {
				$pid   = (int) $row->post_id;
				$attrs = maybe_unserialize( $row->meta_value );

				if ( ! is_array( $attrs ) ) {
					continue;
				}

				foreach ( $custom_attrs as $slug ) {
					if ( isset( $attrs[ $slug ]['value'] ) && ! empty( $attrs[ $slug ]['value'] ) ) {
						$result[ $pid ][ $slug ] = $attrs[ $slug ]['value'];
					}
				}
			}
		}

		return $result;
	}

	/**
	 * Generate summary from product description.
	 *
	 * Prefers short description, falls back to truncated long description.
	 *
	 * @param string $short_description Short description.
	 * @param string $long_description  Long description.
	 * @param int    $max_length        Maximum summary length (default 200).
	 * @return string Truncated summary.
	 */
	public function generateSummary(
		string $short_description,
		string $long_description,
		int $max_length = self::MAX_SUMMARY_LENGTH
	): string {

		$text = ! empty( $short_description ) ? $short_description : $long_description;

		$text = wp_strip_all_tags( $text );
		$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );

		$text = preg_replace( '/\s+/', ' ', $text );
		$text = trim( $text );

		if ( mb_strlen( $text ) > $max_length ) {

			$text       = mb_substr( $text, 0, $max_length );
			$last_space = mb_strrpos( $text, ' ' );
			if ( $last_space !== false && $last_space > ( $max_length * 0.7 ) ) {
				$text = mb_substr( $text, 0, $last_space );
			}
			$text = rtrim( $text, '.,!? ' ) . '...';
		}

		return $text;
	}

	/**
	 * Build profile from WC_Product object.
	 *
	 * @param \WC_Product $product              WooCommerce product object.
	 * @param array       $attributes           Attribute slugs to include.
	 * @param array       $prefetched_attributes Pre-fetched attributes from bulk query (optional).
	 * @param array       $included_fields      Fields to include in the profile.
	 * @return array AIProfile structure.
	 */
	private function buildFromWCProduct( \WC_Product $product, array $attributes = array(), array $prefetched_attributes = array(), array $included_fields = array() ): array {
		$product_id = $product->get_id();

		$profile = array(
			'id' => $product_id,
		);

		if ( in_array( 'sku', $included_fields, true ) ) {
			$profile['sku'] = $product->get_sku();
		}

		if ( in_array( 'title', $included_fields, true ) ) {
			$profile['title'] = $product->get_name();
		}

		if ( in_array( 'short_description', $included_fields, true ) ) {
			$profile['short_description'] = wp_strip_all_tags( $product->get_short_description() );
		}

		if ( in_array( 'description', $included_fields, true ) ) {
			$profile['description'] = wp_strip_all_tags( $product->get_description() );
		}

		if ( in_array( 'price', $included_fields, true ) ) {
			$profile['price']         = (float) $product->get_price();
			$profile['regular_price'] = (float) $product->get_regular_price();
			$sale_price               = $product->get_sale_price();
			if ( ! empty( $sale_price ) ) {
				$profile['sale_price'] = (float) $sale_price;
			}
		}

		if ( in_array( 'stock_status', $included_fields, true ) ) {
			$profile['stock_status'] = $product->get_stock_status();
		}

		if ( in_array( 'stock_quantity', $included_fields, true ) ) {
			$quantity = $product->get_stock_quantity();
			if ( $quantity !== null ) {
				$profile['stock_quantity'] = (int) $quantity;
			}
		}

		if ( in_array( 'categories', $included_fields, true ) ) {
			$profile['categories'] = $this->getProductCategoryIds( $product_id );
		}

		if ( in_array( 'tags', $included_fields, true ) ) {
			$profile['tags'] = $this->getProductTagIds( $product_id );
		}

		if ( in_array( 'attributes', $included_fields, true ) ) {

			if ( ! empty( $prefetched_attributes ) ) {
				$profile['attributes'] = $prefetched_attributes;
			} elseif ( ! empty( $attributes ) ) {
				$profile['attributes'] = $this->getProductAttributes( $product, $attributes );
			}
		}

		if ( in_array( 'short_description', $included_fields, true ) || in_array( 'description', $included_fields, true ) ) {
			$profile['summary'] = $this->generateSummary(
				$product->get_short_description(),
				$product->get_description()
			);
		}

		/**
		 * Filter the AI profile before indexing.
		 *
		 * @param array $profile         The generated profile.
		 * @param int   $product_id      The product ID.
		 * @param array $included_fields Fields included in this profile.
		 */
		return apply_filters( 'aipt_store_index_profile', $profile, $product_id, $included_fields );
	}

	/**
	 * Build profile from raw database row.
	 *
	 * Used for batch processing with direct SQL queries.
	 *
	 * @param object $row                   Raw database row object.
	 * @param array  $attributes            Attribute slugs to include.
	 * @param array  $prefetched_attributes Pre-fetched attributes from bulk query (optional).
	 * @param array  $included_fields       Fields to include in the profile.
	 * @return array AIProfile structure.
	 */
	private function buildFromRawData( object $row, array $attributes = array(), array $prefetched_attributes = array(), array $included_fields = array() ): array {
		$product_id = (int) $row->ID;

		$profile = array(
			'id' => $product_id,
		);

		if ( in_array( 'sku', $included_fields, true ) ) {
			$profile['sku'] = get_post_meta( $product_id, '_sku', true );
		}

		if ( in_array( 'title', $included_fields, true ) ) {
			$profile['title'] = $row->post_title;
		}

		if ( in_array( 'short_description', $included_fields, true ) ) {
			$profile['short_description'] = wp_strip_all_tags( $row->post_excerpt ?? '' );
		}

		if ( in_array( 'description', $included_fields, true ) ) {
			$profile['description'] = wp_strip_all_tags( $row->post_content ?? '' );
		}

		if ( in_array( 'price', $included_fields, true ) ) {
			$profile['price']         = isset( $row->_price ) ? (float) $row->_price : 0.0;
			$regular_price            = get_post_meta( $product_id, '_regular_price', true );
			$sale_price               = get_post_meta( $product_id, '_sale_price', true );
			$profile['regular_price'] = ! empty( $regular_price ) ? (float) $regular_price : 0.0;
			if ( ! empty( $sale_price ) ) {
				$profile['sale_price'] = (float) $sale_price;
			}
		}

		if ( in_array( 'stock_status', $included_fields, true ) ) {
			$profile['stock_status'] = isset( $row->_stock_status ) ? $row->_stock_status : 'instock';
		}

		if ( in_array( 'stock_quantity', $included_fields, true ) ) {
			$quantity = get_post_meta( $product_id, '_stock', true );
			if ( $quantity !== '' && $quantity !== null ) {
				$profile['stock_quantity'] = (int) $quantity;
			}
		}

		if ( in_array( 'categories', $included_fields, true ) ) {
			$profile['categories'] = $this->getProductCategoryIds( $product_id );
		}

		if ( in_array( 'tags', $included_fields, true ) ) {
			$profile['tags'] = $this->getProductTagIds( $product_id );
		}

		if ( in_array( 'attributes', $included_fields, true ) ) {

			if ( ! empty( $prefetched_attributes ) ) {
				$profile['attributes'] = $prefetched_attributes;
			} elseif ( ! empty( $attributes ) ) {
				$profile['attributes'] = $this->getProductAttributesById( $product_id, $attributes );
			}
		}

		if ( in_array( 'short_description', $included_fields, true ) || in_array( 'description', $included_fields, true ) ) {
			$profile['summary'] = $this->generateSummary(
				$row->post_excerpt ?? '',
				$row->post_content ?? ''
			);
		}

		/**
		 * Filter the AI profile before indexing.
		 *
		 * @param array $profile         The generated profile.
		 * @param int   $product_id      The product ID.
		 * @param array $included_fields Fields included in this profile.
		 */
		return apply_filters( 'aipt_store_index_profile', $profile, $product_id, $included_fields );
	}

	/**
	 * Get category IDs for a product.
	 *
	 * @param int $product_id Product ID.
	 * @return array Array of category term IDs.
	 */
	private function getProductCategoryIds( int $product_id ): array {
		$terms = get_the_terms( $product_id, 'product_cat' );

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		return array_map(
			function ( $term ) {
				return (int) $term->term_id;
			},
			$terms
		);
	}

	/**
	 * Get tag IDs for a product.
	 *
	 * @param int $product_id Product ID.
	 * @return array Array of tag term IDs.
	 */
	private function getProductTagIds( int $product_id ): array {
		$terms = get_the_terms( $product_id, 'product_tag' );

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		return array_map(
			function ( $term ) {
				return (int) $term->term_id;
			},
			$terms
		);
	}

	/**
	 * Get product attributes from WC_Product.
	 *
	 * @param \WC_Product $product    Product object.
	 * @param array       $attributes Attribute slugs to include.
	 * @return array<string, string> Map of attribute slug => value.
	 */
	private function getProductAttributes( \WC_Product $product, array $attributes ): array {
		$result = array();

		foreach ( $attributes as $slug ) {
			$value = $product->get_attribute( $slug );
			if ( ! empty( $value ) ) {
				$result[ $slug ] = $value;
			}
		}

		return $result;
	}

	/**
	 * Get product attributes by product ID.
	 *
	 * Used when working with raw database rows.
	 * Note: For batch processing, prefer getBatchAttributes() to avoid N+1 queries.
	 * This method is kept for backward compatibility and single-product use cases.
	 *
	 * @deprecated Use getBatchAttributes() for batch processing.
	 * @param int   $product_id Product ID.
	 * @param array $attributes Attribute slugs to include.
	 * @return array<string, string> Map of attribute slug => value.
	 */
	private function getProductAttributesById( int $product_id, array $attributes ): array {
		$result = array();

		foreach ( $attributes as $slug ) {

			if ( strpos( $slug, 'pa_' ) === 0 ) {
				$terms = get_the_terms( $product_id, $slug );
				if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
					$values          = wp_list_pluck( $terms, 'name' );
					$result[ $slug ] = implode( ', ', $values );
					continue;
				}
			}

			$product_attributes = get_post_meta( $product_id, '_product_attributes', true );
			if ( is_array( $product_attributes ) && isset( $product_attributes[ $slug ]['value'] ) ) {
				$result[ $slug ] = $product_attributes[ $slug ]['value'];
			}
		}

		return $result;
	}

	/**
	 * Map stock status to standardized values.
	 *
	 * @param string|null $status Raw stock status.
	 * @return string Normalized stock status.
	 */
	public function normalizeStockStatus( ?string $status ): string {
		$valid_statuses = array( 'instock', 'outofstock', 'onbackorder' );

		if ( $status && in_array( $status, $valid_statuses, true ) ) {
			return $status;
		}

		return 'instock';
	}
}
